@cachly-dev/mcp-server 0.10.64 โ†’ 0.10.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/dist/src/handlers/brain.js +1 -1
  2. package/dist/src/index.js +1 -1
  3. package/package.json +7 -1
  4. package/server.json +2 -2
  5. package/.env.test.example +0 -23
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  7. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  9. package/.github/workflows/eas-build-android.yml +0 -58
  10. package/.github/workflows/eas-update.yml +0 -55
  11. package/.github/workflows/publish.yml +0 -72
  12. package/BENCH.md +0 -91
  13. package/CHANGELOG.md +0 -604
  14. package/CHANGELOG.md.tmp +0 -0
  15. package/Dockerfile +0 -16
  16. package/LAUNCH_TEXTS.md +0 -182
  17. package/PROGRESS.md +0 -137
  18. package/STRATEGY.md +0 -228
  19. package/VISION_10X.md +0 -133
  20. package/apps/travel-chaos-organizer/.env.example +0 -49
  21. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  22. package/apps/travel-chaos-organizer/Makefile +0 -57
  23. package/apps/travel-chaos-organizer/README.md +0 -159
  24. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  25. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  26. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  27. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  28. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  29. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  30. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  31. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  32. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  33. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  34. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  35. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  36. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  37. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  38. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  39. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  40. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  41. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  42. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  43. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  44. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  45. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  46. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  47. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  48. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  49. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  50. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  51. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  52. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  53. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  54. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  55. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  56. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  57. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  58. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  59. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  60. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  61. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  62. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  63. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  64. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  65. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  66. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  67. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  68. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  69. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  70. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  71. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  72. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  73. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  74. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  75. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  76. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  77. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  78. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  79. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  80. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  81. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  82. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  83. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  84. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  85. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  86. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  87. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  88. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  89. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  90. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  91. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  92. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  93. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  94. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  95. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  96. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  97. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  98. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  99. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  100. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  101. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  102. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  103. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  104. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  105. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  106. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  107. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  108. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  109. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  110. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  111. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  112. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  113. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  114. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  115. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  116. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  117. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  118. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  119. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  120. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  121. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  122. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  123. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  124. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  125. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  126. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  127. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  128. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  129. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  130. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  131. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  132. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  133. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  134. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  135. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  136. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  137. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  138. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  139. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  140. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  141. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  142. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  143. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  144. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  145. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  146. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  147. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  148. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  149. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  150. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  151. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  152. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  153. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  154. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  155. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  156. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  157. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  158. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  159. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  160. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  161. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  162. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  163. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  164. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  165. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  166. package/dist/src/__tests__/auth.test.d.ts +0 -1
  167. package/dist/src/__tests__/auth.test.js +0 -107
  168. package/dist/src/__tests__/auth.test.js.map +0 -1
  169. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  170. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  171. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  172. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  173. package/dist/src/__tests__/confidence.test.js +0 -233
  174. package/dist/src/__tests__/confidence.test.js.map +0 -1
  175. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  176. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  177. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  178. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  179. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  180. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  181. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  182. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  183. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  184. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  185. package/dist/src/__tests__/e2e/config.js +0 -53
  186. package/dist/src/__tests__/e2e/config.js.map +0 -1
  187. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  188. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  189. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  190. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  191. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  192. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  193. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  194. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  195. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  196. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  197. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  198. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  199. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  200. package/dist/src/__tests__/namespace.test.js +0 -114
  201. package/dist/src/__tests__/namespace.test.js.map +0 -1
  202. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  203. package/dist/src/__tests__/rerank.test.js +0 -115
  204. package/dist/src/__tests__/rerank.test.js.map +0 -1
  205. package/dist/src/__tests__/utils.test.d.ts +0 -1
  206. package/dist/src/__tests__/utils.test.js +0 -83
  207. package/dist/src/__tests__/utils.test.js.map +0 -1
  208. package/dist/src/bench/cachly-bench.d.ts +0 -35
  209. package/dist/src/bench/cachly-bench.js +0 -182
  210. package/dist/src/bench/cachly-bench.js.map +0 -1
  211. package/dist/src/bench/fixtures.d.ts +0 -37
  212. package/dist/src/bench/fixtures.js +0 -225
  213. package/dist/src/bench/fixtures.js.map +0 -1
  214. package/dist/src/search.test.d.ts +0 -1
  215. package/dist/src/search.test.js +0 -713
  216. package/dist/src/search.test.js.map +0 -1
  217. package/eslint.config.js +0 -16
  218. package/glama.json +0 -36
  219. package/llms.txt +0 -155
  220. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  221. package/packages/telegram-notify/client.ts +0 -88
  222. package/packages/telegram-notify/emojis.json +0 -22
  223. package/packages/telegram-notify/pyproject.toml +0 -17
  224. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  225. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  226. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  227. package/scripts/e2e-run.sh +0 -160
  228. package/scripts/generate-icons.js +0 -28
  229. package/smithery.yaml +0 -45
  230. package/src/__tests__/auth.test.ts +0 -147
  231. package/src/__tests__/brain-flow.test.ts +0 -1472
  232. package/src/__tests__/confidence.test.ts +0 -285
  233. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  234. package/src/__tests__/e2e/api-health.test.ts +0 -103
  235. package/src/__tests__/e2e/cli.test.ts +0 -228
  236. package/src/__tests__/e2e/config.ts +0 -53
  237. package/src/__tests__/e2e/funnel.test.ts +0 -99
  238. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  239. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  240. package/src/__tests__/handlers-unit.test.ts +0 -677
  241. package/src/__tests__/namespace.test.ts +0 -126
  242. package/src/__tests__/rerank.test.ts +0 -130
  243. package/src/__tests__/utils.test.ts +0 -81
  244. package/src/auth.ts +0 -46
  245. package/src/bench/cachly-bench.ts +0 -217
  246. package/src/bench/fixtures.ts +0 -256
  247. package/src/ckg.ts +0 -103
  248. package/src/confidence.ts +0 -38
  249. package/src/embeddings.ts +0 -217
  250. package/src/handlers/advanced.ts +0 -647
  251. package/src/handlers/brain.ts +0 -2622
  252. package/src/handlers/cache.ts +0 -633
  253. package/src/handlers/context.ts +0 -222
  254. package/src/handlers/fedbrain.ts +0 -1232
  255. package/src/handlers/instances.ts +0 -180
  256. package/src/handlers/roadmap.ts +0 -201
  257. package/src/handlers/syndicate.ts +0 -460
  258. package/src/handlers/tco.ts +0 -345
  259. package/src/handlers/team.ts +0 -1249
  260. package/src/index.ts +0 -2815
  261. package/src/namespace.ts +0 -128
  262. package/src/notifier.ts +0 -5
  263. package/src/rerank.ts +0 -148
  264. package/src/search.test.ts +0 -848
  265. package/src/search.ts +0 -1781
  266. package/src/tools.ts +0 -2056
  267. package/src/utils.ts +0 -90
  268. package/test-features.cjs +0 -23
  269. package/tsconfig.json +0 -16
  270. package/vitest.config.ts +0 -14
@@ -1,1249 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import type { Redis } from 'ioredis';
4
- import { calculateConfidence, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_VALUE,
5
- CONFIDENCE_WARN_DAYS, CONFIDENCE_STALE_DAYS } from '../confidence.js';
6
- import type { Instance } from './brain.js';
7
- import { safeJsonParse } from '../utils.js';
8
-
9
- type GetConnection = (instanceId: string) => Promise<Redis>;
10
- type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
11
-
12
- export const TEAM_TOOL_NAMES = new Set([
13
- 'sync_file_changes', 'team_learn', 'team_confirm', 'team_recall', 'team_synthesize', 'memory_crystalize',
14
- 'brain_doctor', 'recall_at', 'trace_dependency', 'global_learn', 'global_recall',
15
- 'publish_lesson', 'import_public_brain', 'setup_ai_memory',
16
- ]);
17
-
18
- export async function handleTeamTool(
19
- name: string,
20
- args: Record<string, unknown>,
21
- getConnection: GetConnection,
22
- apiFetch: ApiFetch,
23
- ): Promise<string | null> {
24
- switch (name) {
25
- case 'sync_file_changes': {
26
- const { instance_id, changed_files, git_diff_stat, commit_msg } = args as {
27
- instance_id: string;
28
- changed_files: string[];
29
- git_diff_stat?: string;
30
- commit_msg?: string;
31
- };
32
- const redis = await getConnection(instance_id);
33
-
34
- // Store file change event in session history
35
- const changeRecord = {
36
- ts: new Date().toISOString(),
37
- files: changed_files,
38
- commit_msg,
39
- diff_stat: git_diff_stat?.slice(0, 500),
40
- };
41
- await redis.lpush('cachly:session:file_changes', JSON.stringify(changeRecord));
42
- await redis.ltrim('cachly:session:file_changes', 0, 99);
43
- await redis.expire('cachly:session:file_changes', 30 * 86400);
44
-
45
- // Find lessons relevant to the changed files
46
- const lessonKeys: string[] = [];
47
- const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
48
- await new Promise<void>((resolve, reject) => {
49
- lStream.on('data', (batch: string[]) => lessonKeys.push(...batch));
50
- lStream.on('end', resolve);
51
- lStream.on('error', reject);
52
- });
53
-
54
- type Lesson = { topic: string; what_worked: string; outcome: string; file_paths?: string[] };
55
- const relevant: string[] = [];
56
- const sfRaws = lessonKeys.length > 0 ? await redis.mget(...lessonKeys) : [];
57
- for (const raw of sfRaws) {
58
- if (!raw) continue;
59
- const lesson = safeJsonParse<Lesson | null>(raw, null);
60
- if (!lesson) continue;
61
- // Match by file_paths stored in lesson OR by topic keywords matching file name
62
- const topicWords = lesson.topic.toLowerCase().split(/[:\-_]/);
63
- const fileMatches = changed_files.some(f => {
64
- const fname = f.split('/').pop()?.replace(/\.[^.]+$/, '').toLowerCase() ?? '';
65
- return topicWords.some(w => w.length > 3 && fname.includes(w))
66
- || (lesson.file_paths ?? []).some(lf => f.includes(lf) || lf.includes(f));
67
- });
68
- if (fileMatches) {
69
- const emoji = lesson.outcome === 'success' ? 'โœ…' : 'โš ๏ธ';
70
- relevant.push(` ${emoji} \`${lesson.topic}\` โ€” ${lesson.what_worked.slice(0, 80)}`);
71
- }
72
- }
73
-
74
- const lines = [
75
- `๐Ÿ“ **File sync recorded**: ${changed_files.length} files`,
76
- commit_msg ? `๐Ÿ“ Commit: "${commit_msg}"` : '',
77
- '',
78
- `**Changed:** ${changed_files.slice(0, 8).map(f => `\`${f}\``).join(', ')}${changed_files.length > 8 ? ` +${changed_files.length - 8} more` : ''}`,
79
- '',
80
- ];
81
- if (relevant.length > 0) {
82
- lines.push(`๐Ÿง  **Relevant brain lessons (${relevant.length}):**`, ...relevant);
83
- } else {
84
- lines.push(`๐Ÿ’ก No existing lessons match these files yet. Add them with \`learn_from_attempts\`.`);
85
- }
86
- return lines.filter(Boolean).join('\n');
87
- }
88
-
89
- // โ”€โ”€ team_learn โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
90
- case 'team_learn': {
91
- const { instance_id, author, topic, outcome, what_worked, what_failed, severity, file_paths, commands, tags } = args as {
92
- instance_id: string; author: string; topic: string; outcome: string;
93
- what_worked: string; what_failed?: string; severity?: string;
94
- file_paths?: string[]; commands?: string[]; tags?: string[];
95
- };
96
- if (!author || !topic || !outcome || !what_worked) {
97
- return 'โŒ Required: author, topic, outcome, what_worked';
98
- }
99
- const iid = instance_id;
100
- if (!iid) return 'โŒ instance_id required';
101
-
102
- // Store with author attribution via the same learn_from_attempts Redis structure
103
- const lesson = {
104
- topic, outcome, what_worked,
105
- what_failed: what_failed ?? '',
106
- severity: severity ?? 'minor',
107
- author,
108
- file_paths: file_paths ?? [],
109
- commands: commands ?? [],
110
- tags: [...(tags ?? []), 'team'],
111
- timestamp: new Date().toISOString(),
112
- recall_count: 0,
113
- version: 2,
114
- };
115
-
116
- const redis = await getConnection(iid);
117
- const key = `cachly:lessons:${topic}`;
118
- await redis.rpush(key, JSON.stringify(lesson));
119
- if (outcome === 'success') {
120
- await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lesson));
121
- }
122
-
123
- return `โœ… Team lesson stored by **${author}**: \`${topic}\` (${outcome})\n๐Ÿ’ก ${what_worked.slice(0, 120)}`;
124
- }
125
-
126
- // โ”€โ”€ team_confirm โ€” knowledge governance (Phase 3) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
127
- // A human reviewer endorses a stored lesson. The endorsement raises the
128
- // lesson's recall ranking (see src/rerank.ts), so trusted, reviewed knowledge
129
- // surfaces above unreviewed auto-learned entries.
130
- case 'team_confirm': {
131
- const { instance_id, topic, reviewer, level = 'peer', note = '' } = args as {
132
- instance_id: string; topic: string; reviewer: string;
133
- level?: 'senior' | 'peer'; note?: string;
134
- };
135
- if (!instance_id) return 'โŒ instance_id required';
136
- if (!topic || !reviewer) return 'โŒ Required: topic, reviewer';
137
- const reviewLevel = level === 'senior' ? 'senior' : 'peer';
138
-
139
- const redis = await getConnection(instance_id);
140
- const key = `cachly:lesson:best:${topic}`;
141
- const raw = await redis.get(key);
142
- const lesson = safeJsonParse<Record<string, unknown> | null>(raw, null);
143
- if (!lesson) {
144
- return `๐Ÿ“ญ No best-solution lesson found for \`${topic}\`.\n\nA lesson must exist (store one with \`team_learn\` / \`learn_from_attempts\`) before it can be confirmed.`;
145
- }
146
-
147
- // Track distinct reviewers so endorsements can't be inflated by one person.
148
- const endorsersKey = `cachly:lesson:endorsers:${topic}`;
149
- const isNewEndorser = (await redis.sadd(endorsersKey, reviewer)) === 1;
150
- await redis.expire(endorsersKey, 365 * 86400);
151
- const endorsements = await redis.scard(endorsersKey);
152
-
153
- // Senior beats peer; never downgrade an existing senior review to peer.
154
- const prevLevel = (lesson.review_level as string) ?? '';
155
- const effectiveLevel = prevLevel === 'senior' ? 'senior' : reviewLevel;
156
-
157
- const updated = {
158
- ...lesson,
159
- reviewed_by: reviewer,
160
- review_level: effectiveLevel,
161
- endorsements,
162
- reviewed_at: new Date().toISOString(),
163
- };
164
- await redis.set(key, JSON.stringify(updated));
165
-
166
- // Append an immutable review record for the audit trail.
167
- const reviewLog = {
168
- reviewer, level: reviewLevel, note: note.slice(0, 280), ts: new Date().toISOString(),
169
- };
170
- const logKey = `cachly:lesson:reviews:${topic}`;
171
- await redis.rpush(logKey, JSON.stringify(reviewLog));
172
- await redis.ltrim(logKey, -50, -1);
173
- await redis.expire(logKey, 365 * 86400);
174
-
175
- const badge = effectiveLevel === 'senior' ? '๐Ÿ›ก๏ธ senior-reviewed' : 'โœ”๏ธ peer-reviewed';
176
- const alreadyNote = isNewEndorser ? '' : ` (already endorsed by ${reviewer} โ€” no change to rank)`;
177
- return `${badge} โ€” \`${topic}\` confirmed by **${reviewer}** (${endorsements} distinct endorsement${endorsements === 1 ? '' : 's'})${alreadyNote}.\n๐Ÿ“ˆ This lesson now ranks higher in \`smart_recall\` / \`team_recall\`.${note ? `\n๐Ÿ“ ${note.slice(0, 200)}` : ''}`;
178
- }
179
-
180
- // โ”€โ”€ team_recall โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
181
- case 'team_recall': {
182
- const { instance_id, topic, author, limit = 10 } = args as {
183
- instance_id: string;
184
- topic?: string;
185
- author?: string;
186
- limit?: number;
187
- };
188
- const redis = await getConnection(instance_id);
189
-
190
- const lessonKeys: string[] = [];
191
- const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
192
- await new Promise<void>((resolve, reject) => {
193
- lStream.on('data', (batch: string[]) => lessonKeys.push(...batch));
194
- lStream.on('end', resolve);
195
- lStream.on('error', reject);
196
- });
197
-
198
- type TeamLesson = {
199
- topic: string; outcome: string; what_worked: string;
200
- ts: string; severity?: string; recall_count?: number;
201
- author?: string; tags?: string[];
202
- reviewed_by?: string; review_level?: string; endorsements?: number;
203
- };
204
- let lessons: TeamLesson[] = [];
205
- if (lessonKeys.length > 0) {
206
- const raws = await redis.mget(...lessonKeys);
207
- for (const raw of raws) {
208
- const l = safeJsonParse<TeamLesson | null>(raw ?? null, null);
209
- if (l) lessons.push(l);
210
- }
211
- }
212
-
213
- // Filter
214
- if (topic) {
215
- const t = topic.toLowerCase();
216
- lessons = lessons.filter(l =>
217
- l.topic.toLowerCase().includes(t) ||
218
- (l.tags ?? []).some((tag: string) => tag.toLowerCase().includes(t))
219
- );
220
- }
221
- if (author) {
222
- const a = author.toLowerCase();
223
- lessons = lessons.filter(l => l.author?.toLowerCase().includes(a));
224
- }
225
-
226
- // Sort: reviewed lessons first (senior > peer), then by recall_count desc.
227
- const reviewRank = (l: TeamLesson) => l.reviewed_by ? (l.review_level === 'senior' ? 2 : 1) : 0;
228
- lessons.sort((a, b) =>
229
- (reviewRank(b) - reviewRank(a)) || ((b.recall_count ?? 0) - (a.recall_count ?? 0)));
230
- lessons = lessons.slice(0, limit);
231
-
232
- if (lessons.length === 0) {
233
- return topic
234
- ? `๐Ÿ“ญ No team lessons found for \`${topic}\`.\n\nShared instance: add lessons with \`learn_from_attempts\` and include an \`author\` field.`
235
- : `๐Ÿ“ญ No lessons in this brain yet.\n\nAll team members sharing this instance will see lessons here.`;
236
- }
237
-
238
- const lines = [`๐Ÿ‘ฅ **Team Brain** โ€” ${lessons.length} lesson${lessons.length > 1 ? 's' : ''}`, ''];
239
- for (const l of lessons) {
240
- const emoji = l.outcome === 'success' ? 'โœ…' : l.outcome === 'partial' ? 'โš ๏ธ' : 'โŒ';
241
- const sev = l.severity === 'critical' ? '๐Ÿ”ด ' : l.severity === 'major' ? '๐ŸŸก ' : '';
242
- const reviewBadge = l.reviewed_by
243
- ? (l.review_level === 'senior' ? ' ๐Ÿ›ก๏ธ' : ' โœ”๏ธ')
244
- : '';
245
- const authorStr = l.author ? ` ยท _by ${l.author}_` : '';
246
- const reviewStr = l.reviewed_by
247
- ? ` ยท reviewed by ${l.reviewed_by}${(l.endorsements ?? 0) > 1 ? ` +${(l.endorsements ?? 0) - 1}` : ''}`
248
- : '';
249
- const recallStr = (l.recall_count ?? 0) > 0 ? ` ยท recalled ${l.recall_count}ร—` : '';
250
- const ago = Math.round((Date.now() - new Date(l.ts).getTime()) / 86400000);
251
- const agoStr = ago === 0 ? 'today' : ago === 1 ? 'yesterday' : `${ago}d ago`;
252
- lines.push(`${emoji} ${sev}**\`${l.topic}\`**${reviewBadge}${authorStr}${reviewStr}${recallStr} ยท ${agoStr}`);
253
- lines.push(` ${l.what_worked.slice(0, 120)}`);
254
- lines.push('');
255
- }
256
- return lines.join('\n');
257
- }
258
-
259
- // โ”€โ”€ team_synthesize โ€” Team Brain Synthesis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
260
- case 'team_synthesize': {
261
- const { instance_id, topic } = args as { instance_id: string; topic: string };
262
- const redis = await getConnection(instance_id);
263
-
264
- // Load history list for this topic (all authors' contributions)
265
- const listKey = `cachly:lessons:${topic}`;
266
- const all = await redis.lrange(listKey, 0, -1);
267
- if (all.length < 2) {
268
- return `๐Ÿ“ญ Need at least 2 entries for topic \`${topic}\` to synthesize.\n\nCurrently: ${all.length} entr${all.length === 1 ? 'y' : 'ies'}.\n\nHave team members store lessons via \`learn_from_attempts(topic="${topic}", ...)\`.`;
269
- }
270
-
271
- type Entry = { outcome: string; what_worked: string; what_failed?: string; author?: string; ts: string; severity?: string };
272
- const entries: Entry[] = all.map(r => { try { return JSON.parse(r) as Entry; } catch { return null; } }).filter((e): e is Entry => e !== null);
273
-
274
- // Group by outcome
275
- const successes = entries.filter(e => e.outcome === 'success');
276
- const failures = entries.filter(e => e.outcome === 'failure');
277
- const partials = entries.filter(e => e.outcome === 'partial');
278
-
279
- const authors = [...new Set(entries.map(e => e.author).filter(Boolean))];
280
- const hasMultiAuthor = authors.length > 1;
281
-
282
- // Build canonical merged version
283
- // what_worked: pick the most recent success, or longest for most detail
284
- const bestSuccess = successes.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())[0];
285
- const whatWorkedCandidates = successes.map(e => e.what_worked).filter(w => w && w.length > 10);
286
- const canonicalWorked = whatWorkedCandidates.sort((a, b) => b.length - a.length)[0] ?? bestSuccess?.what_worked ?? '';
287
-
288
- // what_failed: union of all unique failure reasons
289
- const allFailed = [...new Set(
290
- [...failures, ...partials].map(e => e.what_failed).filter((w): w is string => !!w && w.length > 5)
291
- )];
292
-
293
- const severities = entries.map(e => e.severity).filter(Boolean);
294
- const canonicalSeverity = severities.includes('critical') ? 'critical' : severities.includes('major') ? 'major' : 'minor';
295
-
296
- const lines = [
297
- `๐Ÿงฌ **Team Brain Synthesis: \`${topic}\`**`,
298
- `_${entries.length} entries from ${authors.length} author${authors.length === 1 ? '' : 's'}${hasMultiAuthor ? ` (${authors.join(', ')})` : ''} ยท ${successes.length} success ยท ${failures.length} failure ยท ${partials.length} partial_`,
299
- '',
300
- `**Canonical "what worked":**`,
301
- `> ${canonicalWorked}`,
302
- '',
303
- allFailed.length > 0 ? `**Avoid (combined failures):**` : '',
304
- ...allFailed.map(f => `> โŒ ${f}`),
305
- allFailed.length > 0 ? '' : '',
306
- `**Suggested canonical lesson:**`,
307
- '```',
308
- `learn_from_attempts(`,
309
- ` topic = "${topic}",`,
310
- ` outcome = "success",`,
311
- ` what_worked = "${canonicalWorked.replace(/"/g, "'")}",`,
312
- allFailed.length > 0 ? ` what_failed = "${allFailed[0].replace(/"/g, "'")}",` : '',
313
- ` severity = "${canonicalSeverity}",`,
314
- `)`,
315
- '```',
316
- '',
317
- hasMultiAuthor
318
- ? `๐Ÿ’ก _${authors.length} team members contributed to this synthesis. Store the canonical version to replace individual entries._`
319
- : `๐Ÿ’ก _Single author โ€” more value when multiple team members contribute to the same topic._`,
320
- ].filter(l => l !== undefined).join('\n');
321
- return lines;
322
- }
323
-
324
- // โ”€โ”€ brain_doctor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
325
- // โ”€โ”€ memory_crystalize โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
326
- case 'memory_crystalize': {
327
- const { instance_id, label: crystalLabel = '' } = args as { instance_id: string; label?: string };
328
- const redis = await getConnection(instance_id);
329
- const now = new Date();
330
- const week = `${now.getFullYear()}-W${String(Math.ceil((now.getDate() - now.getDay() + 10) / 7)).padStart(2, '0')}`;
331
- const effectiveLabel = crystalLabel || `${now.toISOString().slice(0, 7)} Crystal`;
332
-
333
- // Read session history
334
- const sessionHistory = await redis.lrange('cachly:session:history', 0, 49);
335
-
336
- // Read all auto-learned lessons
337
- const allLessonKeys: string[] = [];
338
- const ls = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
339
- await new Promise<void>((res, rej) => {
340
- ls.on('data', (b: string[]) => allLessonKeys.push(...b));
341
- ls.on('end', res);
342
- ls.on('error', rej);
343
- });
344
-
345
- type RawLesson = { topic: string; outcome: string; what_worked: string; severity?: string; ts: string; auto_learned?: boolean };
346
- const allLessons: RawLesson[] = [];
347
- if (allLessonKeys.length > 0) {
348
- const raws = await redis.mget(...allLessonKeys);
349
- for (const raw of raws) {
350
- const l = safeJsonParse<RawLesson | null>(raw ?? null, null);
351
- if (l) allLessons.push(l);
352
- }
353
- }
354
-
355
- // Group lessons by top-level category
356
- const categoryMap = new Map<string, RawLesson[]>();
357
- for (const l of allLessons) {
358
- const cat = l.topic.split(':')[0] || 'misc';
359
- if (!categoryMap.has(cat)) categoryMap.set(cat, []);
360
- categoryMap.get(cat)!.push(l);
361
- }
362
-
363
- // Build top patterns (most frequent categories with a representative insight)
364
- const topPatterns: Array<{ category: string; insight: string; count: number }> = [];
365
- for (const [cat, catLessons] of [...categoryMap.entries()].sort((a, b) => b[1].length - a[1].length).slice(0, 8)) {
366
- const successLessons = catLessons.filter(l => l.outcome === 'success');
367
- const best = successLessons[0] ?? catLessons[0];
368
- if (!best) continue;
369
- topPatterns.push({
370
- category: cat,
371
- insight: best.what_worked.slice(0, 120),
372
- count: catLessons.length,
373
- });
374
- }
375
-
376
- const crystal = {
377
- label: effectiveLabel,
378
- ts: now.toISOString(),
379
- session_count: sessionHistory.length,
380
- lesson_count: allLessons.length,
381
- top_patterns: topPatterns,
382
- categories: [...categoryMap.keys()],
383
- created_from: `${sessionHistory.length} sessions, ${allLessons.length} lessons`,
384
- };
385
-
386
- const crystalJson = JSON.stringify(crystal);
387
- await redis.set('cachly:crystal:latest', crystalJson);
388
- await redis.expire('cachly:crystal:latest', 90 * 86400);
389
- await redis.set(`cachly:crystal:${week}`, crystalJson);
390
- await redis.expire(`cachly:crystal:${week}`, 365 * 86400);
391
-
392
- const lines = [
393
- `๐Ÿ’Ž **Memory Crystal created: ${effectiveLabel}**`,
394
- ``,
395
- `๐Ÿ“Š Compressed: **${sessionHistory.length} sessions** + **${allLessons.length} lessons** โ†’ ${topPatterns.length} top patterns`,
396
- ``,
397
- `**Top patterns by category:**`,
398
- ...topPatterns.slice(0, 6).map(p => ` โ€ข **${p.category}** (${p.count}ร—): ${p.insight.slice(0, 90)}`),
399
- ``,
400
- `๐Ÿ’ก This crystal will appear in every future \`session_start\` briefing.`,
401
- `๐Ÿ’ก Re-run \`memory_crystalize\` monthly to keep it fresh.`,
402
- ];
403
- return lines.join('\n');
404
- }
405
-
406
- case 'brain_doctor': {
407
- const { instance_id, workspace_path: drWorkspacePath = '' } = args as { instance_id: string; workspace_path?: string };
408
- const redis = await getConnection(instance_id);
409
- const issues: string[] = [];
410
- const checks: string[] = [];
411
-
412
- // Count lessons
413
- const lessonKeys: string[] = [];
414
- const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
415
- await new Promise<void>((resolve, reject) => {
416
- lStream.on('data', (batch: string[]) => lessonKeys.push(...batch));
417
- lStream.on('end', resolve);
418
- lStream.on('error', reject);
419
- });
420
-
421
- // Count context
422
- let ctxCount = 0;
423
- const ctxStream = redis.scanStream({ match: 'cachly:ctx:*', count: 200 });
424
- await new Promise<void>((resolve, reject) => {
425
- ctxStream.on('data', (batch: string[]) => {
426
- ctxCount += batch.filter((k: string) => !k.endsWith(':meta')).length;
427
- });
428
- ctxStream.on('end', resolve);
429
- ctxStream.on('error', reject);
430
- });
431
-
432
- // Load lessons for analysis
433
- type DrLesson = {
434
- topic: string; outcome: string; recall_count?: number; ts: string;
435
- verified_at?: string; severity?: string; audit_trail?: unknown[];
436
- };
437
- const lessons: DrLesson[] = [];
438
- if (lessonKeys.length > 0) {
439
- const raws = await redis.mget(...lessonKeys);
440
- for (const raw of raws) {
441
- const l = safeJsonParse<DrLesson | null>(raw ?? null, null);
442
- if (l) lessons.push(l);
443
- }
444
- }
445
-
446
- // Last session
447
- const lastSessionRaw = await redis.get('cachly:session:last');
448
- const lastSession = safeJsonParse<{ ts: string; summary: string } | null>(lastSessionRaw, null);
449
-
450
- // Open failures
451
- const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
452
- // Unused lessons (never recalled)
453
- const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
454
- // Critical lessons
455
- const criticalLessons = lessons.filter(l => l.severity === 'critical');
456
- // Confidence decay analysis
457
- const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
458
- const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
459
- const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
460
- // Team lessons
461
- type DrLessonWithAuthor = DrLesson & { author?: string };
462
- const teamLessons = (lessons as DrLessonWithAuthor[]).filter(l => l.author);
463
- const uniqueAuthors = new Set((lessons as DrLessonWithAuthor[]).map(l => l.author).filter(Boolean));
464
- // Effective IQ boost: total recalls / lessons (how much the brain actually helped)
465
- const totalRecalls = lessons.reduce((sum, l) => sum + (l.recall_count ?? 0), 0);
466
- const iqBoostPct = lessons.length > 0 ? Math.min(100, Math.round((totalRecalls / lessons.length) * 10)) : 0;
467
-
468
- // Quality score (0-100)
469
- let score = 50;
470
- if (lessonKeys.length >= 5) score += 10;
471
- if (lessonKeys.length >= 20) score += 10;
472
- if (ctxCount >= 3) score += 10;
473
- if (ctxCount >= 10) score += 5;
474
- if (lastSession) score += 10;
475
- if (openFailures.length === 0) score += 5;
476
- const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
477
- if (unusedRatio < 0.5) score += 10;
478
- if (staleLessons.length === 0) score += 5;
479
- if (uniqueAuthors.size >= 2) score += 5; // team collaboration bonus
480
-
481
- const scoreEmoji = score >= 80 ? '๐ŸŸข' : score >= 50 ? '๐ŸŸก' : '๐Ÿ”ด';
482
- const iqEmoji = iqBoostPct >= 50 ? '๐Ÿš€' : iqBoostPct >= 20 ? '๐Ÿ“ˆ' : '๐Ÿ’ค';
483
-
484
- checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
485
- checks.push(`${iqEmoji} **Effective IQ Boost: ${iqBoostPct}%** (${totalRecalls} recalls across ${lessons.length} lessons)`);
486
- checks.push(`๐Ÿ“š **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical ยท ${withAudit.length} with audit trail ยท ${teamLessons.length} from team)`);
487
- checks.push(`๐Ÿ’พ **Context entries:** ${ctxCount}`);
488
- checks.push(`๐ŸŽฏ **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh ยท ${warnLessons.length} warn ยท ${staleLessons.length} stale`);
489
- checks.push(`โฑ๏ธ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d ยท stale after ${CONFIDENCE_STALE_DAYS}d`);
490
- if (uniqueAuthors.size >= 2) {
491
- checks.push(`๐Ÿ‘ฅ **Team:** ${uniqueAuthors.size} contributors (${[...uniqueAuthors].join(', ')})`);
492
- }
493
-
494
- // Stale index detection
495
- try {
496
- const lastIndexRaw = await redis.get('cachly:index:last_run');
497
- if (lastIndexRaw) {
498
- const lastIndexAge = Math.round((Date.now() - new Date(lastIndexRaw).getTime()) / 86_400_000);
499
- if (lastIndexAge > 7) {
500
- issues.push(`๐Ÿ”„ Index is ${lastIndexAge}d stale โ€” run \`index_project\` to re-sync semantic search`);
501
- } else {
502
- checks.push(`๐Ÿ—‚๏ธ **Semantic index:** ${lastIndexAge}d old (fresh)`);
503
- }
504
- } else {
505
- issues.push(`๐Ÿ’ก No semantic index โ€” run \`index_project(dir="<your-src>")\` to enable semantic search`);
506
- }
507
- } catch { /* non-critical */ }
508
-
509
- // Memory crystal status
510
- try {
511
- const crystalRaw = await redis.get('cachly:crystal:latest');
512
- if (crystalRaw) {
513
- const crystal = JSON.parse(crystalRaw) as { ts: string; label: string };
514
- const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
515
- checks.push(`๐Ÿ’Ž **Memory Crystal:** ${crystal.label} (${crystalAge}d ago)`);
516
- if (crystalAge > 30) issues.push(`๐Ÿ’ก Memory Crystal is ${crystalAge}d old โ€” re-run \`memory_crystalize\` to compress new sessions`);
517
- } else if (lessonKeys.length >= 10) {
518
- issues.push(`๐Ÿ’ก ${lessonKeys.length} lessons but no Memory Crystal โ€” run \`memory_crystalize\` to compress wisdom`);
519
- }
520
- } catch { /* non-critical */ }
521
-
522
- // openclaw cross-promo (check package.json in workspace)
523
- if (drWorkspacePath) {
524
- try {
525
- const pkgPath = drWorkspacePath.replace(/\/$/, '') + '/package.json';
526
- const pkgRaw = readFileSync(pkgPath, 'utf-8');
527
- const pkg = JSON.parse(pkgRaw) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
528
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
529
- const hasLLMDep = ['openai', '@anthropic-ai/sdk', '@google/generative-ai', 'mistralai', 'cohere-ai'].some(d => d in allDeps);
530
- const hasOpenclaw = '@cachly-dev/openclaw' in allDeps;
531
- if (hasLLMDep && !hasOpenclaw) {
532
- issues.push(`๐Ÿ’ก **openclaw missing:** you use LLM APIs (${Object.keys(allDeps).filter(d => ['openai','@anthropic-ai/sdk'].includes(d)).join(', ')}) but not \`@cachly-dev/openclaw\``);
533
- issues.push(` โ†’ \`npm install @cachly-dev/openclaw\` cuts LLM costs 60โ€“90% with 3 lines of code`);
534
- } else if (hasOpenclaw) {
535
- checks.push(`โœ… **@cachly-dev/openclaw installed** (LLM cost caching active)`);
536
- }
537
- } catch { /* no package.json or unreadable */ }
538
- }
539
-
540
- if (lastSession) {
541
- const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
542
- const ageStr = ageMin < 60 ? `${ageMin}m` : ageMin < 1440 ? `${Math.round(ageMin / 60)}h` : `${Math.round(ageMin / 1440)}d`;
543
- checks.push(`๐Ÿ• **Last session:** ${ageStr} ago`);
544
- } else {
545
- issues.push('โŒ No session history โ€” call `session_start` + `session_end` to start tracking');
546
- }
547
-
548
- if (lessonKeys.length === 0) {
549
- issues.push('โŒ No lessons โ€” call `learn_from_attempts` after solving bugs');
550
- } else if (lessonKeys.length < 5) {
551
- issues.push(`๐Ÿ’ก Only ${lessonKeys.length} lessons โ€” add more after each problem solved`);
552
- }
553
-
554
- if (iqBoostPct === 0 && lessons.length >= 5) {
555
- issues.push(`๐Ÿ’ค **IQ Boost is 0%** โ€” lessons exist but are never recalled. Use \`recall_best_solution\` BEFORE tasks.`);
556
- }
557
-
558
- if (ctxCount === 0) {
559
- issues.push('๐Ÿ’ก No context โ€” use `remember_context` to cache architecture docs, ADRs, etc.');
560
- }
561
-
562
- if (openFailures.length > 0) {
563
- issues.push(`โš ๏ธ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
564
- }
565
-
566
- if (staleLessons.length > 0) {
567
- issues.push(`๐Ÿ”ด ${staleLessons.length} STALE lesson${staleLessons.length > 1 ? 's' : ''} (>${CONFIDENCE_STALE_DAYS}d, confidence <${CONFIDENCE_STALE_VALUE * 100}%): ${staleLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
568
- issues.push(` โ†’ Re-verify with \`recall_best_solution\` to reset confidence clock`);
569
- }
570
-
571
- if (warnLessons.length > 0) {
572
- issues.push(`โš ๏ธ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
573
- }
574
-
575
- if (unusedRatio > 0.7 && lessons.length > 5) {
576
- issues.push(`๐Ÿ’ก ${unusedLessons.length} lessons never recalled โ€” verify topics match your workflow`);
577
- }
578
-
579
- const divider = 'โ”€'.repeat(48);
580
- const lines = [
581
- `๐Ÿฉบ **Brain Doctor**`,
582
- `\`${divider}\``,
583
- '',
584
- '**๐Ÿ“Š Health Checks**',
585
- ...checks.map(c => ' ' + c),
586
- '',
587
- ];
588
- if (issues.length > 0) {
589
- lines.push(`**๐Ÿ”ง Action Items** (${issues.length} issue${issues.length > 1 ? 's' : ''})`);
590
- issues.forEach((item, idx) => lines.push(` ${idx + 1}. ${item}`));
591
- lines.push('');
592
- lines.push(`_Fix the action items above, then re-run \`brain_doctor\` to verify._`);
593
- } else {
594
- lines.push('**โœ… All checks passed** โ€” Brain is healthy!');
595
- lines.push('');
596
- lines.push('_Keep the brain sharp: `session_start` โ†’ work โ†’ `learn_from_attempts` โ†’ `session_end`_');
597
- }
598
- return lines.join('\n');
599
- }
600
-
601
- // โ”€โ”€ recall_at โ€” Brain Archaeology โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
602
- case 'recall_at': {
603
- const { instance_id, topic, date } = args as { instance_id: string; topic: string; date: string };
604
- const redis = await getConnection(instance_id);
605
- const cutoff = new Date(date).getTime();
606
- if (isNaN(cutoff)) return `โŒ Invalid date "${date}". Use ISO format: "2026-01-15"`;
607
-
608
- const listKey = `cachly:lessons:${topic}`;
609
- const all = await redis.lrange(listKey, 0, -1);
610
- if (all.length === 0) return `๐Ÿ“ญ No history found for \`${topic}\`. Lessons are stored via \`learn_from_attempts\`.`;
611
-
612
- const before = all
613
- .map(raw => { try { return JSON.parse(raw) as { ts: string; outcome?: string; what_worked?: string; what_failed?: string }; } catch { return null; } })
614
- .filter((l): l is NonNullable<typeof l> => l !== null && new Date(l.ts).getTime() <= cutoff)
615
- .sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
616
-
617
- if (before.length === 0) {
618
- const first = safeJsonParse(all[0] ?? null, null as null | { ts?: string });
619
- const earliest = first?.ts ? new Date(first.ts).toLocaleDateString('de-DE') : 'unknown';
620
- return `๐Ÿ“ญ No entries for \`${topic}\` found before **${date}**. Earliest entry: ${earliest}.`;
621
- }
622
-
623
- const lines = [
624
- `๐Ÿบ **Brain Archaeology: \`${topic}\` before ${date}**`,
625
- `_${before.length} of ${all.length} total entries shown_`,
626
- '',
627
- ];
628
- for (const l of before.slice(-10)) {
629
- const emoji = l.outcome === 'success' ? 'โœ…' : l.outcome === 'partial' ? 'โš ๏ธ' : 'โŒ';
630
- const d = new Date(l.ts).toLocaleDateString('de-DE');
631
- lines.push(`**${d}** ${emoji} ${l.outcome}`);
632
- if (l.what_worked) lines.push(` โ†’ ${l.what_worked.slice(0, 100)}`);
633
- lines.push('');
634
- }
635
- lines.push(`_Full evolution: ${all.map(r => { try { const l = JSON.parse(r) as { outcome?: string }; return l.outcome === 'success' ? 'โœ…' : l.outcome === 'partial' ? 'โš ๏ธ' : 'โŒ'; } catch { return '?'; } }).join(' โ†’ ')}_`);
636
- return lines.join('\n');
637
- }
638
-
639
- // โ”€โ”€ trace_dependency โ€” Causal Chain โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
640
- case 'trace_dependency': {
641
- const { instance_id, dependency, mark_review = false } = args as { instance_id: string; dependency: string; mark_review?: boolean };
642
- const redis = await getConnection(instance_id);
643
-
644
- const depKey = `cachly:dep:${dependency}`;
645
- const raw = await redis.get(depKey);
646
- if (!raw) return `๐Ÿ“ญ No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
647
-
648
- const topics: string[] = safeJsonParse<string[]>(raw, []);
649
- const lines = [
650
- `๐Ÿ”— **Causal Chain: \`${dependency}\`** โ€” ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
651
- '',
652
- ];
653
-
654
- for (const t of topics) {
655
- const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
656
- if (!lessonRaw) { lines.push(` โ€ข \`${t}\` _(lesson deleted)_`); continue; }
657
- const lesson = safeJsonParse(lessonRaw, null as null | { outcome?: string; severity?: string; needs_review?: boolean });
658
- if (!lesson) { lines.push(` โ€ข \`${t}\` _(lesson corrupted)_`); continue; }
659
- const emoji = lesson.outcome === 'success' ? 'โœ…' : lesson.outcome === 'partial' ? 'โš ๏ธ' : 'โŒ';
660
- const reviewBadge = lesson.needs_review ? ' ๐Ÿ” **needs_review**' : '';
661
- lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
662
-
663
- if (mark_review) {
664
- const updated = { ...lesson, needs_review: true };
665
- await redis.set(`cachly:lesson:best:${t}`, JSON.stringify(updated));
666
- }
667
- }
668
-
669
- if (mark_review) {
670
- lines.push('', `๐Ÿ” All ${topics.length} lessons marked as **needs_review** โ€” verify they still work with the changed dependency.`);
671
- } else {
672
- lines.push('', `_Run with \`mark_review: true\` to flag all dependent lessons for re-verification._`);
673
- }
674
- return lines.join('\n');
675
- }
676
-
677
- // โ”€โ”€ global_learn โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
678
- case 'global_learn': {
679
- const { instance_id, topic, lesson, severity = 'minor', tags = [] } = args as {
680
- instance_id: string;
681
- topic: string;
682
- lesson: string;
683
- severity?: string;
684
- tags?: string[];
685
- };
686
- const redis = await getConnection(instance_id);
687
- const key = `cachly:global:lesson:${topic}`;
688
- const record = {
689
- topic,
690
- lesson,
691
- severity,
692
- tags,
693
- ts: new Date().toISOString(),
694
- scope: 'global',
695
- recall_count: 0,
696
- };
697
- // Preserve recall_count on update
698
- const existing = await redis.get(key);
699
- if (existing) {
700
- const prev = safeJsonParse(existing, null as null | { recall_count?: number });
701
- record.recall_count = prev?.recall_count ?? 0;
702
- }
703
- await redis.set(key, JSON.stringify(record));
704
- return `๐ŸŒ **Global lesson stored**: \`${topic}\`\n\n${lesson}\n\nRecallable from any project via \`global_recall(topic="${topic}")\`.`;
705
- }
706
-
707
- // โ”€โ”€ global_recall โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
708
- case 'global_recall': {
709
- const { instance_id, topic } = args as { instance_id: string; topic?: string };
710
- const redis = await getConnection(instance_id);
711
- const keys: string[] = [];
712
- const gStream = redis.scanStream({ match: 'cachly:global:lesson:*', count: 200 });
713
- await new Promise<void>((resolve, reject) => {
714
- gStream.on('data', (batch: string[]) => keys.push(...batch));
715
- gStream.on('end', resolve);
716
- gStream.on('error', reject);
717
- });
718
-
719
- type GlobalLesson = { topic: string; lesson: string; severity?: string; ts: string; recall_count?: number };
720
- let lessons: GlobalLesson[] = [];
721
- if (keys.length > 0) {
722
- const raws = await redis.mget(...keys);
723
- for (const raw of raws) {
724
- const l = safeJsonParse<GlobalLesson | null>(raw ?? null, null);
725
- if (l) lessons.push(l);
726
- }
727
- }
728
-
729
- if (topic) {
730
- const t = topic.toLowerCase();
731
- lessons = lessons.filter(l => l.topic.toLowerCase().includes(t));
732
- }
733
-
734
- if (lessons.length === 0) {
735
- return `๐Ÿ“ญ No global lessons${topic ? ` for \`${topic}\`` : ''}.\n\nAdd cross-project knowledge with \`global_learn(topic="...", lesson="...")\`.`;
736
- }
737
-
738
- // Increment recall_count
739
- for (const l of lessons) {
740
- const k = `cachly:global:lesson:${l.topic}`;
741
- const raw = await redis.get(k);
742
- if (raw) {
743
- const rec = safeJsonParse(raw, null as null | { recall_count?: number });
744
- if (!rec) continue;
745
- rec.recall_count = (rec.recall_count ?? 0) + 1;
746
- await redis.set(k, JSON.stringify(rec));
747
- }
748
- }
749
-
750
- const lines = [`๐ŸŒ **Global Brain** โ€” ${lessons.length} lesson${lessons.length > 1 ? 's' : ''}`, ''];
751
- for (const l of lessons) {
752
- const sev = l.severity === 'critical' ? '๐Ÿ”ด ' : l.severity === 'major' ? '๐ŸŸก ' : '';
753
- lines.push(`${sev}**\`${l.topic}\`**`);
754
- lines.push(l.lesson.slice(0, 200));
755
- lines.push('');
756
- }
757
- return lines.join('\n');
758
- }
759
-
760
- // โ”€โ”€ publish_lesson โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
761
- case 'publish_lesson': {
762
- const { instance_id, topic, lesson, framework = 'general', severity = 'minor' } = args as {
763
- instance_id: string;
764
- topic: string;
765
- lesson: string;
766
- framework?: string;
767
- severity?: string;
768
- };
769
- const redis = await getConnection(instance_id);
770
-
771
- // Strip potential PII patterns (emails, tokens, paths)
772
- const sanitized = lesson
773
- .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[email]')
774
- .replace(/\b(sk-|cky_live_|Bearer\s)[A-Za-z0-9_-]{8,}/g, '[token]')
775
- .replace(/\/Users\/[^\s/]+/g, '/Users/[user]')
776
- .replace(/\/home\/[^\s/]+/g, '/home/[user]');
777
-
778
- const publicLesson = {
779
- topic,
780
- lesson: sanitized,
781
- framework,
782
- severity,
783
- ts: new Date().toISOString(),
784
- published: true,
785
- votes: 0,
786
- };
787
-
788
- // Store locally with public flag (future: sync to Cachly public API)
789
- const key = `cachly:public:lesson:${framework}:${topic}`;
790
- await redis.set(key, JSON.stringify(publicLesson), 'EX', 365 * 86400);
791
-
792
- return [
793
- `๐Ÿ“ข **Lesson published!**`,
794
- ``,
795
- `**Topic:** \`${topic}\``,
796
- `**Framework:** ${framework}`,
797
- `**Content:** ${sanitized.slice(0, 200)}${sanitized.length > 200 ? 'โ€ฆ' : ''}`,
798
- ``,
799
- `This lesson is now available in the Public Brain for other developers.`,
800
- `Import it anywhere: \`import_public_brain(framework="${framework}")\``,
801
- ].join('\n');
802
- }
803
-
804
- // โ”€โ”€ import_public_brain โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
805
- case 'import_public_brain': {
806
- const { instance_id, framework, limit = 20 } = args as {
807
- instance_id: string;
808
- framework: string;
809
- limit?: number;
810
- };
811
- const redis = await getConnection(instance_id);
812
-
813
- // Community-curated lessons per framework
814
- const COMMUNITY_LESSONS: Record<string, Array<{ topic: string; lesson: string; severity: string }>> = {
815
- nextjs: [
816
- { topic: 'nextjs:image-layout', lesson: 'Use fill + relative parent instead of layout="fill" (deprecated since Next.js 13)', severity: 'major' },
817
- { topic: 'nextjs:app-router-fetch', lesson: 'fetch() in Server Components is cached by default โ€” add {cache:"no-store"} for dynamic data', severity: 'major' },
818
- { topic: 'nextjs:metadata-export', lesson: 'Export metadata const or generateMetadata() โ€” never both in same file', severity: 'minor' },
819
- { topic: 'nextjs:client-boundary', lesson: '"use client" propagates down โ€” keep it at the lowest component, not at page level', severity: 'major' },
820
- { topic: 'nextjs:env-prefix', lesson: 'Only NEXT_PUBLIC_* env vars are exposed to client โ€” others are server-only', severity: 'critical' },
821
- { topic: 'nextjs:revalidate', lesson: 'export const revalidate = 0 disables caching for entire route; use revalidatePath() for on-demand', severity: 'minor' },
822
- ],
823
- fastapi: [
824
- { topic: 'fastapi:async-db', lesson: 'Use async session with asyncpg โ€” sync SQLAlchemy blocks the event loop', severity: 'critical' },
825
- { topic: 'fastapi:pydantic-v2', lesson: 'Pydantic v2: use model_validate() instead of parse_obj(), .dict() โ†’ .model_dump()', severity: 'major' },
826
- { topic: 'fastapi:lifespan', lesson: 'Use lifespan context manager instead of deprecated on_event startup/shutdown', severity: 'minor' },
827
- { topic: 'fastapi:background-tasks', lesson: 'BackgroundTasks run after response is sent โ€” not in a separate thread pool', severity: 'major' },
828
- { topic: 'fastapi:cors-order', lesson: 'CORSMiddleware must be added before other middleware to work correctly', severity: 'critical' },
829
- ],
830
- go: [
831
- { topic: 'go:context-cancel', lesson: 'Always call cancel() from context.WithCancel โ€” leak goroutines if not cancelled', severity: 'critical' },
832
- { topic: 'go:defer-in-loop', lesson: 'defer in a loop runs at function return, not loop iteration โ€” use IIFE or explicit close', severity: 'major' },
833
- { topic: 'go:nil-interface', lesson: 'nil interface != interface containing nil pointer โ€” use explicit nil checks', severity: 'major' },
834
- { topic: 'go:goroutine-leak', lesson: 'Goroutines with channel sends block forever if receiver is gone โ€” use select with done chan', severity: 'critical' },
835
- { topic: 'go:embed-path', lesson: '//go:embed path must be relative and known at compile time โ€” no os.Getenv', severity: 'minor' },
836
- ],
837
- docker: [
838
- { topic: 'docker:layer-cache', lesson: 'Copy package.json before source code โ€” Docker caches layers, npm install only reruns on dep changes', severity: 'major' },
839
- { topic: 'docker:non-root', lesson: 'Run as non-root user (USER 1001) โ€” some k8s clusters reject root containers by policy', severity: 'critical' },
840
- { topic: 'docker:build-arg-secret', lesson: 'Never use ARG for secrets โ€” visible in image history. Use --secret mount instead', severity: 'critical' },
841
- { topic: 'docker:entrypoint-exec', lesson: 'Use exec form ["cmd","arg"] not shell form "cmd arg" โ€” shell form ignores SIGTERM', severity: 'major' },
842
- { topic: 'docker:multi-stage', lesson: 'Multi-stage builds: copy only built artifacts to final stage โ€” keep image small', severity: 'minor' },
843
- ],
844
- kubernetes: [
845
- { topic: 'k8s:resource-limits', lesson: 'Always set resource limits โ€” unbounded pods cause node evictions and OOMKill', severity: 'critical' },
846
- { topic: 'k8s:liveness-vs-readiness', lesson: 'Liveness failures restart pod; Readiness failures remove from LB. Use different endpoints', severity: 'major' },
847
- { topic: 'k8s:imagepullpolicy', lesson: 'imagePullPolicy: Always in production โ€” IfNotPresent can serve stale images', severity: 'major' },
848
- { topic: 'k8s:configmap-env', lesson: 'ConfigMap changes don\'t restart pods โ€” use rollout restart or mount as volume', severity: 'critical' },
849
- { topic: 'k8s:pdb', lesson: 'Set PodDisruptionBudget for stateful apps โ€” node drains kill all pods without it', severity: 'major' },
850
- ],
851
- react: [
852
- { topic: 'react:useeffect-deps', lesson: 'Omitting dependencies from useEffect deps array causes stale closure bugs โ€” use exhaustive-deps ESLint rule', severity: 'critical' },
853
- { topic: 'react:key-index', lesson: 'Never use array index as key in lists โ€” causes subtle re-render bugs on reorder/delete', severity: 'major' },
854
- { topic: 'react:setState-in-render', lesson: 'setState() during render causes infinite loop โ€” move to useEffect or event handler', severity: 'critical' },
855
- { topic: 'react:memo-reference', lesson: 'Object/array literals in JSX recreate on every render โ€” useMemo for expensive derived values', severity: 'minor' },
856
- ],
857
- typescript: [
858
- { topic: 'ts:type-guard', lesson: 'Use "x is Type" return type for type guard functions โ€” not "boolean"', severity: 'minor' },
859
- { topic: 'ts:strict-null', lesson: 'Enable strictNullChecks in tsconfig โ€” catches 90% of runtime null errors at compile time', severity: 'critical' },
860
- { topic: 'ts:enum-avoid', lesson: 'Prefer union types ("a"|"b") over enum โ€” enums have surprising runtime behavior', severity: 'minor' },
861
- { topic: 'ts:satisfies', lesson: 'Use "satisfies" operator to validate type without widening โ€” more precise than explicit annotation', severity: 'minor' },
862
- ],
863
- python: [
864
- { topic: 'python:mutable-default', lesson: 'Never use mutable default arguments (def f(x=[])) โ€” shared across all calls. Use None + guard', severity: 'critical' },
865
- { topic: 'python:walrus-operator', lesson: ':= (walrus) assigns and returns โ€” useful in while/comprehensions but hard to read in complex expr', severity: 'minor' },
866
- { topic: 'python:asyncio-run', lesson: 'asyncio.run() creates new event loop โ€” calling it inside an existing loop raises RuntimeError', severity: 'major' },
867
- { topic: 'python:typing-optional', lesson: 'Optional[X] == Union[X, None] โ€” in Python 3.10+ use X | None syntax', severity: 'minor' },
868
- ],
869
- };
870
-
871
- const fw = framework.toLowerCase();
872
- const lessons = COMMUNITY_LESSONS[fw];
873
-
874
- if (!lessons) {
875
- const available = Object.keys(COMMUNITY_LESSONS).join(', ');
876
- return `โŒ No public brain available for \`${framework}\`.\n\nAvailable: ${available}\n\nOr publish your own: \`publish_lesson(framework="${framework}", ...)\``;
877
- }
878
-
879
- const toImport = lessons.slice(0, limit);
880
- let importedCount = 0;
881
-
882
- for (const l of toImport) {
883
- const key = `cachly:lesson:best:${l.topic}`;
884
- const existing = await redis.get(key);
885
- if (!existing) {
886
- await redis.set(key, JSON.stringify({
887
- ...l,
888
- what_worked: l.lesson,
889
- outcome: 'success',
890
- ts: new Date().toISOString(),
891
- recall_count: 0,
892
- source: 'public_brain',
893
- version: 2,
894
- }));
895
- importedCount++;
896
- }
897
- }
898
-
899
- const lines = [
900
- `๐Ÿ“ฅ **Public Brain imported: ${framework}**`,
901
- ``,
902
- `${importedCount} new lessons added (${toImport.length - importedCount} already existed)`,
903
- ``,
904
- `**Imported topics:**`,
905
- ...toImport.map(l => {
906
- const sev = l.severity === 'critical' ? '๐Ÿ”ด' : l.severity === 'major' ? '๐ŸŸก' : '๐Ÿ’ก';
907
- return ` ${sev} \`${l.topic}\``;
908
- }),
909
- ``,
910
- `These lessons will now appear in \`session_start\` when relevant.`,
911
- `Recall any time: \`recall_best_solution(topic="${fw}:...")\``,
912
- ];
913
- return lines.join('\n');
914
- }
915
-
916
- // โ”€โ”€ setup_ai_memory โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
917
- case 'setup_ai_memory': {
918
- const {
919
- instance_id,
920
- project_dir,
921
- embed_provider: providerArg = 'openai',
922
- project_description = 'a software project',
923
- } = args as {
924
- instance_id: string;
925
- project_dir?: string;
926
- embed_provider?: string;
927
- project_description?: string;
928
- };
929
-
930
- const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
931
-
932
- // Provider-specific env var instructions
933
- const providerEnvMap: Record<string, { key: string; hint: string }> = {
934
- openai: { key: 'OPENAI_API_KEY', hint: 'Get at: https://platform.openai.com/api-keys' },
935
- mistral: { key: 'MISTRAL_API_KEY', hint: 'Get at: https://console.mistral.ai/api-keys' },
936
- cohere: { key: 'COHERE_API_KEY', hint: 'Get at: https://dashboard.cohere.com/api-keys' },
937
- ollama: { key: 'OLLAMA_BASE_URL', hint: 'Run: brew install ollama && ollama serve (free, local, no key needed)' },
938
- gemini: { key: 'GEMINI_API_KEY', hint: 'Get at: https://aistudio.google.com/app/apikey' },
939
- };
940
- const provInfo = providerEnvMap[providerArg] ?? providerEnvMap['openai'];
941
- const hasVector = !!inst.vector_token;
942
-
943
- // Generate .mcp.json snippet
944
- const mcpJsonSnippet = JSON.stringify({
945
- mcpServers: {
946
- cachly: {
947
- command: 'npx',
948
- args: ['-y', '@cachly-dev/mcp-server@latest'],
949
- env: {
950
- CACHLY_JWT: 'your-api-token-from-cachly.dev/settings',
951
- [provInfo.key]: providerArg === 'ollama' ? 'http://localhost:11434' : 'your-key-here',
952
- ...(providerArg !== 'openai' ? { CACHLY_EMBED_PROVIDER: providerArg } : {}),
953
- },
954
- },
955
- },
956
- }, null, 2);
957
-
958
- // Generate copilot-instructions.md content
959
- const tier = inst.tier.toUpperCase();
960
- const smartRecallNote = hasVector
961
- ? '- `smart_recall("natural language query")` โ€” semantic search (finds by meaning)'
962
- : '- `recall_context("arch:*")` โ€” exact/glob key lookup (upgrade to Speed/Business for smart_recall)';
963
- const layerNote = hasVector
964
- ? `Layer 3 (Semantic): smart_recall uses pgvector HNSW on your ${tier} instance`
965
- : `Layer 3 (Autopilot): this file โ€” upgrade to Speed/Business tier to unlock smart_recall`;
966
-
967
- const copilotInstructions = `# cachly AI Brain โ€” ${project_description}
968
-
969
- > AI memory system powered by [cachly.dev](https://cachly.dev). Works with GitHub Copilot, Claude Code, Cursor, Windsurf, Continue.dev, and any MCP-compatible editor.
970
-
971
- ---
972
-
973
- ## ๐Ÿ”ด MANDATORY RULES โ€” Always enforced, no exceptions
974
-
975
- ### Rule 1 โ€” Session Start (FIRST thing in every window)
976
- Call \`session_start\` **before reading any source files or making any changes**:
977
-
978
- \`\`\`
979
- session_start(
980
- instance_id = "${instance_id}",
981
- focus = "<what you plan to work on today>",
982
- )
983
- \`\`\`
984
-
985
- Returns: last session summary, handoff from previous window, recent lessons, open failures, brain health.
986
-
987
- **If \`session_start\` is unavailable**, fall back:
988
- 1. \`smart_recall("recent lessons <topic>")\`
989
- 2. \`recall_best_solution("<topic>")\`
990
-
991
- ### Rule 2 โ€” Learn After Every Fix/Deploy/Discovery (AUTOMATIC โ€” no prompting needed)
992
- After **every** bug fix, deployment, failed attempt, or discovery โ€” call \`learn_from_attempts\` immediately:
993
-
994
- \`\`\`
995
- learn_from_attempts(
996
- instance_id = "${instance_id}",
997
- topic = "category:keyword", # deploy:web | fix:auth | debug:db | infra:docker
998
- outcome = "success" | "failure" | "partial",
999
- what_worked = "exact approach or command that worked",
1000
- what_failed = "what did NOT work (prevents future wasted time)",
1001
- severity = "critical" | "major" | "minor",
1002
- commands = ["exact command that worked"],
1003
- file_paths = ["path/to/changed/file"],
1004
- )
1005
- \`\`\`
1006
-
1007
- **These events MUST automatically trigger \`learn_from_attempts\`:**
1008
- - โœ… Successfully deployed โ†’ \`topic="deploy:<component>", outcome="success"\`
1009
- - โœ… Fixed a bug โ†’ \`topic="fix:<description>", outcome="success"\`
1010
- - โœ… Discovered root cause โ†’ \`topic="debug:<area>", outcome="partial"\`
1011
- - โœ… Tried something that failed โ†’ \`topic="<area>", outcome="failure", what_failed="..."\`
1012
- - โœ… Found a config quirk โ†’ \`topic="infra:<topic>", outcome="success"\`
1013
- - โœ… Ran a working command โ†’ store it in \`commands=[...]\`
1014
-
1015
- ### Rule 3 โ€” Session Handoff (when closing window or task list > 3 items)
1016
- \`\`\`
1017
- session_handoff(
1018
- instance_id = "${instance_id}",
1019
- completed_tasks = ["what was done"],
1020
- remaining_tasks = ["what is still to do"],
1021
- files_changed = [{"path": "src/...", "status": "modified", "description": "what changed"}],
1022
- instructions = "IMPORTANT: what the next window MUST know to not break things",
1023
- context_summary = "current state of the system",
1024
- )
1025
- \`\`\`
1026
-
1027
- The next \`session_start\` automatically includes this handoff. The new window sees exactly what's done, what remains, and what NOT to break.
1028
-
1029
- ### Rule 4 โ€” Session End (LAST thing before closing)
1030
- \`\`\`
1031
- session_end(
1032
- instance_id = "${instance_id}",
1033
- summary = "What was accomplished (2-3 sentences with specifics)",
1034
- files_changed = ["list", "of", "changed", "files"],
1035
- lessons_learned = <number of learn_from_attempts calls made>,
1036
- )
1037
- \`\`\`
1038
-
1039
- > \`session_end\` **automatically extracts and stores lessons** from the summary text โ€” no manual calls needed.
1040
-
1041
- ### Rule 5 โ€” Code Index Invalidation (AUTOMATIC)
1042
- - \`index_project\` tracks MD5 hashes per file โ€” **skips unchanged files automatically**
1043
- - After code changes: next \`index_project\` run re-indexes only modified files
1044
- - After delete/rename: run \`index_project\` again to sync
1045
- - No manual invalidation needed
1046
-
1047
- ---
1048
-
1049
- ## Task-type trigger table
1050
-
1051
- | You're about to... | Call BEFORE | Call AFTER |
1052
- |---|---|---|
1053
- | Deploy anything | \`recall_best_solution("deploy:<component>")\` | \`learn_from_attempts(topic="deploy:...")\` |
1054
- | Fix a bug | \`recall_best_solution("fix:<area>")\` | \`learn_from_attempts(topic="fix:...")\` |
1055
- | Add a feature | \`session_start(focus="feat:<area>")\` | \`learn_from_attempts(topic="feat:...")\` |
1056
- | Infra/server work | \`recall_best_solution("infra:<topic>")\` | \`learn_from_attempts(topic="infra:...")\` |
1057
- | Debug an issue | \`smart_recall("<error message or symptom>")\` | \`learn_from_attempts(topic="debug:...")\` |
1058
-
1059
- ---
1060
-
1061
- ## Available Brain Tools
1062
-
1063
- | Tool | When to use |
1064
- |------|-------------|
1065
- | \`session_start\` | **FIRST** โ€” mandatory at start of every session |
1066
- | \`session_end\` | **LAST** โ€” mandatory at end, auto-learns from summary |
1067
- | \`session_handoff\` | When closing window with remaining tasks |
1068
- | \`learn_from_attempts\` | **AUTOMATIC** after every fix/deploy/discovery |
1069
- | \`recall_best_solution\` | Before any non-trivial task |
1070
- | \`remember_context\` | After analyzing code โ€” save findings for future sessions |
1071
- ${smartRecallNote ? `| \`smart_recall\` | Search brain by meaning/keywords |\n` : ''}\
1072
- | \`recall_context\` | Get exact key (supports glob: \`arch:*\`, \`file:*\`) |
1073
- | \`brain_search\` | BM25+ full-text search over all brain data |
1074
- | \`auto_learn_session\` | Batch-learn from a list of observations (optional) |
1075
- | \`index_project\` | Index source files (smart hash, skips unchanged files) |
1076
- | \`list_remembered\` | See what's cached in the brain |
1077
- | \`forget_context\` | Remove stale context |
1078
-
1079
- ---
1080
-
1081
- ## Instance Details
1082
-
1083
- - **Instance ID:** \`${instance_id}\`
1084
- - **Instance name:** ${inst.name}
1085
- - **Tier:** ${tier}
1086
- - **${layerNote}**
1087
- - **Embedding provider:** ${providerArg}
1088
-
1089
- ---
1090
-
1091
- ## How the 3-layer system works
1092
-
1093
- \`\`\`
1094
- Layer 1 โ€” Storage: Your cachly Valkey instance (${inst.name}) โ€” persists forever
1095
- Layer 2 โ€” Tools: learn_from_attempts ยท recall_best_solution ยท brain_search ยท session_start/end
1096
- Layer 3 โ€” Autopilot: This file โ€” AI reads it and runs tools automatically every session
1097
- \`\`\`
1098
-
1099
- Result: Your AI **never solves the same problem twice** and always picks up exactly where it left off. ๐Ÿš€
1100
- `;
1101
-
1102
-
1103
- const lines: string[] = [
1104
- `๐Ÿง  **cachly AI Memory Setup Complete**`,
1105
- ``,
1106
- `**Instance:** ${inst.name} (${tier}) ยท ID: \`${instance_id}\``,
1107
- `**Embedding Provider:** ${providerArg}`,
1108
- `**Semantic Search:** ${hasVector ? 'โœ… pgvector HNSW available' : 'โš ๏ธ Not available on ' + tier + ' โ€” upgrade to Speed/Business'}`,
1109
- ``,
1110
- `โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€`,
1111
- `**Step 1 โ€” Add to .mcp.json:**`,
1112
- `\`\`\`json`,
1113
- mcpJsonSnippet,
1114
- `\`\`\``,
1115
- ``,
1116
- `**Step 2 โ€” Set your ${providerArg} key:**`,
1117
- `\`\`\`bash`,
1118
- `export ${provInfo.key}="your-key-here"`,
1119
- `\`\`\``,
1120
- `_(${provInfo.hint})_`,
1121
- ``,
1122
- `โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€`,
1123
- `**Step 3 โ€” copilot-instructions.md (Layer 3 Autopilot):**`,
1124
- ``,
1125
- ...(project_dir
1126
- ? [`๐Ÿ” Detecting IDEs in \`${project_dir}\`โ€ฆ`]
1127
- : [`Copy this to \`.github/copilot-instructions.md\` (Copilot), \`CLAUDE.md\` (Claude Code), or \`.cursor/rules\` (Cursor) in your project:`]),
1128
- ``,
1129
- `\`\`\`markdown`,
1130
- copilotInstructions,
1131
- `\`\`\``,
1132
- ``,
1133
- `โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€`,
1134
- `**How the 3 layers work together:**`,
1135
- ` Layer 1 โ†’ Your Valkey instance stores all lessons + context (persists forever)`,
1136
- ` Layer 2 โ†’ MCP tools (learn_from_attempts, recall_best_solution, smart_recall) read/write it`,
1137
- ` Layer 3 โ†’ copilot-instructions.md makes your AI run them automatically`,
1138
- ``,
1139
- `Result: Your AI never solves the same problem twice. ๐Ÿš€`,
1140
- ];
1141
-
1142
- // โ”€โ”€ IDE auto-detection + file writing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1143
- if (project_dir) {
1144
- const { mkdir, writeFile, access } = await import('node:fs/promises');
1145
- const { constants } = await import('node:fs');
1146
-
1147
- const exists = async (p: string) => access(p, constants.F_OK).then(() => true).catch(() => false);
1148
-
1149
- // Detect which IDEs are present based on marker files/dirs
1150
- interface IdeTarget { ide: string; path: string; content: string }
1151
- const targets: IdeTarget[] = [];
1152
- let stopHookWritten = false;
1153
-
1154
- // GitHub Copilot โ€” always write (universal fallback)
1155
- targets.push({
1156
- ide: 'GitHub Copilot',
1157
- path: join(project_dir, '.github', 'copilot-instructions.md'),
1158
- content: copilotInstructions,
1159
- });
1160
-
1161
- // Claude Code โ€” CLAUDE.md in project root
1162
- if (await exists(join(project_dir, 'CLAUDE.md')) || await exists(join(project_dir, '.claude'))) {
1163
- targets.push({
1164
- ide: 'Claude Code',
1165
- path: join(project_dir, 'CLAUDE.md'),
1166
- content: copilotInstructions,
1167
- });
1168
-
1169
- // Claude Code Stop-Hook โ€” auto-saves checkpoint when Claude stops responding
1170
- const claudeDir = join(project_dir, '.claude');
1171
- await mkdir(claudeDir, { recursive: true });
1172
- const stopHook = {
1173
- hooks: {
1174
- Stop: [
1175
- {
1176
- matcher: '',
1177
- hooks: [
1178
- {
1179
- type: 'command',
1180
- command: `npx --yes @cachly-dev/mcp-server@latest checkpoint --instance-id ${instance_id}`,
1181
- },
1182
- ],
1183
- },
1184
- ],
1185
- },
1186
- };
1187
- const settingsPath = join(claudeDir, 'settings.json');
1188
- let existingSettings: Record<string, unknown> = {};
1189
- try {
1190
- const { readFile: rf } = await import('node:fs/promises');
1191
- existingSettings = JSON.parse(await rf(settingsPath, 'utf-8'));
1192
- } catch { /* new file */ }
1193
- const merged = { ...existingSettings, hooks: (stopHook as Record<string, unknown>).hooks };
1194
- await writeFile(settingsPath, JSON.stringify(merged, null, 2), 'utf-8');
1195
- stopHookWritten = true;
1196
- }
1197
-
1198
- // Cursor โ€” .cursor/rules (new format) or .cursorrules (legacy)
1199
- if (
1200
- await exists(join(project_dir, '.cursor')) ||
1201
- await exists(join(project_dir, '.cursorrules'))
1202
- ) {
1203
- const cursorDir = join(project_dir, '.cursor');
1204
- await mkdir(cursorDir, { recursive: true });
1205
- targets.push({
1206
- ide: 'Cursor',
1207
- path: join(cursorDir, 'rules'),
1208
- content: copilotInstructions,
1209
- });
1210
- }
1211
-
1212
- // Windsurf โ€” .windsurfrules
1213
- if (
1214
- await exists(join(project_dir, '.windsurfrules')) ||
1215
- await exists(join(project_dir, '.windsurf'))
1216
- ) {
1217
- targets.push({
1218
- ide: 'Windsurf',
1219
- path: join(project_dir, '.windsurfrules'),
1220
- content: copilotInstructions,
1221
- });
1222
- }
1223
-
1224
- // VS Code (Copilot) โ€” already covered by .github/copilot-instructions.md above
1225
- // Continue.dev โ€” .continue/config.json is JSON, not markdown โ€” skip, copilot-instructions handles it
1226
-
1227
- const written: string[] = [];
1228
- for (const target of targets) {
1229
- const dir = target.path.substring(0, target.path.lastIndexOf('/'));
1230
- await mkdir(dir, { recursive: true });
1231
- await writeFile(target.path, target.content, 'utf-8');
1232
- written.push(`โœ… [${target.ide}] โ†’ \`${target.path.replace(project_dir, '.')}\``);
1233
- }
1234
-
1235
- if (stopHookWritten) {
1236
- written.push(`โœ… [Claude Code Stop-Hook] โ†’ \`.claude/settings.json\` (auto-checkpoint on stop)`);
1237
- }
1238
-
1239
- lines.push(...written);
1240
- }
1241
-
1242
- return lines.join('\n');
1243
- }
1244
-
1245
- default:
1246
- return null;
1247
- }
1248
- }
1249
-