@cachly-dev/mcp-server 0.5.37 → 0.5.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from 'node:crypto';
3
- import { createHash } from 'node:crypto';
4
3
  import { readdir, readFile, stat } from 'node:fs/promises';
5
- import { readFileSync } from 'node:fs';
6
- import { execSync } from 'node:child_process';
7
4
  import { join, relative, extname } from 'node:path';
8
5
  /**
9
- * cachly MCP Server v0.4.0
6
+ * cachly MCP Server v0.5.38
10
7
  *
11
8
  * Exposes cachly.dev as MCP tools so any AI assistant
12
9
  * (GitHub Copilot, Claude, Cursor, Windsurf, Continue.dev …) can:
@@ -34,6 +31,7 @@ import { join, relative, extname } from 'node:path';
34
31
  *
35
32
  * Configuration (env vars):
36
33
  * CACHLY_API_URL – default https://api.cachly.dev
34
+ * CACHLY_AUTH_URL – default https://auth.cachly.dev (Keycloak base URL for health checks)
37
35
  * CACHLY_JWT – your JWT (Keycloak access token)
38
36
  * CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
39
37
  * CACHLY_EMBED_MODEL – override embedding model (optional)
@@ -41,7 +39,6 @@ import { join, relative, extname } from 'node:path';
41
39
 
42
40
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
43
41
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
44
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
45
42
  import {
46
43
  CallToolRequestSchema,
47
44
  ErrorCode,
@@ -53,9 +50,9 @@ import { Redis } from 'ioredis';
53
50
  // ── Config ────────────────────────────────────────────────────────────────────
54
51
 
55
52
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
53
+ const AUTH_URL = process.env.CACHLY_AUTH_URL ?? 'https://auth.cachly.dev';
56
54
  const JWT = process.env.CACHLY_JWT ?? '';
57
55
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
58
- const CURRENT_VERSION = '0.5.26';
59
56
  /**
60
57
  * ┌──────────────────────────────────────────────────────────────────────┐
61
58
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -75,44 +72,6 @@ const CURRENT_VERSION = '0.5.26';
75
72
  */
76
73
  const EMBED_PROVIDER = (process.env.CACHLY_EMBED_PROVIDER ?? detectEmbedProvider()).toLowerCase();
77
74
 
78
- // ── Confidence Decay Config (all values configurable via env) ─────────────────
79
- const CONFIDENCE_WARN_DAYS = Number(process.env.CACHLY_CONFIDENCE_WARN_DAYS ?? 5); // → 0.7
80
- const CONFIDENCE_STALE_DAYS = Number(process.env.CACHLY_CONFIDENCE_STALE_DAYS ?? 10); // → 0.5
81
- const CONFIDENCE_WARN_VALUE = 0.7;
82
- const CONFIDENCE_STALE_VALUE = 0.5;
83
-
84
- /** Calculate current confidence for a lesson based on how long since last verified. */
85
- function calculateConfidence(lesson: { verified_at?: string; ts: string; recall_count?: number }): number {
86
- const ref = lesson.verified_at ?? lesson.ts;
87
- const ageMs = Date.now() - new Date(ref).getTime();
88
- const ageDays = ageMs / 86400000;
89
- if (ageDays >= CONFIDENCE_STALE_DAYS) return CONFIDENCE_STALE_VALUE;
90
- if (ageDays >= CONFIDENCE_WARN_DAYS) return CONFIDENCE_WARN_VALUE;
91
- // Linear interpolation between fresh (1.0) and warn threshold
92
- return 1.0 - (ageDays / CONFIDENCE_WARN_DAYS) * (1.0 - CONFIDENCE_WARN_VALUE);
93
- }
94
-
95
- /** Render a confidence badge string. */
96
- function confidenceBadge(confidence: number, ageDays: number): string {
97
- if (confidence >= 0.9) return '✅';
98
- if (confidence >= 0.7) return `⚠️ (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — verify before applying)`;
99
- return `🔴 STALE (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — likely outdated!)`;
100
- }
101
-
102
- /** Category-specific required fields for structured lessons. */
103
- const STRUCTURED_TEMPLATES: Record<string, { required: string[]; hint: string }> = {
104
- 'deploy': { required: ['commands'], hint: 'deploy:* needs commands[]' },
105
- 'bash': { required: ['commands'], hint: 'bash:* needs commands[]' },
106
- 'infra': { required: ['commands'], hint: 'infra:* needs commands[] + verified on real system' },
107
- 'pricing': { required: [], hint: 'pricing:* — add context with source (e.g. "Stripe Dashboard Apr 2026")' },
108
- 'stripe': { required: [], hint: 'stripe:* — add context with Stripe API version' },
109
- };
110
-
111
- /** Fast content hash for index invalidation (not cryptographic, just change detection) */
112
- function simpleHash(text: string): string {
113
- return createHash('md5').update(text).digest('hex').slice(0, 12);
114
- }
115
-
116
75
  function detectEmbedProvider(): string {
117
76
  // Client-side keys first (direct, fast, no server roundtrip)
118
77
  if (process.env.OPENAI_API_KEY) return 'openai';
@@ -301,18 +260,13 @@ function embedProviderHint(): string {
301
260
  // • Recency boost — newer entries rank higher (exponential decay, 7-day half-life)
302
261
  // • Multi-query splitting — numbered lists, semicolons, conjunctions
303
262
  // • Fuzzy matching with Levenshtein distance ≤ 2
304
- // • Multilingual stopwords (EN, DE, FR, ES, IT, PT, ZH, JA, KO, AR, HE)
305
- // • CJK character bigram extraction — handles Chinese, Japanese, Korean
306
- // • RTL language support (Arabic, Hebrew) — word tokenization + light stemming
307
- // • Romanization matching — katakana → romaji tokens for romaji queries
308
- // • Cross-language retrieval — tech term synonyms EN↔JA↔ZH↔KO↔AR↔HE
263
+ // • Multilingual stopwords (EN, DE, FR, ES, IT, PT)
309
264
  // • Pipeline Redis reads for performance
310
265
  //
311
266
 
312
267
  /**
313
268
  * Stopwords — filtered out during tokenization.
314
- * Covers: English, German, French, Spanish, Italian, Portuguese,
315
- * Chinese (Simplified + Traditional), Japanese, Korean.
269
+ * Covers: English, German, French, Spanish, Italian, Portuguese.
316
270
  * Keeps the index small and scores meaningful.
317
271
  */
318
272
  const STOPWORDS = new Set([
@@ -386,436 +340,15 @@ const STOPWORDS = new Set([
386
340
  'eu', 'tu', 'ele', 'ela', 'nós', 'eles', 'elas', 'você', 'vocês',
387
341
  'não', 'já', 'sim', 'bem', 'muito', 'também', 'ainda', 'sempre',
388
342
  'todo', 'toda', 'todos', 'cada', 'outro', 'outra', 'quando', 'porque',
389
- // ── Chinese (Simplified + Traditional) — high-frequency function characters ──
390
- '的', '了', '是', '在', '不', '和', '我', '他', '这', '中', '大', '为',
391
- '上', '个', '国', '以', '要', '就', '出', '说', '们', '有', '来', '到',
392
- '时', '地', '年', '得', '着', '那', '过', '后', '还', '与', '也', '可',
393
- '于', '从', '但', '而', '被', '把', '让', '使', '对', '很', '都', '一',
394
- '会', '没', '人', '它', '这个', '那个', '什么', '如果', '因为', '所以',
395
- '已经', '可以', '这些', '那些', '我们', '他们', '她们', '它们',
396
- // ── Japanese — common hiragana particles and auxiliary verbs ──
397
- 'の', 'は', 'が', 'を', 'に', 'で', 'と', 'も', 'や', 'か', 'な', 'ね',
398
- 'よ', 'わ', 'て', 'い', 'う', 'え', 'お', 'き', 'く', 'け', 'こ', 'さ',
399
- 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'ぬ', 'ん', 'から', 'まで',
400
- 'より', 'へ', 'です', 'ます', 'ない', 'ある', 'いる', 'する', 'こと',
401
- 'もの', 'ので', 'では', 'には', 'との', 'への', 'から', 'まで',
402
- // ── Korean — common particles and auxiliary forms ──
403
- '이', '가', '은', '는', '을', '를', '의', '에', '와', '과', '도',
404
- '로', '에서', '한', '하', '있', '없', '것', '수', '않', '들',
405
- '이다', '하다', '이고', '이며', '라고', '에게', '에서', '으로',
406
- // ── Arabic — high-frequency function words & particles ──
407
- 'في', 'من', 'إلى', 'على', 'عن', 'مع', 'هذا', 'هذه', 'ذلك', 'تلك',
408
- 'الذي', 'التي', 'الذين', 'اللواتي', 'هو', 'هي', 'هم', 'هن', 'نحن',
409
- 'أنت', 'أنتم', 'أنا', 'كان', 'كانت', 'كانوا', 'يكون', 'تكون',
410
- 'أن', 'إن', 'لأن', 'لكن', 'أو', 'بل', 'ثم', 'حتى', 'إذا', 'كي',
411
- 'قد', 'لقد', 'لم', 'لن', 'ما', 'لا', 'ليس', 'غير', 'بعض', 'كل',
412
- 'جميع', 'أي', 'كيف', 'متى', 'أين', 'لماذا', 'ماذا', 'هل', 'ال',
413
- 'و', 'ف', 'ب', 'ل', 'ك', 'يا', 'أم', 'إلا', 'عند', 'بين',
414
- // ── Hebrew — common particles, pronouns, conjunctions ──
415
- 'של', 'את', 'אל', 'על', 'עם', 'לא', 'הוא', 'היא', 'הם', 'הן',
416
- 'אני', 'אתה', 'אנחנו', 'אתם', 'כי', 'אם', 'אבל', 'גם', 'כבר',
417
- 'רק', 'עוד', 'יש', 'אין', 'מה', 'זה', 'זאת', 'אלה', 'כל', 'כן',
418
- 'לו', 'לה', 'להם', 'בו', 'בה', 'בהם', 'שם', 'כך', 'כן', 'מי',
419
- 'אשר', 'אחרי', 'לפני', 'תחת', 'בין', 'מאז', 'עד', 'כמו', 'אז',
420
- ]);
421
-
422
- // CJK Unicode ranges used for bigram extraction
423
- const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
424
- const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+|[^\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+)/g;
425
-
426
- // RTL Unicode ranges: Arabic (U+0600–U+06FF + extended), Hebrew (U+0590–U+05FF)
427
- const RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufb4f\ufb50-\ufdff\ufe70-\ufeff]/;
428
-
429
- // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
430
- // Digraphs must be listed before single chars so they match first.
431
- const KATA_ROMAJI_TABLE: [string, string][] = [
432
- // Special combinations for loanwords
433
- ['ファ', 'fa'], ['フィ', 'fi'], ['フェ', 'fe'], ['フォ', 'fo'],
434
- ['ティ', 'ti'], ['ディ', 'di'], ['トゥ', 'tu'], ['ドゥ', 'du'],
435
- ['ウィ', 'wi'], ['ウェ', 'we'], ['ウォ', 'wo'],
436
- ['チェ', 'che'], ['ジェ', 'je'], ['シェ', 'she'],
437
- ['イェ', 'ye'], ['ヴァ', 'va'], ['ヴィ', 'vi'], ['ヴェ', 've'], ['ヴォ', 'vo'],
438
- // Digraphs (2-char → romaji)
439
- ['キャ', 'kya'], ['キュ', 'kyu'], ['キョ', 'kyo'],
440
- ['シャ', 'sha'], ['シュ', 'shu'], ['ショ', 'sho'],
441
- ['チャ', 'cha'], ['チュ', 'chu'], ['チョ', 'cho'],
442
- ['ニャ', 'nya'], ['ニュ', 'nyu'], ['ニョ', 'nyo'],
443
- ['ヒャ', 'hya'], ['ヒュ', 'hyu'], ['ヒョ', 'hyo'],
444
- ['ミャ', 'mya'], ['ミュ', 'myu'], ['ミョ', 'myo'],
445
- ['リャ', 'rya'], ['リュ', 'ryu'], ['リョ', 'ryo'],
446
- ['ギャ', 'gya'], ['ギュ', 'gyu'], ['ギョ', 'gyo'],
447
- ['ジャ', 'ja'], ['ジュ', 'ju'], ['ジョ', 'jo'],
448
- ['ビャ', 'bya'], ['ビュ', 'byu'], ['ビョ', 'byo'],
449
- ['ピャ', 'pya'], ['ピュ', 'pyu'], ['ピョ', 'pyo'],
450
- // Single chars
451
- ['ア', 'a'], ['イ', 'i'], ['ウ', 'u'], ['エ', 'e'], ['オ', 'o'],
452
- ['カ', 'ka'], ['キ', 'ki'], ['ク', 'ku'], ['ケ', 'ke'], ['コ', 'ko'],
453
- ['サ', 'sa'], ['シ', 'shi'], ['ス', 'su'], ['セ', 'se'], ['ソ', 'so'],
454
- ['タ', 'ta'], ['チ', 'chi'], ['ツ', 'tsu'], ['テ', 'te'], ['ト', 'to'],
455
- ['ナ', 'na'], ['ニ', 'ni'], ['ヌ', 'nu'], ['ネ', 'ne'], ['ノ', 'no'],
456
- ['ハ', 'ha'], ['ヒ', 'hi'], ['フ', 'fu'], ['ヘ', 'he'], ['ホ', 'ho'],
457
- ['マ', 'ma'], ['ミ', 'mi'], ['ム', 'mu'], ['メ', 'me'], ['モ', 'mo'],
458
- ['ヤ', 'ya'], ['ユ', 'yu'], ['ヨ', 'yo'],
459
- ['ラ', 'ra'], ['リ', 'ri'], ['ル', 'ru'], ['レ', 're'], ['ロ', 'ro'],
460
- ['ワ', 'wa'], ['ヲ', 'wo'], ['ン', 'n'],
461
- // Voiced
462
- ['ガ', 'ga'], ['ギ', 'gi'], ['グ', 'gu'], ['ゲ', 'ge'], ['ゴ', 'go'],
463
- ['ザ', 'za'], ['ジ', 'ji'], ['ズ', 'zu'], ['ゼ', 'ze'], ['ゾ', 'zo'],
464
- ['ダ', 'da'], ['ヂ', 'ji'], ['ヅ', 'zu'], ['デ', 'de'], ['ド', 'do'],
465
- ['バ', 'ba'], ['ビ', 'bi'], ['ブ', 'bu'], ['ベ', 'be'], ['ボ', 'bo'],
466
- ['パ', 'pa'], ['ピ', 'pi'], ['プ', 'pu'], ['ペ', 'pe'], ['ポ', 'po'],
467
- ['ヴ', 'v'],
468
- // Long vowel / small chars
469
- ['ー', ''], ['ァ', 'a'], ['ィ', 'i'], ['ゥ', 'u'], ['ェ', 'e'], ['ォ', 'o'],
470
- ['ッ', ''], // handled separately (doubles next consonant)
471
- ];
472
-
473
- const KATA_MAP = new Map<string, string>(KATA_ROMAJI_TABLE);
474
-
475
- /**
476
- * Convert a katakana string to Hepburn romaji.
477
- * Handles digraphs, geminate consonants (ッ), and long vowel marks (ー).
478
- */
479
- function katakanaToRomaji(kata: string): string {
480
- let result = '';
481
- let i = 0;
482
- while (i < kata.length) {
483
- // Geminate consonant: ッ doubles the following consonant
484
- if (kata[i] === 'ッ' && i + 1 < kata.length) {
485
- const next = KATA_MAP.get(kata[i + 1]) ?? KATA_MAP.get(kata[i + 1] + kata[i + 2]) ?? '';
486
- if (next.length > 0) result += next[0]; // double first consonant
487
- i++;
488
- continue;
489
- }
490
- // Try 2-char digraph first
491
- if (i + 1 < kata.length) {
492
- const two = kata[i] + kata[i + 1];
493
- const r2 = KATA_MAP.get(two);
494
- if (r2 !== undefined) { result += r2; i += 2; continue; }
495
- }
496
- // Single char
497
- const r1 = KATA_MAP.get(kata[i]);
498
- if (r1 !== undefined) result += r1;
499
- i++;
500
- }
501
- return result;
502
- }
503
-
504
- /**
505
- * Arabic light stemmer — strips definite article and common prefix particles.
506
- * Handles: ال (al-), و (wa-), ب (bi-), ل (li-), ف (fa-), ك (ka-).
507
- * Only strips when the result is still ≥ 3 chars to avoid over-stemming.
508
- */
509
- function arabicLightStem(word: string): string {
510
- if (word.startsWith('ال') && word.length > 4) return word.slice(2);
511
- if (word.length > 3 && 'وبلفك'.includes(word[0]) && RTL_RE.test(word[1])) return word.slice(1);
512
- return word;
513
- }
514
-
515
- // ── Cross-lingual tech term dictionary ────────────────────────────────────────
516
- // Maps tokens (English, Japanese katakana, Chinese, Korean, Arabic, Hebrew)
517
- // to their equivalents in other languages. Used to expand tokens at search time
518
- // so "deploy" finds documents containing "デプロイ" / "部署" / "배포", and vice versa.
519
- //
520
- // Keys must be lowercase. CJK values will be bigram-expanded at use time.
521
- const CROSS_LINGUAL_MAP = new Map<string, string[]>([
522
- // English → all
523
- ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying']],
524
- ['deployment', ['デプロイ', '部署', '배포', 'deploy']],
525
- ['container', ['コンテナ', '容器', '컨테이너', 'docker']],
526
- ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת']],
527
- ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات']],
528
- ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון']],
529
- ['error', ['エラー', '错误', '오류', 'خطأ', 'שגיאה', 'exception', 'err']],
530
- ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل']],
531
- ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון']],
532
- ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה']],
533
- ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה']],
534
- ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות']],
535
- ['kubernetes', ['クベルネテス', 'k8s', 'kube']],
536
- ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת']],
537
- ['timeout', ['タイムアウト', '超时', '타임아웃', 'مهلة']],
538
- ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון']],
539
- ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות']],
540
- ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה']],
541
- ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון']],
542
- ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן']],
543
- ['port', ['ポート', '端口', '포트', 'منفذ', 'פורט']],
544
- ['file', ['ファイル', '文件', '파일', 'ملف', 'קובץ']],
545
- ['image', ['イメージ', '镜像', '이미지', 'صورة', 'תמונה']],
546
- ['volume', ['ボリューム', '卷', '볼륨']],
547
- ['cluster', ['クラスター', '集群', '클러스터']],
548
- ['node', ['ノード', '节点', '노드']],
549
- ['service', ['サービス', '服务', '서비스', 'svc', 'خدمة', 'שירות']],
550
- ['certificate', ['証明書', '证书', '인증서', 'cert', 'ssl', 'tls', 'شهادة', 'תעודה']],
551
- ['password', ['パスワード', '密码', '비밀번호', 'passwd', 'pwd', 'كلمة المرور', 'סיסמה']],
552
- ['token', ['トークン', '令牌', '토큰', 'jwt', 'secret', 'رمز', 'אסימון']],
553
- ['health', ['ヘルス', '健康', '헬스', 'healthcheck', 'probe', 'صحة']],
554
- ['migration', ['マイグレーション', '迁移', '마이그레이션', 'migrate', 'هجرة']],
555
- ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי']],
556
- ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור']],
557
- ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput']],
558
- ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור']],
559
- ['queue', ['キュー', '队列', '큐', 'طابور']],
560
- ['redis', ['レディス', '레디스']],
561
- ['nginx', ['エンジンエックス']],
562
- ['linux', ['リナックス', 'لينكس']],
563
- ['api', ['エーピーアイ', '接口', 'endpoint', 'واجهة برمجية', 'ממשק']],
564
- ['healthcheck', ['ヘルスチェック', 'health', 'probe', 'فحص الصحة']],
565
- ['loadbalancer',['ロードバランサー', '负载均衡', '로드밸런서', 'lb']],
566
- ['ssl', ['tls', 'https', 'certificate', 'cert']],
567
- ['tls', ['ssl', 'https', 'certificate', 'cert']],
568
- ['docker', ['container', 'コンテナ', '容器', '컨테이너', 'دوكر']],
569
- ['git', ['version control', 'repo', 'repository', 'commit', 'push', 'pull']],
570
- ['ci', ['pipeline', 'github actions', 'gitlab', 'jenkins', 'build']],
571
- ['cd', ['deploy', 'deployment', 'release']],
572
- ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים']],
573
- ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار']],
574
- ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש']],
575
- ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה']],
576
- // Japanese katakana → English
577
- ['デプロイ', ['deploy', 'deployment']],
578
- ['コンテナ', ['container', 'docker']],
579
- ['サーバー', ['server']],
580
- ['サーバ', ['server']],
581
- ['データベース',['database', 'db']],
582
- ['キャッシュ', ['cache']],
583
- ['エラー', ['error', 'err']],
584
- ['バグ', ['bug', 'issue']],
585
- ['ビルド', ['build']],
586
- ['テスト', ['test']],
587
- ['認証', ['auth', 'authentication']],
588
- ['設定', ['config', 'configuration']],
589
- ['インストール',['install']],
590
- ['アップデート',['update', 'upgrade']],
591
- ['ログ', ['log', 'logs']],
592
- ['ポート', ['port']],
593
- ['ファイル', ['file']],
594
- ['イメージ', ['image']],
595
- ['クラスター', ['cluster']],
596
- ['ノード', ['node']],
597
- ['サービス', ['service']],
598
- ['パスワード', ['password']],
599
- ['トークン', ['token']],
600
- ['バックアップ',['backup']],
601
- ['モニター', ['monitor', 'monitoring']],
602
- ['接続', ['connection']],
603
- ['キュー', ['queue']],
604
- ['ヘルスチェック',['healthcheck', 'health check', 'health']],
605
- ['ロードバランサー',['loadbalancer', 'load balancer']],
606
- ['デバッグ', ['debug']],
607
- ['クラッシュ', ['crash', 'panic']],
608
- ['再起動', ['restart', 'reboot']],
609
- ['権限', ['permission', 'access']],
610
- // Chinese → English
611
- ['部署', ['deploy', 'deployment']],
612
- ['容器', ['container', 'docker']],
613
- ['服务器', ['server']],
614
- ['数据库', ['database']],
615
- ['缓存', ['cache']],
616
- ['错误', ['error']],
617
- ['修复', ['fix', 'bugfix']],
618
- ['构建', ['build']],
619
- ['测试', ['test']],
620
- ['认证', ['auth']],
621
- ['配置', ['config']],
622
- ['安装', ['install']],
623
- ['更新', ['update']],
624
- ['日志', ['log']],
625
- ['端口', ['port']],
626
- ['镜像', ['image']],
627
- ['集群', ['cluster']],
628
- ['节点', ['node']],
629
- ['服务', ['service']],
630
- ['密码', ['password']],
631
- ['令牌', ['token']],
632
- ['备份', ['backup']],
633
- ['监控', ['monitor']],
634
- ['连接', ['connection']],
635
- ['队列', ['queue']],
636
- ['性能', ['performance']],
637
- ['内存', ['memory']],
638
- ['调试', ['debug']],
639
- ['崩溃', ['crash']],
640
- ['重启', ['restart']],
641
- ['权限', ['permission']],
642
- // Korean → English
643
- ['배포', ['deploy', 'deployment']],
644
- ['컨테이너', ['container']],
645
- ['서버', ['server']],
646
- ['데이터베이스',['database']],
647
- ['캐시', ['cache']],
648
- ['오류', ['error']],
649
- ['수정', ['fix']],
650
- ['빌드', ['build']],
651
- ['테스트', ['test']],
652
- ['인증', ['auth']],
653
- ['설정', ['config']],
654
- ['설치', ['install']],
655
- ['업데이트', ['update']],
656
- ['로그', ['log']],
657
- ['포트', ['port']],
658
- ['이미지', ['image']],
659
- ['클러스터', ['cluster']],
660
- ['노드', ['node']],
661
- ['서비스', ['service']],
662
- ['토큰', ['token']],
663
- ['백업', ['backup']],
664
- ['모니터링', ['monitor']],
665
- ['연결', ['connection']],
666
- ['큐', ['queue']],
667
- ['성능', ['performance']],
668
- ['메모리', ['memory']],
669
- ['디버그', ['debug']],
670
- ['크래시', ['crash']],
671
- ['재시작', ['restart']],
672
- ['권한', ['permission']],
673
- // Arabic → English
674
- ['خطأ', ['error']],
675
- ['إصلاح', ['fix']],
676
- ['بناء', ['build']],
677
- ['اختبار', ['test']],
678
- ['مصادقة', ['auth']],
679
- ['إعدادات', ['config']],
680
- ['تثبيت', ['install']],
681
- ['تحديث', ['update']],
682
- ['سجل', ['log']],
683
- ['صورة', ['image']],
684
- ['خدمة', ['service']],
685
- ['نسخ احتياطي', ['backup']],
686
- ['مراقبة', ['monitor']],
687
- ['اتصال', ['connection']],
688
- ['ذاكرة', ['memory']],
689
- ['تصحيح', ['debug']],
690
- ['شبكة', ['network']],
691
- // Hebrew → English
692
- ['שגיאה', ['error']],
693
- ['תיקון', ['fix']],
694
- ['בנייה', ['build']],
695
- ['בדיקה', ['test']],
696
- ['אימות', ['auth']],
697
- ['הגדרות', ['config']],
698
- ['התקנה', ['install']],
699
- ['עדכון', ['update']],
700
- ['יומן', ['log']],
701
- ['שרת', ['server']],
702
- ['שירות', ['service']],
703
- ['גיבוי', ['backup']],
704
- ['ניטור', ['monitor']],
705
- ['חיבור', ['connection']],
706
- ['זיכרון', ['memory']],
707
- ['רשת', ['network']],
708
- ['תמונה', ['image']],
709
- ['סיסמה', ['password']],
710
- ['הרשאה', ['permission']],
711
343
  ]);
712
344
 
713
- /**
714
- * Expand a token to its cross-lingual synonyms.
715
- * Returns synonyms to be added to the token stream (pre-tokenized).
716
- * CJK synonyms are NOT pre-bigram'd here — caller handles that.
717
- */
718
- function expandCrossLingual(token: string): string[] {
719
- return CROSS_LINGUAL_MAP.get(token) ?? [];
720
- }
721
-
722
- /**
723
- * Tokenize text into meaningful keywords.
724
- * Handles accented Latin, RTL (Arabic, Hebrew), and CJK (Chinese, Japanese, Korean).
725
- *
726
- * Features:
727
- * • CJK → character bigrams
728
- * • Katakana → additionally emits Hepburn romaji tokens (enables romaji queries)
729
- * • Arabic/Hebrew → word tokenization with light Arabic stemming
730
- * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE)
731
- */
345
+ /** Tokenize text into meaningful keywords. Handles unicode (accented chars). */
732
346
  function tokenize(text: string): string[] {
733
- const tokens: string[] = [];
734
- const lower = text.toLowerCase();
735
-
736
- for (const [seg] of lower.matchAll(SEGMENT_RE)) {
737
- if (CJK_RE.test(seg)) {
738
- // CJK segment — extract overlapping character bigrams
739
- for (let i = 0; i < seg.length - 1; i++) {
740
- const bigram = seg[i] + seg[i + 1];
741
- if (!STOPWORDS.has(seg[i]) && !STOPWORDS.has(bigram)) {
742
- tokens.push(bigram);
743
- }
744
- }
745
- // Trailing single character (for 1-char CJK terms)
746
- if (seg.length >= 1) {
747
- const last = seg[seg.length - 1];
748
- if (!STOPWORDS.has(last)) tokens.push(last);
749
- }
750
- // Whole-segment cross-lingual lookup (for known tech terms like デプロイ, 部署, 배포)
751
- // This ensures CJK full-words map to their English equivalents
752
- const segSyns = expandCrossLingual(seg);
753
- for (const syn of segSyns) {
754
- if (!CJK_RE.test(syn)) {
755
- syn.split(/\s+/).filter(w => w.length > 1 && !STOPWORDS.has(w)).forEach(w => tokens.push(w));
756
- }
757
- }
758
- // Romanization: extract katakana-only portion and emit romaji tokens.
759
- // This allows users to search "kontena" or "depuroi" to find katakana docs.
760
- const kataOnly = seg.replace(/[^\u30a0-\u30ff]/g, '');
761
- if (kataOnly.length >= 2) {
762
- const romaji = katakanaToRomaji(kataOnly);
763
- if (romaji.length >= 2 && romaji.length <= 40 && !STOPWORDS.has(romaji)) {
764
- tokens.push(romaji);
765
- }
766
- }
767
- } else if (RTL_RE.test(seg)) {
768
- // RTL segment (Arabic / Hebrew) — space-separated words with light stemming
769
- const words = seg.trim().split(/\s+/);
770
- for (const w of words) {
771
- if (w.length <= 1 || STOPWORDS.has(w)) continue;
772
- const stemmed = arabicLightStem(w);
773
- if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
774
- }
775
- } else {
776
- // Latin / other — existing logic preserved exactly
777
- const words = seg
778
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
779
- .split(/\s+/)
780
- .filter(w => w.length > 1 && !STOPWORDS.has(w));
781
- tokens.push(...words);
782
- }
783
- }
784
-
785
- // ── Cross-lingual expansion ──────────────────────────────────────────────
786
- // For each token, look up synonyms in other languages and add them.
787
- // CJK synonyms are bigram-expanded inline; Latin/RTL synonyms added as-is.
788
- const expansions: string[] = [];
789
- const seen = new Set(tokens);
790
- for (const tok of tokens) {
791
- const syns = expandCrossLingual(tok);
792
- for (const syn of syns) {
793
- if (seen.has(syn)) continue;
794
- seen.add(syn);
795
- if (CJK_RE.test(syn)) {
796
- // CJK synonym → expand to character bigrams
797
- for (let i = 0; i < syn.length - 1; i++) {
798
- const bg = syn[i] + syn[i + 1];
799
- if (!STOPWORDS.has(syn[i]) && !STOPWORDS.has(bg)) expansions.push(bg);
800
- }
801
- if (syn.length >= 1) {
802
- const last = syn[syn.length - 1];
803
- if (!STOPWORDS.has(last)) expansions.push(last);
804
- }
805
- } else if (RTL_RE.test(syn)) {
806
- // RTL synonym
807
- const stemmed = arabicLightStem(syn);
808
- if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
809
- } else {
810
- // Latin synonym — split in case of multi-word (e.g. "load balancer")
811
- const parts = syn.split(/\s+/).filter(w => w.length > 1 && !STOPWORDS.has(w));
812
- expansions.push(...parts);
813
- }
814
- }
815
- }
816
- tokens.push(...expansions);
817
-
818
- return tokens;
347
+ return text
348
+ .toLowerCase()
349
+ .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
350
+ .split(/\s+/)
351
+ .filter(w => w.length > 1 && !STOPWORDS.has(w));
819
352
  }
820
353
 
821
354
  /**
@@ -1128,8 +661,7 @@ async function keywordSearch(
1128
661
  }
1129
662
 
1130
663
  // ── Exported for testing ──────────────────────────────────────────────────────
1131
- export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
1132
- katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
664
+ export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS };
1133
665
 
1134
666
  // ── Types ─────────────────────────────────────────────────────────────────────
1135
667
 
@@ -1245,6 +777,20 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
1245
777
  },
1246
778
  });
1247
779
  if (!res.ok) {
780
+ if (res.status === 401 || res.status === 403) {
781
+ throw new McpError(
782
+ ErrorCode.InvalidRequest,
783
+ `cachly API: authentication failed (${res.status}) — your JWT may be expired or invalid.\n` +
784
+ `Get a new API token at https://cachly.dev/settings or run \`get_api_status\` to check token expiry.`
785
+ );
786
+ }
787
+ if (res.status === 503 || res.status === 502) {
788
+ throw new McpError(
789
+ ErrorCode.InternalError,
790
+ `cachly API: auth service unavailable (${res.status}) — the authentication service may be temporarily down.\n` +
791
+ `Please try again in a moment or contact support@cachly.dev`
792
+ );
793
+ }
1248
794
  const body = await res.json().catch(() => ({ error: res.statusText }));
1249
795
  throw new McpError(
1250
796
  ErrorCode.InternalError,
@@ -1271,8 +817,8 @@ const TOOLS = [
1271
817
  description:
1272
818
  'Create a new managed Valkey/Redis cache instance on cachly.dev. ' +
1273
819
  'Free tier provisions in ~30 seconds. Paid tiers return a Stripe checkout URL. ' +
1274
- 'Available tiers: free (25 MB), dev (200 MB, €19/mo), pro (900 MB, €49/mo), ' +
1275
- 'speed (900 MB Dragonfly + Semantic Cache, €79/mo), business (7 GB, €199/mo).',
820
+ 'Available tiers: free (30 MB), dev (256 MB, €8/mo), pro (1 GB, €25/mo), ' +
821
+ 'speed (1 GB Dragonfly + Semantic Cache, €39/mo), business (8 GB, €99/mo).',
1276
822
  inputSchema: {
1277
823
  type: 'object',
1278
824
  properties: {
@@ -1647,10 +1193,10 @@ const TOOLS = [
1647
1193
  {
1648
1194
  name: 'get_api_status',
1649
1195
  description:
1650
- 'Check the cachly API health and your authentication status. ' +
1651
- 'Returns whether the JWT is valid, your user ID (sub claim), ' +
1652
- 'token expiry, and the auth provider (keycloak). ' +
1653
- 'Use this to debug connection issues or verify your CACHLY_JWT is correct.',
1196
+ 'Check the cachly API health, auth service reachability, and your authentication status. ' +
1197
+ 'Returns API health, whether the Keycloak auth service is reachable, JWT validity, ' +
1198
+ 'user ID (sub claim), token expiry, and the auth provider. ' +
1199
+ 'Use this to debug connection issues, verify your CACHLY_JWT, or diagnose "Auth Service Unavailable" errors.',
1654
1200
  inputSchema: { type: 'object', properties: {}, required: [] },
1655
1201
  },
1656
1202
  // ── Thinking/Context Cache (for AI assistants) ────────────────────────────
@@ -1741,9 +1287,7 @@ const TOOLS = [
1741
1287
  'Fields: topic (short slug like "deploy:web"), outcome ("success"|"failure"), ' +
1742
1288
  'what_worked (what solved it), what_failed (what did NOT work), context (extra details). ' +
1743
1289
  'Supports structured metadata: severity, file_paths (files involved), commands (working commands), tags. ' +
1744
- 'Deduplication: if a lesson for this topic already exists, it is updated with full audit trail. ' +
1745
- 'Contradiction detection: warns if new outcome conflicts with existing lesson outcome. ' +
1746
- 'Confidence: lesson starts at 1.0, decays after 5d (→0.7) and 10d (→0.5) without recall. ' +
1290
+ 'Deduplication: if a lesson for this topic already exists, it is updated instead of duplicated. ' +
1747
1291
  'Example: learn_from_attempts(topic="deploy:api", outcome="success", what_worked="nohup docker compose up -d --build", what_failed="docker compose up hangs on SSH timeout", severity="critical", commands=["nohup docker compose up -d --build"])',
1748
1292
  inputSchema: {
1749
1293
  type: 'object',
@@ -1774,15 +1318,6 @@ const TOOLS = [
1774
1318
  items: { type: 'string' },
1775
1319
  description: 'Topic tags for filtering (e.g. ["bash", "deploy", "env"])',
1776
1320
  },
1777
- depends_on: {
1778
- type: 'array',
1779
- items: { type: 'string' },
1780
- description: 'Prerequisites this lesson depends on (e.g. ["node:>=20", "docker:running", "wireguard:active"]). When a dependency is marked stale, all dependent lessons get needs_review.',
1781
- },
1782
- author: {
1783
- type: 'string',
1784
- description: 'Name or handle of the person storing this lesson (e.g. "alice", "bob"). Used for Team Telepathy — teammates see each other\'s lessons in session_start.',
1785
- },
1786
1321
  },
1787
1322
  required: ['instance_id', 'topic', 'outcome', 'what_worked'],
1788
1323
  },
@@ -1792,9 +1327,7 @@ const TOOLS = [
1792
1327
  description:
1793
1328
  'Recall the best known solution for a topic from past lessons. ' +
1794
1329
  'Call this BEFORE attempting any task that might have been done before. ' +
1795
- 'Returns the most recent successful lesson for the topic, with confidence indicator. ' +
1796
- '⚠️ badge = lesson is >5d old (verify before applying). 🔴 = >10d old (likely stale!). ' +
1797
- 'Recalling a lesson resets its confidence clock to 1.0 (marks as recently verified). ' +
1330
+ 'Returns the most recent successful lesson for the topic, or a summary of attempts. ' +
1798
1331
  'Example: recall_best_solution(topic="deploy:web") → returns the working deploy command.',
1799
1332
  inputSchema: {
1800
1333
  type: 'object',
@@ -1805,6 +1338,21 @@ const TOOLS = [
1805
1338
  required: ['instance_id', 'topic'],
1806
1339
  },
1807
1340
  },
1341
+ {
1342
+ name: 'forget_lesson',
1343
+ description:
1344
+ 'Delete a stored lesson from the Brain. Use this when a lesson is wrong, outdated, or was learned incorrectly. ' +
1345
+ 'Removes both the best-solution entry and the attempt history for the topic. ' +
1346
+ 'Example: forget_lesson(topic="deploy:web") → deletes the wrong deployment lesson so a correct one can be stored.',
1347
+ inputSchema: {
1348
+ type: 'object',
1349
+ properties: {
1350
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1351
+ topic: { type: 'string', description: 'Exact topic slug to delete, e.g. "deploy:web"' },
1352
+ },
1353
+ required: ['instance_id', 'topic'],
1354
+ },
1355
+ },
1808
1356
  {
1809
1357
  name: 'smart_recall',
1810
1358
  description:
@@ -1827,8 +1375,7 @@ const TOOLS = [
1827
1375
  description:
1828
1376
  'Single-call session briefing. Call this at the START of every session INSTEAD of multiple separate smart_recall/recall_best_solution calls. ' +
1829
1377
  'Returns: last session summary, recent lessons sorted by recency, relevant lessons for your focus area, ' +
1830
- 'open failures (topics with only failure outcomes), brain health stats, team telepathy (what teammates learned this week), ' +
1831
- 'predictive pre-warnings (if your focus area has known failure patterns), and memory crystals (compressed wisdom from old sessions). ' +
1378
+ 'open failures (topics with only failure outcomes), and brain health stats (lesson count, context count). ' +
1832
1379
  'Also saves a session start marker so session_end can compute duration.',
1833
1380
  inputSchema: {
1834
1381
  type: 'object',
@@ -1838,10 +1385,6 @@ const TOOLS = [
1838
1385
  type: 'string',
1839
1386
  description: 'Keywords for what you plan to work on today (e.g. "deploy infra api"). Used to surface relevant lessons at the top.',
1840
1387
  },
1841
- author: {
1842
- type: 'string',
1843
- description: 'Your name or handle (e.g. "alice"). Enables Team Telepathy — filters YOUR lessons vs TEAM lessons from past 7 days.',
1844
- },
1845
1388
  },
1846
1389
  required: ['instance_id'],
1847
1390
  },
@@ -1852,8 +1395,7 @@ const TOOLS = [
1852
1395
  'Save a session summary when you finish working. ' +
1853
1396
  'Records what was accomplished, files changed, and lesson count. ' +
1854
1397
  'The next session_start will show this summary as "Last session". ' +
1855
- 'Call this when ending a work session, before going idle, or before summarizing. ' +
1856
- 'Ambient Learning: if workspace_path is provided, reads git log since session start and auto-learns from commits.',
1398
+ 'Call this when ending a work session, before going idle, or before summarizing.',
1857
1399
  inputSchema: {
1858
1400
  type: 'object',
1859
1401
  properties: {
@@ -1871,67 +1413,10 @@ const TOOLS = [
1871
1413
  type: 'number',
1872
1414
  description: 'Number of new lessons stored this session (optional)',
1873
1415
  },
1874
- workspace_path: {
1875
- type: 'string',
1876
- description: 'Absolute path to the project root (e.g. "/Users/you/myproject"). Enables Ambient Learning — reads git log since session start and auto-learns from commit messages.',
1877
- },
1878
1416
  },
1879
1417
  required: ['instance_id', 'summary'],
1880
1418
  },
1881
1419
  },
1882
- // ── Session Handoff — cross-window continuity ─────────────────────────────
1883
- {
1884
- name: 'session_handoff',
1885
- description:
1886
- 'Save a detailed handoff for the NEXT chat window / session. ' +
1887
- 'Stores: current progress, TODO list (done + remaining), changed files with descriptions, ' +
1888
- 'instructions for the next assistant, and any incomplete work. ' +
1889
- 'The next session_start automatically includes this handoff so the new window knows EXACTLY what happened and what remains. ' +
1890
- 'Call this BEFORE closing a chat window, especially if work is incomplete. ' +
1891
- 'This prevents the "continue" problem where new windows lose context, skip tasks, or produce broken code.',
1892
- inputSchema: {
1893
- type: 'object',
1894
- properties: {
1895
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
1896
- completed_tasks: {
1897
- type: 'array',
1898
- items: { type: 'string' },
1899
- description: 'Tasks that were fully completed (e.g. "Implemented brainSearch() in JS SDK")',
1900
- },
1901
- remaining_tasks: {
1902
- type: 'array',
1903
- items: { type: 'string' },
1904
- description: 'Tasks NOT yet done — the next window MUST pick these up',
1905
- },
1906
- files_changed: {
1907
- type: 'array',
1908
- items: {
1909
- type: 'object',
1910
- properties: {
1911
- path: { type: 'string', description: 'File path relative to project root' },
1912
- status: { type: 'string', enum: ['complete', 'partial', 'broken'], description: 'State of this file' },
1913
- description: { type: 'string', description: 'What was changed and what still needs work' },
1914
- },
1915
- required: ['path', 'status'],
1916
- },
1917
- description: 'Changed files with their current state — marks partial/broken files so next window knows to fix them',
1918
- },
1919
- instructions: {
1920
- type: 'string',
1921
- description: 'Free-form instructions for the next assistant. Be specific: what to do next, what to avoid, what broke.',
1922
- },
1923
- context_summary: {
1924
- type: 'string',
1925
- description: 'Brief summary of what happened this session (architecture decisions, key findings, blockers)',
1926
- },
1927
- blocked_on: {
1928
- type: 'string',
1929
- description: 'If work is blocked, describe what is needed to unblock (e.g. "waiting for API deploy", "needs user input on design")',
1930
- },
1931
- },
1932
- required: ['instance_id', 'completed_tasks', 'remaining_tasks'],
1933
- },
1934
- },
1935
1420
  // ── AI Brain — Extended features ─────────────────────────────────────────
1936
1421
  {
1937
1422
  name: 'auto_learn_session',
@@ -2022,154 +1507,16 @@ const TOOLS = [
2022
1507
  required: ['instance_id'],
2023
1508
  },
2024
1509
  },
2025
- {
2026
- name: 'team_synthesize',
2027
- description:
2028
- 'Team Brain Synthesis — merge multiple contributors\' lessons on the same topic into one canonical version. ' +
2029
- 'When 2+ developers store lessons for the same topic with different details, this proposes the best merged version. ' +
2030
- 'Shows: all contributions by author, what worked (consensus), what failed (union), canonical lesson to store. ' +
2031
- 'Use this when onboarding new team members or before documenting a process.',
2032
- inputSchema: {
2033
- type: 'object',
2034
- properties: {
2035
- instance_id: { type: 'string', description: 'UUID of the shared team brain instance' },
2036
- topic: { type: 'string', description: 'Topic slug to synthesize (e.g. "deploy:api")' },
2037
- },
2038
- required: ['instance_id', 'topic'],
2039
- },
2040
- },
2041
- {
2042
- name: 'memory_crystalize',
2043
- description:
2044
- 'Compress the last 30-50 sessions and auto-learned lessons into a dense Memory Crystal. ' +
2045
- 'A crystal is a compact, structured summary of everything the brain learned — grouped by category (deploy, fix, debug, …). ' +
2046
- 'Crystals survive session cleanup and appear in session_start once enough sessions have accumulated. ' +
2047
- 'Run this monthly or after a big milestone to preserve institutional knowledge. ' +
2048
- 'Returns a digest of what was crystallized.',
2049
- inputSchema: {
2050
- type: 'object',
2051
- properties: {
2052
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
2053
- label: {
2054
- type: 'string',
2055
- description: 'Optional label for this crystal (e.g. "Q1 2026", "v2 launch"). Auto-generated from date if omitted.',
2056
- },
2057
- },
2058
- required: ['instance_id'],
2059
- },
2060
- },
2061
- // ── Roadmap — Persistent project plan tracker ───────────────────────────
2062
- {
2063
- name: 'roadmap_add',
2064
- description:
2065
- 'Add a new item to the persistent project roadmap stored in the Brain. ' +
2066
- 'Items survive across sessions and editors — the roadmap is always up to date. ' +
2067
- 'Use for features, bugs, refactors, or any planned work. ' +
2068
- 'Call roadmap_list to see all open items, roadmap_next to get the next actionable item.',
2069
- inputSchema: {
2070
- type: 'object',
2071
- properties: {
2072
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
2073
- title: { type: 'string', description: 'Short title of the task/feature (3–10 words)' },
2074
- description: { type: 'string', description: 'What needs to be done, acceptance criteria, context' },
2075
- priority: {
2076
- type: 'string',
2077
- enum: ['critical', 'high', 'medium', 'low'],
2078
- description: 'Priority level (default: medium)',
2079
- },
2080
- tags: {
2081
- type: 'array',
2082
- items: { type: 'string' },
2083
- description: 'Tags for filtering (e.g. ["api", "web", "sdk", "infra"])',
2084
- },
2085
- milestone: { type: 'string', description: 'Milestone/epic this belongs to (optional)' },
2086
- },
2087
- required: ['instance_id', 'title'],
2088
- },
2089
- },
2090
- {
2091
- name: 'roadmap_update',
2092
- description:
2093
- 'Update the status, priority, or details of a roadmap item. ' +
2094
- 'Use to move items through the lifecycle: planned → in-progress → done (or blocked/cancelled). ' +
2095
- 'Also use to add notes/findings while working on an item.',
2096
- inputSchema: {
2097
- type: 'object',
2098
- properties: {
2099
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
2100
- id: { type: 'string', description: 'Item ID returned by roadmap_add or roadmap_list' },
2101
- status: {
2102
- type: 'string',
2103
- enum: ['planned', 'in-progress', 'done', 'blocked', 'cancelled'],
2104
- description: 'New status for the item',
2105
- },
2106
- priority: {
2107
- type: 'string',
2108
- enum: ['critical', 'high', 'medium', 'low'],
2109
- description: 'Updated priority (optional)',
2110
- },
2111
- notes: { type: 'string', description: 'Progress notes, findings, or blockers (appended to existing notes)' },
2112
- title: { type: 'string', description: 'Updated title (optional)' },
2113
- description: { type: 'string', description: 'Updated description (optional)' },
2114
- },
2115
- required: ['instance_id', 'id'],
2116
- },
2117
- },
2118
- {
2119
- name: 'roadmap_list',
2120
- description:
2121
- 'List all roadmap items, optionally filtered by status, priority, tag, or milestone. ' +
2122
- 'Returns items sorted by priority then creation date. ' +
2123
- 'Called automatically by session_start to show open work.',
2124
- inputSchema: {
2125
- type: 'object',
2126
- properties: {
2127
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
2128
- status: {
2129
- type: 'string',
2130
- enum: ['planned', 'in-progress', 'done', 'blocked', 'cancelled', 'open'],
2131
- description: 'Filter by status. Use \'open\' to see planned+in-progress+blocked (default: open)',
2132
- },
2133
- tag: { type: 'string', description: 'Filter by tag (optional)' },
2134
- milestone: { type: 'string', description: 'Filter by milestone (optional)' },
2135
- priority: {
2136
- type: 'string',
2137
- enum: ['critical', 'high', 'medium', 'low'],
2138
- description: 'Filter by minimum priority (optional)',
2139
- },
2140
- },
2141
- required: ['instance_id'],
2142
- },
2143
- },
2144
- {
2145
- name: 'roadmap_next',
2146
- description:
2147
- 'Get the single most important next actionable roadmap item. ' +
2148
- 'Returns the highest-priority in-progress item first, then planned items, sorted by priority. ' +
2149
- 'Call at session start to immediately know what to work on next.',
2150
- inputSchema: {
2151
- type: 'object',
2152
- properties: {
2153
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
2154
- tag: { type: 'string', description: 'Filter by tag (optional)' },
2155
- },
2156
- required: ['instance_id'],
2157
- },
2158
- },
2159
1510
  {
2160
1511
  name: 'brain_doctor',
2161
1512
  description:
2162
1513
  'Check the health of your AI Brain and get actionable recommendations. ' +
2163
- 'Reports: lesson count, context entries, last session age, open failures, quality score, effective IQ boost, stale index. ' +
1514
+ 'Reports: lesson count, context entries, last session age, open failures, quality score. ' +
2164
1515
  'Returns a prioritized list of issues with fix instructions.',
2165
1516
  inputSchema: {
2166
1517
  type: 'object',
2167
1518
  properties: {
2168
1519
  instance_id: { type: 'string', description: 'UUID of the cache instance' },
2169
- workspace_path: {
2170
- type: 'string',
2171
- description: 'Absolute path to workspace root — enables package.json analysis for openclaw cross-promo (optional)',
2172
- },
2173
1520
  },
2174
1521
  required: ['instance_id'],
2175
1522
  },
@@ -2241,100 +1588,6 @@ const TOOLS = [
2241
1588
  required: ['instance_id', 'framework'],
2242
1589
  },
2243
1590
  },
2244
- // ── Brain Archaeology + Causal Chain ────────────────────────────────────
2245
- {
2246
- name: 'recall_at',
2247
- description:
2248
- 'Brain Archaeology — see what a lesson looked like at a specific point in time. ' +
2249
- '"What did we know about deployments 3 months ago?" ' +
2250
- 'Returns the history of a topic filtered to entries before the given date. ' +
2251
- 'Shows how the lesson evolved: failure → partial → success. ' +
2252
- 'Also useful to understand WHY old code decisions were made.',
2253
- inputSchema: {
2254
- type: 'object',
2255
- properties: {
2256
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
2257
- topic: { type: 'string', description: 'Topic slug to look up, e.g. "deploy:api"' },
2258
- date: { type: 'string', description: 'ISO date string (e.g. "2026-01-15") — returns entries stored BEFORE this date' },
2259
- },
2260
- required: ['instance_id', 'topic', 'date'],
2261
- },
2262
- },
2263
- {
2264
- name: 'trace_dependency',
2265
- description:
2266
- 'Causal Chain — find all lessons that depend on a given prerequisite. ' +
2267
- '"What lessons are affected if node version changes?" ' +
2268
- 'When a dependency changes (new version, different provider, new OS), call this to see which lessons need review. ' +
2269
- 'Lessons store dependencies via the depends_on field in learn_from_attempts.',
2270
- inputSchema: {
2271
- type: 'object',
2272
- properties: {
2273
- instance_id: { type: 'string', description: 'UUID of the cache instance' },
2274
- dependency: { type: 'string', description: 'Dependency to trace (e.g. "node:>=20", "docker:running", "wireguard:active")' },
2275
- mark_review: { type: 'boolean', description: 'If true, marks all dependent lessons as needs_review (default: false)' },
2276
- },
2277
- required: ['instance_id', 'dependency'],
2278
- },
2279
- },
2280
- // ── Team / Org Management ────────────────────────────────────────────────
2281
- {
2282
- name: 'list_orgs',
2283
- description:
2284
- 'List your Cachly organizations (team/org plans). ' +
2285
- 'Returns each org with plan, seat count, and member info. ' +
2286
- 'Org plans (Team €99, Business €299, Enterprise custom) are billed separately from cache tiers.',
2287
- inputSchema: {
2288
- type: 'object',
2289
- properties: {},
2290
- required: [],
2291
- },
2292
- },
2293
- {
2294
- name: 'create_org',
2295
- description:
2296
- 'Create a new Cachly organization for team collaboration. ' +
2297
- 'After creation, invite team members with invite_member and upgrade the plan via the billing portal. ' +
2298
- 'Org plans: Team (€99/mo, 10 seats), Business (€299/mo, 50 seats), Enterprise (custom).',
2299
- inputSchema: {
2300
- type: 'object',
2301
- properties: {
2302
- name: { type: 'string', description: 'Organization display name (e.g. "Acme Engineering")' },
2303
- slug: { type: 'string', description: 'URL-safe slug (e.g. "acme-eng"). Auto-generated from name if omitted.' },
2304
- },
2305
- required: ['name'],
2306
- },
2307
- },
2308
- {
2309
- name: 'invite_member',
2310
- description:
2311
- 'Invite a team member to a Cachly organization by email. ' +
2312
- 'They will receive an invite email and can join via the dashboard. ' +
2313
- 'Roles: owner (full access), admin (manage members + instances), member (read + cache ops).',
2314
- inputSchema: {
2315
- type: 'object',
2316
- properties: {
2317
- org_id: { type: 'string', description: 'UUID of the organization' },
2318
- email: { type: 'string', description: 'Email address to invite' },
2319
- role: { type: 'string', enum: ['admin', 'member'], description: 'Role for the invited member (default: member)' },
2320
- },
2321
- required: ['org_id', 'email'],
2322
- },
2323
- },
2324
- {
2325
- name: 'get_org_plan',
2326
- description:
2327
- 'Get the current org plan, seat usage, and billing info for an organization. ' +
2328
- 'Shows: plan name, price, seats used/max, next billing date. ' +
2329
- 'To upgrade: use the billing portal URL returned by this tool.',
2330
- inputSchema: {
2331
- type: 'object',
2332
- properties: {
2333
- org_id: { type: 'string', description: 'UUID of the organization' },
2334
- },
2335
- required: ['org_id'],
2336
- },
2337
- },
2338
1591
  // ── Legacy / Setup ────────────────────────────────────────────────────────
2339
1592
  {
2340
1593
  name: 'setup_ai_memory',
@@ -2432,50 +1685,10 @@ function buildConnectionString(inst: Instance): string {
2432
1685
  return `${scheme}://${pw}${inst.host}:${inst.port}`;
2433
1686
  }
2434
1687
 
2435
- // Fires once per process when no JWT is set (anonymous, opt-out via CACHLY_NO_TELEMETRY=1)
2436
- let _telemetryPingSent = false;
2437
- async function sendAnonymousTelemetry(toolName: string): Promise<void> {
2438
- if (_telemetryPingSent) return;
2439
- if (process.env.CACHLY_NO_TELEMETRY === '1') return;
2440
- _telemetryPingSent = true;
2441
- // Detect editor from common env vars injected by IDE extensions
2442
- const editor = process.env.CURSOR_TRACE_ID ? 'cursor'
2443
- : process.env.WINDSURF_SESSION_ID ? 'windsurf'
2444
- : process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
2445
- : process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
2446
- : 'unknown';
2447
- try {
2448
- await fetch(`${API_URL}/api/v1/telemetry/mcp`, {
2449
- method: 'POST',
2450
- headers: { 'Content-Type': 'application/json' },
2451
- body: JSON.stringify({ event: 'first_call_no_jwt', version: CURRENT_VERSION, editor, tool: toolName }),
2452
- signal: AbortSignal.timeout(3000),
2453
- });
2454
- } catch { /* fire-and-forget, never block the user */ }
2455
- }
2456
-
2457
1688
  async function handleTool(name: string, args: Record<string, unknown>): Promise<string> {
2458
- // Guard: if no JWT, return actionable onboarding message instead of HTTP 401
2459
- if (!JWT) {
2460
- void sendAnonymousTelemetry(name);
2461
- return [
2462
- '🧠 **cachly AI Brain — Setup required**',
2463
- '',
2464
- 'You have no API credentials configured. Get started in 30 seconds:',
2465
- '',
2466
- '**Step 1:** Register for free (no credit card):',
2467
- ' 👉 https://cachly.dev/setup-ai',
2468
- '',
2469
- '**Step 2:** Run the setup wizard in your terminal:',
2470
- ' ```',
2471
- ' npx @cachly-dev/mcp-server@latest setup',
2472
- ' ```',
2473
- '',
2474
- 'The wizard auto-detects your editor (Claude Code, Cursor, Copilot, Windsurf)',
2475
- 'and writes the config file with your credentials.',
2476
- '',
2477
- '✨ Free tier includes: 1 Brain instance, persistent memory, semantic search.',
2478
- ].join('\n');
1689
+ // Default instance_id from env so tools work even if AI omits it
1690
+ if (!args.instance_id && process.env.CACHLY_BRAIN_INSTANCE_ID) {
1691
+ args = { ...args, instance_id: process.env.CACHLY_BRAIN_INSTANCE_ID };
2479
1692
  }
2480
1693
 
2481
1694
  // Delegate v0.2 bulk/lock/stream tools first
@@ -2496,7 +1709,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2496
1709
  const { name: instName, tier } = args as { name: string; tier: string };
2497
1710
  const res = await apiFetch<CreateResponse>('/api/v1/instances', {
2498
1711
  method: 'POST',
2499
- body: JSON.stringify({ name: instName, tier, created_via: 'api' }),
1712
+ body: JSON.stringify({ name: instName, tier }),
2500
1713
  });
2501
1714
  if (res.checkout_url) {
2502
1715
  return [
@@ -2556,69 +1769,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2556
1769
  return `✅ Instance \`${instance_id}\` has been deleted and all data removed.`;
2557
1770
  }
2558
1771
 
2559
- // ── Org / Team management ────────────────────────────────────────────
2560
- case 'list_orgs': {
2561
- const res = await apiFetch<{ orgs: Array<{ id: string; name: string; slug: string; plan: string; max_members: number; member_count?: number }> }>('/api/v1/orgs');
2562
- const orgs = res.orgs ?? [];
2563
- if (orgs.length === 0) return `📭 No organizations yet.\n\nCreate one with \`create_org(name="My Team")\`.\nOrg plans: Team €99/mo (10 seats), Business €299/mo (50 seats), Enterprise custom.`;
2564
- return [
2565
- `🏢 **Your organizations (${orgs.length})**\n`,
2566
- ...orgs.map(o => `• **${o.name}** (\`${o.slug}\`) — plan: ${o.plan} · seats: ${o.member_count ?? '?'}/${o.max_members}\n ID: \`${o.id}\``),
2567
- `\n_Manage: \`get_org_plan\`, \`invite_member\`, dashboard → /team_`,
2568
- ].join('\n');
2569
- }
2570
-
2571
- case 'create_org': {
2572
- const { name: orgName, slug } = args as { name: string; slug?: string };
2573
- const body: Record<string, string> = { name: orgName };
2574
- if (slug) body.slug = slug;
2575
- const res = await apiFetch<{ id: string; name: string; slug: string; plan: string }>('/api/v1/orgs', {
2576
- method: 'POST',
2577
- body: JSON.stringify(body),
2578
- });
2579
- return [
2580
- `✅ **Organization created:** ${res.name}`,
2581
- ` ID: \`${res.id}\` · Slug: \`${res.slug}\` · Plan: ${res.plan}`,
2582
- ``,
2583
- `**Next steps:**`,
2584
- `1. Invite team members: \`invite_member(org_id="${res.id}", email="dev@example.com")\``,
2585
- `2. Upgrade plan: open billing portal via dashboard → /team`,
2586
- ` Team: €99/mo (10 seats) · Business: €299/mo (50 seats)`,
2587
- ].join('\n');
2588
- }
2589
-
2590
- case 'invite_member': {
2591
- const { org_id, email, role = 'member' } = args as { org_id: string; email: string; role?: string };
2592
- await apiFetch(`/api/v1/orgs/${org_id}/members`, {
2593
- method: 'POST',
2594
- body: JSON.stringify({ email, role }),
2595
- });
2596
- return `✅ Invite sent to **${email}** as \`${role}\` in org \`${org_id}\`.\n\nThey will receive an email to join the organization.`;
2597
- }
2598
-
2599
- case 'get_org_plan': {
2600
- const { org_id } = args as { org_id: string };
2601
- const org = await apiFetch<{
2602
- id: string; name: string; plan: string; max_members: number;
2603
- members: Array<{ role: string; invite_email: string; accepted_at?: string }>;
2604
- stripe_customer_id?: string;
2605
- }>(`/api/v1/orgs/${org_id}`);
2606
- const accepted = (org.members ?? []).filter(m => m.accepted_at).length;
2607
- const pending = (org.members ?? []).filter(m => !m.accepted_at).length;
2608
- const planPrice: Record<string, string> = { free: '€0', team: '€99/mo', business: '€299/mo', enterprise: 'custom' };
2609
- return [
2610
- `🏢 **${org.name}** — Plan: **${org.plan}** (${planPrice[org.plan] ?? org.plan})`,
2611
- ` Seats: ${accepted} active + ${pending} pending / ${org.max_members} max`,
2612
- ``,
2613
- `**Members:**`,
2614
- ...(org.members ?? []).map(m => ` • ${m.invite_email} (${m.role})${m.accepted_at ? '' : ' — pending'}`),
2615
- ``,
2616
- org.stripe_customer_id
2617
- ? `💳 Billing: managed via Stripe. Upgrade/cancel: dashboard → /billing`
2618
- : `💳 No payment method yet. Upgrade: dashboard → /billing → Team Plans`,
2619
- ].join('\n');
2620
- }
2621
-
2622
1772
  // ── Live cache operations ────────────────────────────────────────────
2623
1773
  case 'cache_get': {
2624
1774
  const { instance_id, key } = args as { instance_id: string; key: string };
@@ -3045,7 +2195,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3045
2195
  let skipped = 0;
3046
2196
  let errors = 0;
3047
2197
  let semanticIndexed = 0;
3048
- let unchanged = 0;
3049
2198
  const details: string[] = [];
3050
2199
  const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
3051
2200
  const redis = await getConnection(instance_id);
@@ -3053,31 +2202,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3053
2202
  for (const filePath of files) {
3054
2203
  const relPath = relative(dir, filePath);
3055
2204
  let content: string;
3056
- let fileSize: number;
3057
2205
  try {
3058
2206
  const s = await stat(filePath);
3059
2207
  if (s.size > 200_000) { skipped++; continue; } // skip files >200 KB
3060
- fileSize = s.size;
3061
2208
  content = await readFile(filePath, 'utf-8');
3062
2209
  } catch {
3063
2210
  errors++;
3064
2211
  continue;
3065
2212
  }
3066
2213
 
3067
- // ── Smart invalidation: hash-based change detection ──
3068
- // Compute a simple hash of file content to skip unchanged files
3069
- const hashKey = `cachly:idx:hash:${relPath}`;
3070
- const contentHash = `${fileSize}:${content.length}:${simpleHash(content)}`;
3071
- const existingHash = await redis.get(hashKey);
3072
- if (existingHash === contentHash) {
3073
- // File unchanged — refresh TTL but skip re-indexing
3074
- const idxKey = `cachly:idx:${relPath}`;
3075
- if (ttl > 0) await redis.expire(idxKey, ttl);
3076
- if (ttl > 0) await redis.expire(hashKey, ttl);
3077
- unchanged++;
3078
- continue;
3079
- }
3080
-
3081
2214
  const excerpt = content.slice(0, summary_chars).replace(/\s+/g, ' ').trim();
3082
2215
 
3083
2216
  // ── Layer 1: Keyword index in Valkey (always works, no embedding needed) ──
@@ -3088,12 +2221,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3088
2221
  } else {
3089
2222
  await redis.set(idxKey, idxValue);
3090
2223
  }
3091
- // Store content hash for smart invalidation on next run
3092
- if (ttl > 0) {
3093
- await redis.set(hashKey, contentHash, 'EX', ttl);
3094
- } else {
3095
- await redis.set(hashKey, contentHash);
3096
- }
3097
2224
  indexed++;
3098
2225
  details.push(` ✅ ${relPath}`);
3099
2226
 
@@ -3122,8 +2249,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3122
2249
  `📂 **index_project Complete** — ${mode}`,
3123
2250
  ``,
3124
2251
  ` 📁 Dir: ${dir}`,
3125
- ` ✅ Indexed: **${indexed}** files (new/changed)`,
3126
- ` ♻️ Unchanged: ${unchanged} files (hash match — skipped)`,
2252
+ ` ✅ Indexed: **${indexed}** files (keyword-searchable via smart_recall)`,
3127
2253
  ...(canEmbed ? [` 🎯 Semantic: **${semanticIndexed}** files (vector-searchable)`] : []),
3128
2254
  ` ⏭️ Skipped: ${skipped} (too large or filtered)`,
3129
2255
  ` ❌ Errors: ${errors}`,
@@ -3346,8 +2472,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3346
2472
  file_paths = [],
3347
2473
  commands = [],
3348
2474
  tags = [],
3349
- depends_on = [],
3350
- author = '',
3351
2475
  } = args as {
3352
2476
  instance_id: string;
3353
2477
  topic: string;
@@ -3359,107 +2483,26 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3359
2483
  file_paths?: string[];
3360
2484
  commands?: string[];
3361
2485
  tags?: string[];
3362
- depends_on?: string[];
3363
- author?: string;
3364
2486
  };
3365
2487
 
3366
2488
  const redis = await getConnection(instance_id);
3367
2489
  const ts = new Date().toISOString();
3368
2490
 
3369
- // ── Structured template hints ──────────────────────────────────────────
3370
- const category = topic.split(':')[0];
3371
- const template = STRUCTURED_TEMPLATES[category];
3372
- const templateWarnings: string[] = [];
3373
- if (template) {
3374
- for (const req of template.required) {
3375
- if (req === 'commands' && commands.length === 0) {
3376
- templateWarnings.push(`📋 ${template.hint}`);
3377
- }
3378
- }
3379
- }
3380
-
3381
- // ── Deduplication + audit trail ────────────────────────────────────────
2491
+ // ── Deduplication: check for existing lesson with same topic ─────────────
3382
2492
  let isUpdate = false;
3383
2493
  let recallCount = 0;
3384
- let auditTrail: Array<{ ts: string; action: string; prev_outcome?: string }> = [];
3385
- const existingRaw = await redis.get(`cachly:lesson:best:${topic}`);
3386
- if (existingRaw) {
3387
- try {
3388
- const prev = JSON.parse(existingRaw) as {
3389
- recall_count?: number;
3390
- outcome?: string;
3391
- audit_trail?: Array<{ ts: string; action: string; prev_outcome?: string }>;
3392
- };
3393
- recallCount = prev.recall_count ?? 0;
3394
- auditTrail = prev.audit_trail ?? [];
3395
- auditTrail.push({ ts, action: 'updated', prev_outcome: prev.outcome });
3396
- if (auditTrail.length > 20) auditTrail = auditTrail.slice(-20);
3397
- isUpdate = true;
3398
-
3399
- // ── Contradiction detection ─────────────────────────────────────────
3400
- const contradictionWarning: string[] = [];
3401
- if (prev.outcome === 'success' && outcome === 'failure') {
3402
- contradictionWarning.push(
3403
- `⚠️ **Contradiction detected!** Existing lesson has outcome: \`success\`, but you're storing \`failure\`.`,
3404
- `The existing "success" lesson will be preserved. Only the audit trail is updated.`,
3405
- `If you meant to mark this as failed permanently, store a new lesson with a distinct topic slug.`,
3406
- );
3407
- } else if (prev.outcome === 'failure' && outcome === 'success') {
3408
- contradictionWarning.push(
3409
- `✅ **Conflict resolved!** Previous lesson was \`failure\` — now overwriting with \`success\`.`,
3410
- );
3411
- }
3412
- if (contradictionWarning.length > 0) {
3413
- // Store contradiction audit but don't block
3414
- auditTrail[auditTrail.length - 1].action = 'contradiction-resolved';
3415
- }
3416
- } catch { /* ignore parse error */ }
3417
- } else {
3418
- auditTrail = [{ ts, action: 'created' }];
3419
- }
3420
-
3421
- // ── "I Was Wrong" Protocol — failure attribution ───────────────────────
3422
- const iWasWrongWarning: string[] = [];
3423
- if (outcome === 'failure') {
3424
- // Search for related success lessons that might have prevented this failure
3425
- const scanKeys: string[] = [];
3426
- const scanStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 100 });
3427
- await new Promise<void>((res, rej) => {
3428
- scanStream.on('data', (b: string[]) => scanKeys.push(...b));
3429
- scanStream.on('end', res);
3430
- scanStream.on('error', rej);
3431
- });
3432
- const topicWords = topic.split(/[:\-_]/).filter(w => w.length > 2);
3433
- for (const k of scanKeys.slice(0, 50)) {
3434
- const raw = await redis.get(k);
3435
- if (!raw) continue;
2494
+ if (outcome === 'success') {
2495
+ const existing = await redis.get(`cachly:lesson:best:${topic}`);
2496
+ if (existing) {
3436
2497
  try {
3437
- const l = JSON.parse(raw) as { outcome?: string; topic?: string; severity?: string };
3438
- if (l.outcome !== 'success') continue;
3439
- const lWords = (l.topic ?? '').split(/[:\-_]/).filter(w => w.length > 2);
3440
- const overlap = topicWords.filter(w => lWords.includes(w)).length;
3441
- if (overlap >= 1 && l.topic !== topic) {
3442
- iWasWrongWarning.push(
3443
- `⚠️ **"I Was Wrong"**: lesson \`${l.topic}\` (success, ${l.severity ?? 'major'}) might have prevented this failure.`,
3444
- ` → Use \`recall_best_solution(topic="${l.topic}")\` before next attempt.`,
3445
- ` → To mark it critical: \`learn_from_attempts(topic="${l.topic}", ..., severity="critical")\``,
3446
- );
3447
- break; // only show the most relevant match
3448
- }
3449
- } catch { /* skip */ }
2498
+ const prev = JSON.parse(existing) as { recall_count?: number };
2499
+ recallCount = (prev.recall_count ?? 0); // preserve existing recall count
2500
+ isUpdate = true;
2501
+ } catch { /* ignore parse error */ }
3450
2502
  }
3451
2503
  }
3452
2504
 
3453
- // ── Register dependency index for causal chain ─────────────────────────
3454
- for (const dep of depends_on) {
3455
- const depKey = `cachly:dep:${dep}`;
3456
- const existing = await redis.get(depKey);
3457
- const depTopics: string[] = existing ? JSON.parse(existing) : [];
3458
- if (!depTopics.includes(topic)) depTopics.push(topic);
3459
- await redis.set(depKey, JSON.stringify(depTopics));
3460
- }
3461
-
3462
- const lessonObj = {
2505
+ const lesson = JSON.stringify({
3463
2506
  topic,
3464
2507
  outcome,
3465
2508
  what_worked,
@@ -3469,36 +2512,20 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3469
2512
  file_paths,
3470
2513
  commands,
3471
2514
  tags,
3472
- depends_on,
3473
- ...(author ? { author } : {}),
3474
2515
  recall_count: recallCount,
3475
2516
  ts,
3476
- verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
3477
- confidence: 1.0,
3478
- audit_trail: auditTrail,
3479
- version: 3,
3480
- };
3481
- const lesson = JSON.stringify(lessonObj);
2517
+ version: 2,
2518
+ });
3482
2519
 
3483
- // Always append to the history list (audit log)
2520
+ // Always append to the history list
3484
2521
  const listKey = `cachly:lessons:${topic}`;
3485
2522
  await redis.rpush(listKey, lesson);
3486
2523
 
3487
- // Update best key for success/partial; for failure only update if no success exists
2524
+ // Update best key only for success/partial (not for failures)
3488
2525
  if (outcome === 'success' || outcome === 'partial') {
3489
2526
  await redis.set(`cachly:lesson:best:${topic}`, lesson);
3490
- } else if (!existingRaw) {
3491
- await redis.set(`cachly:lesson:best:${topic}`, lesson);
3492
2527
  }
3493
2528
 
3494
- // Track in decision log for session replay
3495
- try {
3496
- const dlKey = 'cachly:session:decision-log';
3497
- const dlEntry = JSON.stringify({ ts, topic, outcome, what_worked: what_worked.slice(0, 120) });
3498
- await redis.rpush(dlKey, dlEntry);
3499
- await redis.ltrim(dlKey, -50, -1);
3500
- } catch { /* non-critical */ }
3501
-
3502
2529
  const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
3503
2530
  const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
3504
2531
  const action = isUpdate ? 'updated' : 'stored';
@@ -3513,13 +2540,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3513
2540
  tags.length > 0 ? `**Tags:** ${tags.map(t => `#${t}`).join(' ')}` : '',
3514
2541
  ``,
3515
2542
  isUpdate
3516
- ? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
2543
+ ? `♻️ Updated existing lesson (preserved recall count: ${recallCount})`
3517
2544
  : `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
3518
- depends_on.length > 0
3519
- ? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
3520
- : '',
3521
- ...templateWarnings,
3522
- ...iWasWrongWarning,
3523
2545
  ].filter(l => l !== '').join('\n');
3524
2546
  }
3525
2547
 
@@ -3530,47 +2552,43 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3530
2552
  // Try exact best-solution key first
3531
2553
  const best = await redis.get(`cachly:lesson:best:${topic}`);
3532
2554
  if (best) {
3533
- const lesson = JSON.parse(best) as {
2555
+ let lesson: {
3534
2556
  topic: string; outcome: string; what_worked: string; what_failed?: string;
3535
- context?: string; ts: string; verified_at?: string; severity?: string;
3536
- file_paths?: string[]; commands?: string[]; tags?: string[];
3537
- recall_count?: number; audit_trail?: unknown[];
3538
- };
3539
-
3540
- // ── Confidence decay check ───────────────────────────────────────────
3541
- const confidence = calculateConfidence(lesson);
3542
- const ref = lesson.verified_at ?? lesson.ts;
3543
- const ageDays = (Date.now() - new Date(ref).getTime()) / 86400000;
3544
- const badge = confidenceBadge(confidence, ageDays);
3545
-
3546
- // Recall resets verified_at (confidence clock restart)
3547
- const updatedLesson = {
3548
- ...lesson,
3549
- recall_count: (lesson.recall_count ?? 0) + 1,
3550
- verified_at: new Date().toISOString(),
3551
- confidence: 1.0,
2557
+ context?: string; ts: string; severity?: string; file_paths?: string[];
2558
+ commands?: string[]; tags?: string[]; recall_count?: number;
3552
2559
  };
2560
+ try { lesson = JSON.parse(best); } catch {
2561
+ return `⚠️ Lesson for \`${topic}\` could not be read (corrupted data). Use \`learn_from_attempts\` to overwrite it.`;
2562
+ }
2563
+ // Increment recall_count
2564
+ const updatedLesson = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1 };
3553
2565
  await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
3554
2566
 
3555
- const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
3556
- const auditSummary = (lesson.audit_trail ?? []).length > 1
3557
- ? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
3558
- : '';
3559
-
3560
- // "Remember when..." — emotional header for lessons > 60 days old
3561
- const ageFromStoreDays = (Date.now() - new Date(lesson.ts).getTime()) / 86400000;
3562
- const rememberWhen = ageFromStoreDays > 60
3563
- ? `💭 _Remember when you solved this ${Math.round(ageFromStoreDays / 30)} months ago? Still works._`
3564
- : '';
3565
-
3566
- // "Never Google This Again" — suggest pinning after 3rd recall
3567
- const suggestPin = updatedLesson.recall_count === 3 && !(lesson as { pinned?: boolean }).pinned
3568
- ? `📌 **You've looked this up 3 times.** Consider pinning it for instant access: add \`pinned: true\` via \`learn_from_attempts\` to always surface it first.`
3569
- : '';
2567
+ // Fire-and-forget: track Magic Moment in Plausible + API engagement counter
2568
+ const isFirstRecall = (lesson.recall_count ?? 0) === 0;
2569
+ const recallEvent = isFirstRecall ? 'First Brain Recall' : 'Brain Recall Hit';
2570
+ fetch('https://analytics.cachly.dev/api/event', {
2571
+ method: 'POST',
2572
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'cachly-mcp/recall' },
2573
+ body: JSON.stringify({
2574
+ domain: 'cachly.dev', name: recallEvent,
2575
+ url: 'https://cachly.dev/mcp/recall', props: { topic },
2576
+ }),
2577
+ }).catch(() => { /* non-critical */ });
2578
+ if (JWT) {
2579
+ fetch(`${API_URL}/api/v1/telemetry/mcp`, {
2580
+ method: 'POST',
2581
+ headers: { 'Content-Type': 'application/json' },
2582
+ body: JSON.stringify({ event: isFirstRecall ? 'first_recall' : 'recall', api_key: JWT }),
2583
+ }).catch(() => { /* non-critical */ });
2584
+ }
3570
2585
 
2586
+ const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
2587
+ const sessionAgeMs = Date.now() - new Date(lesson.ts).getTime();
2588
+ const sessionAgeDays = Math.round(sessionAgeMs / 86_400_000);
2589
+ const ageLabel = sessionAgeDays === 0 ? 'today' : sessionAgeDays === 1 ? 'yesterday' : `${sessionAgeDays} days ago`;
3571
2590
  return [
3572
- rememberWhen,
3573
- `${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
2591
+ `✅ **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}× · stored ${ageLabel}`,
3574
2592
  ``,
3575
2593
  `**What worked:** ${lesson.what_worked}`,
3576
2594
  lesson.what_failed ? `**What failed (avoid this):** ${lesson.what_failed}` : '',
@@ -3578,8 +2596,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3578
2596
  (lesson.file_paths ?? []).length > 0 ? `**Files:** ${(lesson.file_paths ?? []).map((f: string) => `\`${f}\``).join(', ')}` : '',
3579
2597
  (lesson.commands ?? []).length > 0 ? `**Commands:** ${(lesson.commands ?? []).map((c: string) => `\`${c}\``).join(', ')}` : '',
3580
2598
  (lesson.tags ?? []).length > 0 ? `**Tags:** ${(lesson.tags ?? []).map((t: string) => `#${t}`).join(' ')}` : '',
3581
- auditSummary,
3582
- suggestPin,
2599
+ ``,
2600
+ `> 🧠 *Your AI remembered this from ${ageLabel} — persistent memory by [cachly.dev](https://cachly.dev)*`,
3583
2601
  ].filter(l => l !== '').join('\n');
3584
2602
  }
3585
2603
 
@@ -3598,7 +2616,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3598
2616
  const histKey = `cachly:lessons:${topic}`;
3599
2617
  const all = await redis.lrange(histKey, -3, -1);
3600
2618
  if (all.length > 0) {
3601
- const parsed = all.map(e => JSON.parse(e) as { outcome: string; what_worked: string; ts: string });
2619
+ const parsed = all.flatMap(e => { try { return [JSON.parse(e) as { outcome: string; what_worked: string; ts: string }]; } catch { return []; } });
3602
2620
  const lines = parsed.map(p => `- ${p.outcome === 'success' ? '✅' : '❌'} ${p.what_worked.slice(0, 120)} (${new Date(p.ts).toLocaleDateString('de-DE')})`);
3603
2621
  return `⚠️ No successful solution for \`${topic}\` yet. Last attempts:\n\n${lines.join('\n')}`;
3604
2622
  }
@@ -3610,12 +2628,28 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3610
2628
  for (const k of matching.slice(0, 5)) {
3611
2629
  const raw = await redis.get(k);
3612
2630
  if (!raw) continue;
3613
- const lesson = JSON.parse(raw) as { topic: string; what_worked: string; context?: string; ts: string };
2631
+ let lesson: { topic: string; what_worked: string; context?: string; ts: string };
2632
+ try { lesson = JSON.parse(raw); } catch { continue; }
3614
2633
  results.push(`**\`${lesson.topic}\`** — ${lesson.what_worked.slice(0, 200)}`);
3615
2634
  }
3616
2635
  return `🔍 **Partial matches for \`${topic}\`:**\n\n${results.join('\n\n')}`;
3617
2636
  }
3618
2637
 
2638
+ case 'forget_lesson': {
2639
+ const { instance_id, topic } = args as { instance_id: string; topic: string };
2640
+ const redis = await getConnection(instance_id);
2641
+ const bestKey = `cachly:lesson:best:${topic}`;
2642
+ const histKey = `cachly:lessons:${topic}`;
2643
+ const [delBest, delHist] = await Promise.all([
2644
+ redis.del(bestKey),
2645
+ redis.del(histKey),
2646
+ ]);
2647
+ if (delBest === 0 && delHist === 0) {
2648
+ return `📭 No lesson found for \`${topic}\` — nothing to delete.`;
2649
+ }
2650
+ return `🗑️ Lesson for \`${topic}\` deleted (best-solution: ${delBest > 0 ? 'removed' : 'not found'}, history: ${delHist > 0 ? 'removed' : 'not found'}). You can now store a corrected lesson with \`learn_from_attempts\`.`;
2651
+ }
2652
+
3619
2653
  case 'smart_recall': {
3620
2654
  const {
3621
2655
  instance_id,
@@ -3730,18 +2764,32 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3730
2764
 
3731
2765
  // ── get_api_status ────────────────────────────────────────────────────────
3732
2766
  case 'get_api_status': {
3733
- // Check health
2767
+ // Check API health and auth service in parallel
2768
+ const [healthResult, authServiceResult] = await Promise.allSettled([
2769
+ fetch(`${API_URL}/health`, { signal: AbortSignal.timeout(5000) }),
2770
+ fetch(`${AUTH_URL}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(5000) }),
2771
+ ]);
2772
+
3734
2773
  let healthStatus = 'unknown';
3735
- try {
3736
- const healthRes = await fetch(`${API_URL}/health`);
2774
+ if (healthResult.status === 'fulfilled') {
2775
+ const healthRes = healthResult.value;
3737
2776
  if (healthRes.ok) {
3738
- const body = await healthRes.json() as { status?: string; db?: string };
3739
- healthStatus = `${body.status ?? 'ok'} (db: ${body.db ?? '?'})`;
2777
+ const body = await healthRes.json().catch(() => ({})) as { status?: string; db?: string };
2778
+ healthStatus = `✅ ${body.status ?? 'ok'}${body.db ? ` (db: ${body.db})` : ''}`;
3740
2779
  } else {
3741
- healthStatus = `HTTP ${healthRes.status}`;
2780
+ healthStatus = `❌ HTTP ${healthRes.status}`;
3742
2781
  }
3743
- } catch (e) {
3744
- healthStatus = `unreachable: ${(e as Error).message}`;
2782
+ } else {
2783
+ healthStatus = `❌ unreachable: ${healthResult.reason?.message ?? 'timeout'}`;
2784
+ }
2785
+
2786
+ let authSvcStatus = 'unknown';
2787
+ if (authServiceResult.status === 'fulfilled') {
2788
+ authSvcStatus = authServiceResult.value.ok
2789
+ ? `✅ reachable (${AUTH_URL})`
2790
+ : `❌ HTTP ${authServiceResult.value.status} — auth service may be down`;
2791
+ } else {
2792
+ authSvcStatus = `❌ unreachable: ${authServiceResult.reason?.message ?? 'timeout'} — auth service may be down`;
3745
2793
  }
3746
2794
 
3747
2795
  // Check JWT / auth
@@ -3749,16 +2797,18 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3749
2797
  return [
3750
2798
  `📡 **cachly API Status**`,
3751
2799
  ``,
3752
- ` 🌐 API: ${API_URL}`,
3753
- ` 💓 Health: ${healthStatus}`,
3754
- ` 🔑 Auth: ❌ CACHLY_JWT not set`,
2800
+ ` 🌐 API: ${API_URL}`,
2801
+ ` 💓 API Health: ${healthStatus}`,
2802
+ ` 🔐 Auth Service: ${authSvcStatus}`,
2803
+ ` 🔑 JWT: ❌ CACHLY_JWT not set`,
3755
2804
  ``,
3756
- `💡 Get your API token at https://cachly.dev/instances → Settings → API Token`,
2805
+ `💡 Get your API token at https://cachly.dev/settings`,
3757
2806
  ].join('\n');
3758
2807
  }
3759
2808
 
3760
2809
  // Decode JWT claims (inspection only, no verification)
3761
2810
  let authInfo = '❌ invalid JWT format';
2811
+ let isExpired = false;
3762
2812
  try {
3763
2813
  const parts = JWT.split('.');
3764
2814
  if (parts.length === 3) {
@@ -3769,33 +2819,44 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3769
2819
  const iss = payload.iss ?? '(unknown)';
3770
2820
  const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
3771
2821
  const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
3772
- const expired = expTs ? expTs < new Date() : false;
2822
+ isExpired = expTs ? expTs < new Date() : false;
3773
2823
  authInfo = [
3774
- `✅ JWT decoded`,
2824
+ `${isExpired ? '⚠️ ' : '✅'} JWT decoded`,
3775
2825
  ` Sub (user ID): ${sub}`,
3776
2826
  ` Provider: ${provider}`,
3777
2827
  ` Issuer: ${iss}`,
3778
- ` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
2828
+ ` Expires: ${expTs ? expTs.toISOString() : 'never'} ${isExpired ? '⚠️ EXPIRED' : '✅'}`,
3779
2829
  ].join('\n');
3780
2830
  }
3781
2831
  } catch {
3782
2832
  authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
3783
2833
  }
3784
2834
 
3785
- return [
2835
+ const lines = [
3786
2836
  `📡 **cachly API Status**`,
3787
2837
  ``,
3788
- ` 🌐 API: ${API_URL}`,
3789
- ` 💓 Health: ${healthStatus}`,
2838
+ ` 🌐 API: ${API_URL}`,
2839
+ ` 💓 API Health: ${healthStatus}`,
2840
+ ` 🔐 Auth Service: ${authSvcStatus}`,
3790
2841
  ``,
3791
- `🔑 **Auth:**`,
2842
+ `🔑 **Auth (JWT):**`,
3792
2843
  authInfo,
3793
- ].join('\n');
2844
+ ];
2845
+
2846
+ if (isExpired) {
2847
+ lines.push(``, `⚠️ Your token is expired — get a new one at https://cachly.dev/settings`);
2848
+ }
2849
+ if (authSvcStatus.startsWith('❌')) {
2850
+ lines.push(``, `⚠️ Auth service appears down. Registration and login on cachly.dev may be unavailable.`);
2851
+ lines.push(` Contact support@cachly.dev if this persists.`);
2852
+ }
2853
+
2854
+ return lines.join('\n');
3794
2855
  }
3795
2856
 
3796
2857
  // ── session_start ─────────────────────────────────────────────────────────
3797
2858
  case 'session_start': {
3798
- const { instance_id, focus = '', author = '' } = args as { instance_id: string; focus?: string; author?: string };
2859
+ const { instance_id, focus = '' } = args as { instance_id: string; focus?: string };
3799
2860
  const redis = await getConnection(instance_id);
3800
2861
 
3801
2862
  // 1. Scan all best-solution lessons
@@ -3810,8 +2871,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3810
2871
  // 2. Fetch all lesson values for recency sorting + focus matching
3811
2872
  type Lesson = {
3812
2873
  topic: string; outcome: string; what_worked: string; what_failed?: string;
3813
- ts: string; verified_at?: string; severity?: string; recall_count?: number;
3814
- tags?: string[]; confidence?: number; audit_trail?: unknown[];
2874
+ ts: string; severity?: string; recall_count?: number; tags?: string[];
3815
2875
  };
3816
2876
  const lessons: Lesson[] = [];
3817
2877
  for (const k of lessonKeys) {
@@ -3850,43 +2910,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3850
2910
  )
3851
2911
  : [];
3852
2912
 
3853
- // 6. Streak tracking
3854
- let streakDays = 0;
3855
- let streakRecord = 0;
3856
- let streakMessage = '';
3857
- try {
3858
- const streakRaw = await redis.get('cachly:streak:current');
3859
- const streak = streakRaw ? JSON.parse(streakRaw) as { days: number; last_date: string; record: number } : null;
3860
- const today = new Date().toISOString().slice(0, 10);
3861
- if (streak) {
3862
- const lastDate = streak.last_date;
3863
- const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
3864
- if (lastDate === today) {
3865
- // Already counted today
3866
- streakDays = streak.days;
3867
- streakRecord = streak.record;
3868
- } else if (lastDate === yesterday) {
3869
- // Continuing streak
3870
- streakDays = streak.days + 1;
3871
- streakRecord = Math.max(streakDays, streak.record);
3872
- await redis.set('cachly:streak:current', JSON.stringify({ days: streakDays, last_date: today, record: streakRecord }));
3873
- } else {
3874
- // Streak broken
3875
- streakDays = 1;
3876
- streakRecord = streak.record;
3877
- await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: streakRecord }));
3878
- }
3879
- } else {
3880
- // First session ever
3881
- streakDays = 1;
3882
- streakRecord = 1;
3883
- await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: 1 }));
3884
- }
3885
- if (streakDays >= 7) streakMessage = `🔥 **${streakDays}-day streak!** ${streakDays === streakRecord ? ' New record!' : `Best: ${streakRecord}d`}`;
3886
- else if (streakDays > 1) streakMessage = `🔥 ${streakDays}-day streak`;
3887
- } catch { /* non-critical */ }
3888
-
3889
- // 7. Save session start marker
2913
+ // 6. Save session start marker
3890
2914
  await redis.set('cachly:session:current', JSON.stringify({
3891
2915
  started: new Date().toISOString(),
3892
2916
  focus,
@@ -3894,38 +2918,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3894
2918
 
3895
2919
  // ── Build briefing ──────────────────────────────────────────────────────
3896
2920
  const lines: string[] = ['🧠 **Session Briefing**', ''];
3897
- if (streakMessage) lines.push(streakMessage, '');
3898
-
3899
- // Handoff from previous window (if any)
3900
- const handoffRaw = await redis.get('cachly:session:handoff');
3901
- if (handoffRaw) {
3902
- try {
3903
- const handoff = JSON.parse(handoffRaw) as {
3904
- ts: string; completed_tasks: string[]; remaining_tasks: string[];
3905
- files_changed?: { path: string; status: string; description?: string }[];
3906
- instructions?: string; context_summary?: string; blocked_on?: string;
3907
- };
3908
- const ago = Math.round((Date.now() - new Date(handoff.ts).getTime()) / 60000);
3909
- const agoStr = ago < 60 ? `${ago}m ago` : ago < 1440 ? `${Math.round(ago / 60)}h ago` : `${Math.round(ago / 1440)}d ago`;
3910
-
3911
- lines.push(`🤝 **Handoff from previous window** (${agoStr}):`);
3912
- if (handoff.context_summary) lines.push(` ${handoff.context_summary}`);
3913
- if (handoff.remaining_tasks.length > 0) {
3914
- lines.push(` ⏳ **Remaining tasks:**`);
3915
- for (const t of handoff.remaining_tasks) lines.push(` - ${t}`);
3916
- }
3917
- if (handoff.completed_tasks.length > 0) {
3918
- lines.push(` ✅ **Already done:** ${handoff.completed_tasks.join(', ')}`);
3919
- }
3920
- const brokenFiles = (handoff.files_changed ?? []).filter(f => f.status === 'broken' || f.status === 'partial');
3921
- if (brokenFiles.length > 0) {
3922
- lines.push(` ⚠️ **Needs fix:** ${brokenFiles.map(f => `\`${f.path}\` (${f.status}${f.description ? ': ' + f.description : ''})`).join(', ')}`);
3923
- }
3924
- if (handoff.blocked_on) lines.push(` 🚫 **Blocked on:** ${handoff.blocked_on}`);
3925
- if (handoff.instructions) lines.push(` 📝 **Instructions:** ${handoff.instructions}`);
3926
- lines.push('');
3927
- } catch { /* ignore corrupt handoff */ }
3928
- }
3929
2921
 
3930
2922
  // Last session
3931
2923
  if (lastSession) {
@@ -3977,136 +2969,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3977
2969
  lines.push('');
3978
2970
  }
3979
2971
 
3980
- // ── Stale / low-confidence lessons (confidence decay) ─────────────────
3981
- const staleSuccessLessons = lessons.filter(l => {
3982
- if (l.outcome !== 'success' && l.outcome !== 'partial') return false;
3983
- return calculateConfidence(l) < CONFIDENCE_WARN_VALUE;
3984
- });
3985
- if (staleSuccessLessons.length > 0) {
3986
- lines.push(`🔴 **Stale lessons** (not recalled in >${CONFIDENCE_WARN_DAYS}d — verify before applying):`);
3987
- for (const l of staleSuccessLessons.slice(0, 4)) {
3988
- const conf = calculateConfidence(l);
3989
- const ageDays = Math.round((Date.now() - new Date(l.verified_at ?? l.ts).getTime()) / 86400000);
3990
- const flag = conf < CONFIDENCE_STALE_VALUE ? '🔴' : '⚠️';
3991
- lines.push(` ${flag} \`${l.topic}\` — ${ageDays}d stale, ${(conf * 100).toFixed(0)}% confidence`);
3992
- }
3993
- lines.push(` _Run \`recall_best_solution\` on these to reset their confidence clock._`);
3994
- lines.push('');
3995
- }
3996
-
3997
- // ── Session Replay: show last session's decision log ──────────────────
3998
- const lastSessionAny = lastSession as unknown as { decision_log?: Array<{ topic: string; outcome: string; what_worked: string }> } | null;
3999
- if (lastSessionAny?.decision_log?.length) {
4000
- const dl = lastSessionAny.decision_log;
4001
- const successes = dl.filter(d => d.outcome === 'success');
4002
- const failures = dl.filter(d => d.outcome === 'failure');
4003
- lines.push(`🎬 **Last session decisions** (${dl.length} lessons stored):`);
4004
- if (successes.length > 0) lines.push(` ✅ Worked: ${successes.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
4005
- if (failures.length > 0) lines.push(` ❌ Failed: ${failures.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
4006
- lines.push('');
4007
- }
4008
-
4009
- // ── 🔮 Predictive Pre-Warning — intent-based danger detection ────────────
4010
- // Fires BEFORE work starts. If focus area has known failure patterns → warn loudly.
4011
- if (focusTerms.length > 0) {
4012
- type LessonAny = typeof lessons[0] & { author?: string; tags?: string[] };
4013
- const dangerLessons = (lessons as LessonAny[]).filter(l => {
4014
- if (l.outcome === 'success') return false;
4015
- const topicCategory = l.topic.split(':')[0];
4016
- return focusTerms.some(term =>
4017
- l.topic.toLowerCase().includes(term) ||
4018
- topicCategory === term ||
4019
- (l.tags ?? []).some((t: string) => t.toLowerCase() === term),
4020
- );
4021
- });
4022
- if (dangerLessons.length >= 1) {
4023
- // Insert warning block right after the title line (index 1 = blank line after title)
4024
- const warning = [
4025
- `🚨 **PRE-WARNING** — Read this BEFORE starting:`,
4026
- ` Known pitfalls for **"${focus}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`,
4027
- ...dangerLessons.slice(0, 3).map(l => ` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`),
4028
- '',
4029
- ];
4030
- lines.splice(2, 0, ...warning); // after '🧠 **Session Briefing**' + empty line
4031
- }
4032
- }
4033
-
4034
- // ── 👥 Team Telepathy — what teammates learned this week ─────────────────
4035
- if (author) {
4036
- type LessonAny = typeof lessons[0] & { author?: string };
4037
- const oneWeekAgo = Date.now() - 7 * 86_400_000;
4038
- const teamLessons = (lessons as LessonAny[]).filter(l =>
4039
- l.author && l.author !== author && new Date(l.ts).getTime() > oneWeekAgo,
4040
- );
4041
- if (teamLessons.length > 0) {
4042
- // Group by author
4043
- const byAuthor = new Map<string, LessonAny[]>();
4044
- for (const l of teamLessons) {
4045
- const a = l.author!;
4046
- if (!byAuthor.has(a)) byAuthor.set(a, []);
4047
- byAuthor.get(a)!.push(l);
4048
- }
4049
- lines.push(`👥 **Team this week** (${teamLessons.length} lesson${teamLessons.length > 1 ? 's' : ''} from teammates):`);
4050
- for (const [teamAuthor, tls] of byAuthor) {
4051
- lines.push(` 👤 **${teamAuthor}**:`);
4052
- for (const l of tls.slice(0, 3)) {
4053
- const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
4054
- lines.push(` ${emoji} \`${l.topic}\` — ${l.what_worked.slice(0, 80)}`);
4055
- }
4056
- if (tls.length > 3) lines.push(` … and ${tls.length - 3} more`);
4057
- }
4058
- lines.push('');
4059
- }
4060
- }
4061
-
4062
- // ── 💎 Memory Crystal — compressed wisdom from old sessions ──────────────
4063
- try {
4064
- const crystalRaw = await redis.get('cachly:crystal:latest');
4065
- if (crystalRaw) {
4066
- const crystal = JSON.parse(crystalRaw) as {
4067
- label: string; ts: string; session_count: number;
4068
- top_patterns: Array<{ category: string; insight: string; count: number }>;
4069
- };
4070
- const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
4071
- if (crystalAge <= 90) {
4072
- lines.push(`💎 **Memory Crystal** (${crystal.label} · ${crystal.session_count} sessions compressed):`);
4073
- for (const p of crystal.top_patterns.slice(0, 3)) {
4074
- lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`);
4075
- }
4076
- lines.push('');
4077
- }
4078
- }
4079
- } catch { /* non-critical */ }
4080
-
4081
- // ── 🗺️ Roadmap — open items at session start ────────────────────────────
4082
- try {
4083
- const roadmapAll = await redis.hgetall(`cachly:roadmap:${instance_id}`);
4084
- if (roadmapAll && Object.keys(roadmapAll).length > 0) {
4085
- const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
4086
- const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
4087
- const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
4088
- const allItems = Object.values(roadmapAll).map(v => JSON.parse(v as string) as Record<string, unknown>);
4089
- const openItems = allItems
4090
- .filter(i => openStatuses.has(i.status as string))
4091
- .sort((a, b) => {
4092
- if (a.status === 'in-progress' && b.status !== 'in-progress') return -1;
4093
- if (b.status === 'in-progress' && a.status !== 'in-progress') return 1;
4094
- return (PRIORITY_ORDER[a.priority as string] ?? 99) - (PRIORITY_ORDER[b.priority as string] ?? 99);
4095
- });
4096
- const doneCount = allItems.filter(i => i.status === 'done').length;
4097
- if (openItems.length > 0) {
4098
- lines.push(`🗺️ **Roadmap** (${openItems.length} open · ${doneCount} done):`);
4099
- for (const it of openItems.slice(0, 5)) {
4100
- const statusIcon = it.status === 'in-progress' ? '⚡' : it.status === 'blocked' ? '🚫' : '📋';
4101
- lines.push(` ${statusIcon} ${PRIORITY_ICON[it.priority as string] ?? '⚪'} \`${it.id}\` **${it.title}**`);
4102
- }
4103
- if (openItems.length > 5) lines.push(` … and ${openItems.length - 5} more`);
4104
- lines.push(` _Use \`roadmap_next\` for the top priority item · \`roadmap_list\` for full view_`);
4105
- lines.push('');
4106
- }
4107
- }
4108
- } catch { /* non-critical */ }
4109
-
4110
2972
  return lines.join('\n');
4111
2973
  }
4112
2974
 
@@ -4117,13 +2979,11 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4117
2979
  summary,
4118
2980
  files_changed = [],
4119
2981
  lessons_learned,
4120
- workspace_path = '',
4121
2982
  } = args as {
4122
2983
  instance_id: string;
4123
2984
  summary: string;
4124
2985
  files_changed?: string[];
4125
2986
  lessons_learned?: number;
4126
- workspace_path?: string;
4127
2987
  };
4128
2988
 
4129
2989
  const redis = await getConnection(instance_id);
@@ -4139,21 +2999,12 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4139
2999
  } catch { /* ignore */ }
4140
3000
  }
4141
3001
 
4142
- // ── Session Replay: capture decision log ─────────────────────────────
4143
- let decisionLog: Array<{ ts: string; topic: string; outcome: string; what_worked: string }> = [];
4144
- try {
4145
- const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
4146
- decisionLog = dlEntries.map(e => JSON.parse(e) as { ts: string; topic: string; outcome: string; what_worked: string });
4147
- await redis.del('cachly:session:decision-log');
4148
- } catch { /* non-critical */ }
4149
-
4150
3002
  const sessionRecord = {
4151
3003
  ts: now.toISOString(),
4152
3004
  summary,
4153
3005
  files_changed,
4154
3006
  ...(lessons_learned !== undefined ? { lessons_learned } : {}),
4155
3007
  ...(durationMin !== undefined ? { duration_min: durationMin } : {}),
4156
- ...(decisionLog.length > 0 ? { decision_log: decisionLog } : {}),
4157
3008
  };
4158
3009
 
4159
3010
  // Save as "last session"
@@ -4167,127 +3018,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4167
3018
  // Clean up current session marker
4168
3019
  await redis.del('cachly:session:current');
4169
3020
 
4170
- // ── AUTO-LEARN from session summary (no manual call needed) ─────────────
4171
- // Parse the summary for actionable lessons and store them automatically.
4172
- const autoLearned: string[] = [];
4173
- try {
4174
- // Extract key sentences from the summary that contain action verbs
4175
- const actionVerbs = /\b(fixed|deployed|added|removed|refactored|migrated|updated|resolved|implemented|improved|optimized|configured|created|deleted|disabled|enabled|discovered|found|learned|debugged|patched|upgraded|installed|tested|built|rewrote|moved|renamed|split|merged|extracted)\b/i;
4176
- const sentences = summary
4177
- .split(/[.!\n]+/)
4178
- .map(s => s.trim())
4179
- .filter(s => s.length > 20 && actionVerbs.test(s));
4180
-
4181
- for (const sentence of sentences.slice(0, 6)) {
4182
- // Build a topic slug from the first meaningful words
4183
- const words = sentence.toLowerCase()
4184
- .replace(/[^a-z0-9\s]/g, ' ')
4185
- .split(/\s+/)
4186
- .filter(w => w.length > 3 && !['that', 'this', 'with', 'from', 'have', 'been', 'were', 'they', 'then', 'when', 'also', 'into', 'will', 'would', 'could', 'should'].includes(w));
4187
- const slug = words.slice(0, 4).join('-');
4188
- if (!slug) continue;
4189
- const topic = `auto:${slug}`;
4190
- const key = `cachly:lesson:best:${topic}`;
4191
-
4192
- // Don't overwrite existing successful lessons
4193
- const existing = await redis.get(key);
4194
- if (existing) {
4195
- try {
4196
- const ex = JSON.parse(existing) as { outcome: string };
4197
- if (ex.outcome === 'success') continue;
4198
- } catch { /* ignore */ }
4199
- }
4200
-
4201
- const lesson = {
4202
- topic,
4203
- outcome: 'success',
4204
- what_worked: sentence,
4205
- context: `Auto-learned from session summary. Full summary: ${summary.slice(0, 300)}`,
4206
- severity: 'minor',
4207
- ts: now.toISOString(),
4208
- recall_count: 0,
4209
- auto_learned: true,
4210
- session_ts: now.toISOString(),
4211
- version: 2,
4212
- };
4213
- await redis.set(key, JSON.stringify(lesson));
4214
- // 90-day TTL for auto-learned lessons
4215
- await redis.expire(key, 90 * 86400);
4216
- autoLearned.push(topic);
4217
- }
4218
-
4219
- // Also store a lesson per changed file area if files were changed
4220
- if (files_changed.length > 0) {
4221
- const areas = [...new Set(files_changed.map(f => f.split('/').slice(0, 2).join('/')))].slice(0, 3);
4222
- for (const area of areas) {
4223
- const slug = area.replace(/[^a-z0-9]/gi, '-').toLowerCase().replace(/-+/g, '-').slice(0, 30);
4224
- const topic = `auto:changed:${slug}`;
4225
- const key = `cachly:lesson:best:${topic}`;
4226
- const lesson = {
4227
- topic,
4228
- outcome: 'success',
4229
- what_worked: `Files changed in ${area}: ${files_changed.filter(f => f.startsWith(area.split('/')[0])).slice(0, 5).join(', ')}`,
4230
- context: summary.slice(0, 200),
4231
- severity: 'minor',
4232
- ts: now.toISOString(),
4233
- recall_count: 0,
4234
- auto_learned: true,
4235
- version: 2,
4236
- };
4237
- await redis.set(key, JSON.stringify(lesson));
4238
- await redis.expire(key, 90 * 86400);
4239
- autoLearned.push(topic);
4240
- }
4241
- }
4242
- } catch { /* auto-learn errors must never break session_end */ }
4243
-
4244
- // ── 🌿 Ambient Git Learning ────────────────────────────────────────────────
4245
- // Read git commits since session start → auto-learn each meaningful commit.
4246
- const ambientLearned: string[] = [];
4247
- if (workspace_path) {
4248
- try {
4249
- // Get the session start time (stored by session_start)
4250
- const sessionStartTs = currentRaw
4251
- ? (() => { try { return (JSON.parse(currentRaw) as { started?: string }).started ?? ''; } catch { return ''; } })()
4252
- : '';
4253
- const sinceArg = sessionStartTs ? `--since="${sessionStartTs}"` : '--since="1 hour ago"';
4254
- const gitOut = execSync(
4255
- `git -C "${workspace_path}" log ${sinceArg} --oneline --format="%H|||%s|||%ai"`,
4256
- { timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
4257
- ).trim();
4258
- if (gitOut) {
4259
- const commitActionRe = /\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|build|rewrite|deploy|feat|chore|docs|test|perf|ci)\b/i;
4260
- for (const line of gitOut.split('\n').slice(0, 10)) {
4261
- const [hash, msg, dateStr] = line.split('|||');
4262
- if (!msg || !commitActionRe.test(msg)) continue;
4263
- const slug = msg
4264
- .toLowerCase().replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert)[:(\s]/i, '')
4265
- .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
4266
- if (!slug) continue;
4267
- const topic = `git:${slug}`;
4268
- const key = `cachly:lesson:best:${topic}`;
4269
- const existing = await redis.get(key);
4270
- if (existing) continue; // don't overwrite existing
4271
- const commitLesson = {
4272
- topic,
4273
- outcome: 'success' as const,
4274
- what_worked: msg.slice(0, 200),
4275
- context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)} at ${dateStr ?? ''} in ${workspace_path}`,
4276
- severity: 'minor' as const,
4277
- ts: now.toISOString(),
4278
- recall_count: 0,
4279
- auto_learned: true,
4280
- source: 'ambient-git',
4281
- version: 3,
4282
- };
4283
- await redis.set(key, JSON.stringify(commitLesson));
4284
- await redis.expire(key, 60 * 86400); // 60 day TTL for git lessons
4285
- ambientLearned.push(topic);
4286
- }
4287
- }
4288
- } catch { /* git not available or not a repo — silent skip */ }
4289
- }
4290
-
4291
3021
  const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
4292
3022
  return [
4293
3023
  `✅ **Session saved**${durationStr}`,
@@ -4295,70 +3025,11 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4295
3025
  `📋 **Summary:** ${summary}`,
4296
3026
  files_changed.length > 0 ? `📁 **Files changed:** ${files_changed.map(f => `\`${f}\``).join(', ')}` : '',
4297
3027
  lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
4298
- autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
4299
- ambientLearned.length > 0 ? `🌿 **Ambient git learning:** ${ambientLearned.length} commit${ambientLearned.length > 1 ? 's' : ''} auto-learned (${ambientLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${ambientLearned.length > 3 ? '…' : ''})` : '',
4300
3028
  ``,
4301
3029
  `💡 Next session: \`session_start(focus="...")\` to see this summary.`,
4302
3030
  ].filter(l => l !== '').join('\n');
4303
3031
  }
4304
3032
 
4305
- // ── session_handoff — cross-window continuity ─────────────────────────────
4306
- case 'session_handoff': {
4307
- const {
4308
- instance_id,
4309
- completed_tasks = [],
4310
- remaining_tasks = [],
4311
- files_changed = [],
4312
- instructions = '',
4313
- context_summary = '',
4314
- blocked_on = '',
4315
- } = args as {
4316
- instance_id: string;
4317
- completed_tasks: string[];
4318
- remaining_tasks: string[];
4319
- files_changed?: { path: string; status: string; description?: string }[];
4320
- instructions?: string;
4321
- context_summary?: string;
4322
- blocked_on?: string;
4323
- };
4324
-
4325
- const redis = await getConnection(instance_id);
4326
- const now = new Date();
4327
-
4328
- const handoff = {
4329
- ts: now.toISOString(),
4330
- completed_tasks,
4331
- remaining_tasks,
4332
- files_changed,
4333
- instructions,
4334
- context_summary,
4335
- blocked_on,
4336
- };
4337
-
4338
- // Store handoff — never expires until next handoff overwrites it
4339
- await redis.set('cachly:session:handoff', JSON.stringify(handoff));
4340
-
4341
- // Also append to history
4342
- await redis.lpush('cachly:session:handoff:history', JSON.stringify(handoff));
4343
- await redis.ltrim('cachly:session:handoff:history', 0, 19);
4344
-
4345
- const totalTasks = completed_tasks.length + remaining_tasks.length;
4346
- const pct = totalTasks > 0 ? Math.round((completed_tasks.length / totalTasks) * 100) : 0;
4347
- const brokenFiles = files_changed.filter(f => f.status === 'broken' || f.status === 'partial');
4348
-
4349
- return [
4350
- `🤝 **Handoff saved** — ${completed_tasks.length}/${totalTasks} tasks done (${pct}%)`,
4351
- ``,
4352
- completed_tasks.length > 0 ? `✅ **Completed:**\n${completed_tasks.map(t => ` - ${t}`).join('\n')}` : '',
4353
- remaining_tasks.length > 0 ? `\n⏳ **Remaining for next window:**\n${remaining_tasks.map(t => ` - ${t}`).join('\n')}` : '',
4354
- brokenFiles.length > 0 ? `\n⚠️ **Needs attention:** ${brokenFiles.map(f => `\`${f.path}\` (${f.status})`).join(', ')}` : '',
4355
- blocked_on ? `\n🚫 **Blocked on:** ${blocked_on}` : '',
4356
- instructions ? `\n📝 **Instructions:** ${instructions}` : '',
4357
- ``,
4358
- `💡 The next \`session_start\` will include this handoff automatically.`,
4359
- ].filter(l => l !== '').join('\n');
4360
- }
4361
-
4362
3033
  // ── auto_learn_session ────────────────────────────────────────────────────
4363
3034
  case 'auto_learn_session': {
4364
3035
  const { instance_id, observations } = args as {
@@ -4586,153 +3257,9 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4586
3257
  return lines.join('\n');
4587
3258
  }
4588
3259
 
4589
- // ── team_synthesize — Team Brain Synthesis ────────────────────────────────
4590
- case 'team_synthesize': {
4591
- const { instance_id, topic } = args as { instance_id: string; topic: string };
4592
- const redis = await getConnection(instance_id);
4593
-
4594
- // Load history list for this topic (all authors' contributions)
4595
- const listKey = `cachly:lessons:${topic}`;
4596
- const all = await redis.lrange(listKey, 0, -1);
4597
- if (all.length < 2) {
4598
- 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}", ...)\`.`;
4599
- }
4600
-
4601
- type Entry = { outcome: string; what_worked: string; what_failed?: string; author?: string; ts: string; severity?: string };
4602
- const entries: Entry[] = all.map(r => { try { return JSON.parse(r) as Entry; } catch { return null; } }).filter((e): e is Entry => e !== null);
4603
-
4604
- // Group by outcome
4605
- const successes = entries.filter(e => e.outcome === 'success');
4606
- const failures = entries.filter(e => e.outcome === 'failure');
4607
- const partials = entries.filter(e => e.outcome === 'partial');
4608
-
4609
- const authors = [...new Set(entries.map(e => e.author).filter(Boolean))];
4610
- const hasMultiAuthor = authors.length > 1;
4611
-
4612
- // Build canonical merged version
4613
- // what_worked: pick the most recent success, or longest for most detail
4614
- const bestSuccess = successes.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())[0];
4615
- const whatWorkedCandidates = successes.map(e => e.what_worked).filter(w => w && w.length > 10);
4616
- const canonicalWorked = whatWorkedCandidates.sort((a, b) => b.length - a.length)[0] ?? bestSuccess?.what_worked ?? '';
4617
-
4618
- // what_failed: union of all unique failure reasons
4619
- const allFailed = [...new Set(
4620
- [...failures, ...partials].map(e => e.what_failed).filter((w): w is string => !!w && w.length > 5)
4621
- )];
4622
-
4623
- const severities = entries.map(e => e.severity).filter(Boolean);
4624
- const canonicalSeverity = severities.includes('critical') ? 'critical' : severities.includes('major') ? 'major' : 'minor';
4625
-
4626
- const lines = [
4627
- `🧬 **Team Brain Synthesis: \`${topic}\`**`,
4628
- `_${entries.length} entries from ${authors.length} author${authors.length === 1 ? '' : 's'}${hasMultiAuthor ? ` (${authors.join(', ')})` : ''} · ${successes.length} success · ${failures.length} failure · ${partials.length} partial_`,
4629
- '',
4630
- `**Canonical "what worked":**`,
4631
- `> ${canonicalWorked}`,
4632
- '',
4633
- allFailed.length > 0 ? `**Avoid (combined failures):**` : '',
4634
- ...allFailed.map(f => `> ❌ ${f}`),
4635
- allFailed.length > 0 ? '' : '',
4636
- `**Suggested canonical lesson:**`,
4637
- '```',
4638
- `learn_from_attempts(`,
4639
- ` topic = "${topic}",`,
4640
- ` outcome = "success",`,
4641
- ` what_worked = "${canonicalWorked.replace(/"/g, "'")}",`,
4642
- allFailed.length > 0 ? ` what_failed = "${allFailed[0].replace(/"/g, "'")}",` : '',
4643
- ` severity = "${canonicalSeverity}",`,
4644
- `)`,
4645
- '```',
4646
- '',
4647
- hasMultiAuthor
4648
- ? `💡 _${authors.length} team members contributed to this synthesis. Store the canonical version to replace individual entries._`
4649
- : `💡 _Single author — more value when multiple team members contribute to the same topic._`,
4650
- ].filter(l => l !== undefined).join('\n');
4651
- return lines;
4652
- }
4653
-
4654
3260
  // ── brain_doctor ──────────────────────────────────────────────────────────
4655
- // ── memory_crystalize ─────────────────────────────────────────────────────
4656
- case 'memory_crystalize': {
4657
- const { instance_id, label: crystalLabel = '' } = args as { instance_id: string; label?: string };
4658
- const redis = await getConnection(instance_id);
4659
- const now = new Date();
4660
- const week = `${now.getFullYear()}-W${String(Math.ceil((now.getDate() - now.getDay() + 10) / 7)).padStart(2, '0')}`;
4661
- const effectiveLabel = crystalLabel || `${now.toISOString().slice(0, 7)} Crystal`;
4662
-
4663
- // Read session history
4664
- const sessionHistory = await redis.lrange('cachly:session:history', 0, 49);
4665
-
4666
- // Read all auto-learned lessons
4667
- const allLessonKeys: string[] = [];
4668
- const ls = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
4669
- await new Promise<void>((res, rej) => {
4670
- ls.on('data', (b: string[]) => allLessonKeys.push(...b));
4671
- ls.on('end', res);
4672
- ls.on('error', rej);
4673
- });
4674
-
4675
- type RawLesson = { topic: string; outcome: string; what_worked: string; severity?: string; ts: string; auto_learned?: boolean };
4676
- const allLessons: RawLesson[] = [];
4677
- for (const k of allLessonKeys) {
4678
- const raw = await redis.get(k);
4679
- if (!raw) continue;
4680
- try { allLessons.push(JSON.parse(raw) as RawLesson); } catch { /* skip */ }
4681
- }
4682
-
4683
- // Group lessons by top-level category
4684
- const categoryMap = new Map<string, RawLesson[]>();
4685
- for (const l of allLessons) {
4686
- const cat = l.topic.split(':')[0] || 'misc';
4687
- if (!categoryMap.has(cat)) categoryMap.set(cat, []);
4688
- categoryMap.get(cat)!.push(l);
4689
- }
4690
-
4691
- // Build top patterns (most frequent categories with a representative insight)
4692
- const topPatterns: Array<{ category: string; insight: string; count: number }> = [];
4693
- for (const [cat, catLessons] of [...categoryMap.entries()].sort((a, b) => b[1].length - a[1].length).slice(0, 8)) {
4694
- const successLessons = catLessons.filter(l => l.outcome === 'success');
4695
- const best = successLessons[0] ?? catLessons[0];
4696
- if (!best) continue;
4697
- topPatterns.push({
4698
- category: cat,
4699
- insight: best.what_worked.slice(0, 120),
4700
- count: catLessons.length,
4701
- });
4702
- }
4703
-
4704
- const crystal = {
4705
- label: effectiveLabel,
4706
- ts: now.toISOString(),
4707
- session_count: sessionHistory.length,
4708
- lesson_count: allLessons.length,
4709
- top_patterns: topPatterns,
4710
- categories: [...categoryMap.keys()],
4711
- created_from: `${sessionHistory.length} sessions, ${allLessons.length} lessons`,
4712
- };
4713
-
4714
- const crystalJson = JSON.stringify(crystal);
4715
- await redis.set('cachly:crystal:latest', crystalJson);
4716
- await redis.expire('cachly:crystal:latest', 90 * 86400);
4717
- await redis.set(`cachly:crystal:${week}`, crystalJson);
4718
- await redis.expire(`cachly:crystal:${week}`, 365 * 86400);
4719
-
4720
- const lines = [
4721
- `💎 **Memory Crystal created: ${effectiveLabel}**`,
4722
- ``,
4723
- `📊 Compressed: **${sessionHistory.length} sessions** + **${allLessons.length} lessons** → ${topPatterns.length} top patterns`,
4724
- ``,
4725
- `**Top patterns by category:**`,
4726
- ...topPatterns.slice(0, 6).map(p => ` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`),
4727
- ``,
4728
- `💡 This crystal will appear in every future \`session_start\` briefing.`,
4729
- `💡 Re-run \`memory_crystalize\` monthly to keep it fresh.`,
4730
- ];
4731
- return lines.join('\n');
4732
- }
4733
-
4734
3261
  case 'brain_doctor': {
4735
- const { instance_id, workspace_path: drWorkspacePath = '' } = args as { instance_id: string; workspace_path?: string };
3262
+ const { instance_id } = args as { instance_id: string };
4736
3263
  const redis = await getConnection(instance_id);
4737
3264
  const issues: string[] = [];
4738
3265
  const checks: string[] = [];
@@ -4758,15 +3285,12 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4758
3285
  });
4759
3286
 
4760
3287
  // Load lessons for analysis
4761
- type DrLesson = {
4762
- topic: string; outcome: string; recall_count?: number; ts: string;
4763
- verified_at?: string; severity?: string; audit_trail?: unknown[];
4764
- };
4765
- const lessons: DrLesson[] = [];
3288
+ type Lesson = { topic: string; outcome: string; recall_count?: number; ts: string; severity?: string };
3289
+ const lessons: Lesson[] = [];
4766
3290
  for (const k of lessonKeys) {
4767
3291
  const raw = await redis.get(k);
4768
3292
  if (!raw) continue;
4769
- try { lessons.push(JSON.parse(raw) as DrLesson); } catch { /* skip */ }
3293
+ try { lessons.push(JSON.parse(raw) as Lesson); } catch { /* skip */ }
4770
3294
  }
4771
3295
 
4772
3296
  // Last session
@@ -4782,17 +3306,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4782
3306
  const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
4783
3307
  // Critical lessons
4784
3308
  const criticalLessons = lessons.filter(l => l.severity === 'critical');
4785
- // Confidence decay analysis
4786
- const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
4787
- const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
4788
- const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
4789
- // Team lessons
4790
- type DrLessonWithAuthor = DrLesson & { author?: string };
4791
- const teamLessons = (lessons as DrLessonWithAuthor[]).filter(l => l.author);
4792
- const uniqueAuthors = new Set((lessons as DrLessonWithAuthor[]).map(l => l.author).filter(Boolean));
4793
- // Effective IQ boost: total recalls / lessons (how much the brain actually helped)
4794
- const totalRecalls = lessons.reduce((sum, l) => sum + (l.recall_count ?? 0), 0);
4795
- const iqBoostPct = lessons.length > 0 ? Math.min(100, Math.round((totalRecalls / lessons.length) * 10)) : 0;
4796
3309
 
4797
3310
  // Quality score (0-100)
4798
3311
  let score = 50;
@@ -4804,67 +3317,12 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4804
3317
  if (openFailures.length === 0) score += 5;
4805
3318
  const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
4806
3319
  if (unusedRatio < 0.5) score += 10;
4807
- if (staleLessons.length === 0) score += 5;
4808
- if (uniqueAuthors.size >= 2) score += 5; // team collaboration bonus
4809
3320
 
4810
3321
  const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
4811
- const iqEmoji = iqBoostPct >= 50 ? '🚀' : iqBoostPct >= 20 ? '📈' : '💤';
4812
3322
 
4813
3323
  checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
4814
- checks.push(`${iqEmoji} **Effective IQ Boost: ${iqBoostPct}%** (${totalRecalls} recalls across ${lessons.length} lessons)`);
4815
- checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail · ${teamLessons.length} from team)`);
3324
+ checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical)`);
4816
3325
  checks.push(`💾 **Context entries:** ${ctxCount}`);
4817
- checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
4818
- checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
4819
- if (uniqueAuthors.size >= 2) {
4820
- checks.push(`👥 **Team:** ${uniqueAuthors.size} contributors (${[...uniqueAuthors].join(', ')})`);
4821
- }
4822
-
4823
- // Stale index detection
4824
- try {
4825
- const lastIndexRaw = await redis.get('cachly:index:last_run');
4826
- if (lastIndexRaw) {
4827
- const lastIndexAge = Math.round((Date.now() - new Date(lastIndexRaw).getTime()) / 86_400_000);
4828
- if (lastIndexAge > 7) {
4829
- issues.push(`🔄 Index is ${lastIndexAge}d stale — run \`index_project\` to re-sync semantic search`);
4830
- } else {
4831
- checks.push(`🗂️ **Semantic index:** ${lastIndexAge}d old (fresh)`);
4832
- }
4833
- } else {
4834
- issues.push(`💡 No semantic index — run \`index_project(dir="<your-src>")\` to enable semantic search`);
4835
- }
4836
- } catch { /* non-critical */ }
4837
-
4838
- // Memory crystal status
4839
- try {
4840
- const crystalRaw = await redis.get('cachly:crystal:latest');
4841
- if (crystalRaw) {
4842
- const crystal = JSON.parse(crystalRaw) as { ts: string; label: string };
4843
- const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
4844
- checks.push(`💎 **Memory Crystal:** ${crystal.label} (${crystalAge}d ago)`);
4845
- if (crystalAge > 30) issues.push(`💡 Memory Crystal is ${crystalAge}d old — re-run \`memory_crystalize\` to compress new sessions`);
4846
- } else if (lessonKeys.length >= 10) {
4847
- issues.push(`💡 ${lessonKeys.length} lessons but no Memory Crystal — run \`memory_crystalize\` to compress wisdom`);
4848
- }
4849
- } catch { /* non-critical */ }
4850
-
4851
- // openclaw cross-promo (check package.json in workspace)
4852
- if (drWorkspacePath) {
4853
- try {
4854
- const pkgPath = drWorkspacePath.replace(/\/$/, '') + '/package.json';
4855
- const pkgRaw = readFileSync(pkgPath, 'utf-8');
4856
- const pkg = JSON.parse(pkgRaw) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
4857
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4858
- const hasLLMDep = ['openai', '@anthropic-ai/sdk', '@google/generative-ai', 'mistralai', 'cohere-ai'].some(d => d in allDeps);
4859
- const hasOpenclaw = '@cachly-dev/openclaw' in allDeps;
4860
- if (hasLLMDep && !hasOpenclaw) {
4861
- 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\``);
4862
- issues.push(` → \`npm install @cachly-dev/openclaw\` cuts LLM costs 60–90% with 3 lines of code`);
4863
- } else if (hasOpenclaw) {
4864
- checks.push(`✅ **@cachly-dev/openclaw installed** (LLM cost caching active)`);
4865
- }
4866
- } catch { /* no package.json or unreadable */ }
4867
- }
4868
3326
 
4869
3327
  if (lastSession) {
4870
3328
  const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
@@ -4880,10 +3338,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4880
3338
  issues.push(`💡 Only ${lessonKeys.length} lessons — add more after each problem solved`);
4881
3339
  }
4882
3340
 
4883
- if (iqBoostPct === 0 && lessons.length >= 5) {
4884
- issues.push(`💤 **IQ Boost is 0%** — lessons exist but are never recalled. Use \`recall_best_solution\` BEFORE tasks.`);
4885
- }
4886
-
4887
3341
  if (ctxCount === 0) {
4888
3342
  issues.push('💡 No context — use `remember_context` to cache architecture docs, ADRs, etc.');
4889
3343
  }
@@ -4892,15 +3346,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4892
3346
  issues.push(`⚠️ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
4893
3347
  }
4894
3348
 
4895
- if (staleLessons.length > 0) {
4896
- 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(', ')}`);
4897
- issues.push(` → Re-verify with \`recall_best_solution\` to reset confidence clock`);
4898
- }
4899
-
4900
- if (warnLessons.length > 0) {
4901
- issues.push(`⚠️ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
4902
- }
4903
-
4904
3349
  if (unusedRatio > 0.7 && lessons.length > 5) {
4905
3350
  issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
4906
3351
  }
@@ -4916,77 +3361,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4916
3361
  return lines.join('\n');
4917
3362
  }
4918
3363
 
4919
- // ── recall_at — Brain Archaeology ────────────────────────────────────────
4920
- case 'recall_at': {
4921
- const { instance_id, topic, date } = args as { instance_id: string; topic: string; date: string };
4922
- const redis = await getConnection(instance_id);
4923
- const cutoff = new Date(date).getTime();
4924
- if (isNaN(cutoff)) return `❌ Invalid date "${date}". Use ISO format: "2026-01-15"`;
4925
-
4926
- const listKey = `cachly:lessons:${topic}`;
4927
- const all = await redis.lrange(listKey, 0, -1);
4928
- if (all.length === 0) return `📭 No history found for \`${topic}\`. Lessons are stored via \`learn_from_attempts\`.`;
4929
-
4930
- const before = all
4931
- .map(raw => { try { return JSON.parse(raw) as { ts: string; outcome?: string; what_worked?: string; what_failed?: string }; } catch { return null; } })
4932
- .filter((l): l is NonNullable<typeof l> => l !== null && new Date(l.ts).getTime() <= cutoff)
4933
- .sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
4934
-
4935
- if (before.length === 0) return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${new Date(JSON.parse(all[0]).ts).toLocaleDateString('de-DE')}.`;
4936
-
4937
- const lines = [
4938
- `🏺 **Brain Archaeology: \`${topic}\` before ${date}**`,
4939
- `_${before.length} of ${all.length} total entries shown_`,
4940
- '',
4941
- ];
4942
- for (const l of before.slice(-10)) {
4943
- const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
4944
- const d = new Date(l.ts).toLocaleDateString('de-DE');
4945
- lines.push(`**${d}** ${emoji} ${l.outcome}`);
4946
- if (l.what_worked) lines.push(` → ${l.what_worked.slice(0, 100)}`);
4947
- lines.push('');
4948
- }
4949
- 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(' → ')}_`);
4950
- return lines.join('\n');
4951
- }
4952
-
4953
- // ── trace_dependency — Causal Chain ──────────────────────────────────────
4954
- case 'trace_dependency': {
4955
- const { instance_id, dependency, mark_review = false } = args as { instance_id: string; dependency: string; mark_review?: boolean };
4956
- const redis = await getConnection(instance_id);
4957
-
4958
- const depKey = `cachly:dep:${dependency}`;
4959
- const raw = await redis.get(depKey);
4960
- if (!raw) return `📭 No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
4961
-
4962
- const topics: string[] = JSON.parse(raw);
4963
- const lines = [
4964
- `🔗 **Causal Chain: \`${dependency}\`** — ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
4965
- '',
4966
- ];
4967
-
4968
- for (const t of topics) {
4969
- const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
4970
- if (!lessonRaw) { lines.push(` • \`${t}\` _(lesson deleted)_`); continue; }
4971
- const lesson = JSON.parse(lessonRaw) as { outcome?: string; severity?: string; needs_review?: boolean };
4972
- const emoji = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'partial' ? '⚠️' : '❌';
4973
- const reviewBadge = lesson.needs_review ? ' 🔍 **needs_review**' : '';
4974
- lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
4975
-
4976
- if (mark_review) {
4977
- const updated = { ...lesson, needs_review: true };
4978
- await redis.set(`cachly:lesson:best:${t}`, JSON.stringify(updated));
4979
- }
4980
- }
4981
-
4982
- if (mark_review) {
4983
- lines.push('', `🔍 All ${topics.length} lessons marked as **needs_review** — verify they still work with the changed dependency.`);
4984
- } else {
4985
- lines.push('', `_Run with \`mark_review: true\` to flag all dependent lessons for re-verification._`);
4986
- }
4987
- return lines.join('\n');
4988
- }
4989
-
4990
3364
  // ── global_learn ──────────────────────────────────────────────────────────
4991
3365
  case 'global_learn': {
4992
3366
  const { instance_id, topic, lesson, severity = 'minor', tags = [] } = args as {
@@ -5268,148 +3642,65 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
5268
3642
  // Generate copilot-instructions.md content
5269
3643
  const tier = inst.tier.toUpperCase();
5270
3644
  const smartRecallNote = hasVector
5271
- ? '- `smart_recall("natural language query")` — semantic search (finds by meaning)'
5272
- : '- `recall_context("arch:*")` — exact/glob key lookup (upgrade to Speed/Business for smart_recall)';
3645
+ ? '3. Call `smart_recall` with keywords for semantic context search'
3646
+ : '3. Call `recall_context` with relevant key patterns like "arch:*" or "file:*"';
5273
3647
  const layerNote = hasVector
5274
- ? `Layer 3 (Semantic): smart_recall uses pgvector HNSW on your ${tier} instance`
5275
- : `Layer 3 (Autopilot): this file — upgrade to Speed/Business tier to unlock smart_recall`;
5276
-
5277
- const copilotInstructions = `# cachly AI Brain — ${project_description}
5278
-
5279
- > 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.
5280
-
5281
- ---
5282
-
5283
- ## 🔴 MANDATORY RULES — Always enforced, no exceptions
5284
-
5285
- ### Rule 1 — Session Start (FIRST thing in every window)
5286
- Call \`session_start\` **before reading any source files or making any changes**:
3648
+ ? `Layer 3 (Semantic): smart_recall uses pgvector HNSW on your ${tier} instance for meaning-based search`
3649
+ : `Layer 3 (Autopilot): This file — upgrade to Speed/Business tier to unlock smart_recall`;
5287
3650
 
5288
- \`\`\`
5289
- session_start(
5290
- instance_id = "${instance_id}",
5291
- focus = "<what you plan to work on today>",
5292
- )
5293
- \`\`\`
5294
-
5295
- Returns: last session summary, handoff from previous window, recent lessons, open failures, brain health.
5296
-
5297
- **If \`session_start\` is unavailable**, fall back:
5298
- 1. \`smart_recall("recent lessons <topic>")\`
5299
- 2. \`recall_best_solution("<topic>")\`
5300
-
5301
- ### Rule 2 — Learn After Every Fix/Deploy/Discovery (AUTOMATIC — no prompting needed)
5302
- After **every** bug fix, deployment, failed attempt, or discovery — call \`learn_from_attempts\` immediately:
5303
-
5304
- \`\`\`
5305
- learn_from_attempts(
5306
- instance_id = "${instance_id}",
5307
- topic = "category:keyword", # deploy:web | fix:auth | debug:db | infra:docker
5308
- outcome = "success" | "failure" | "partial",
5309
- what_worked = "exact approach or command that worked",
5310
- what_failed = "what did NOT work (prevents future wasted time)",
5311
- severity = "critical" | "major" | "minor",
5312
- commands = ["exact command that worked"],
5313
- file_paths = ["path/to/changed/file"],
5314
- )
5315
- \`\`\`
5316
-
5317
- **These events MUST automatically trigger \`learn_from_attempts\`:**
5318
- - ✅ Successfully deployed → \`topic="deploy:<component>", outcome="success"\`
5319
- - ✅ Fixed a bug → \`topic="fix:<description>", outcome="success"\`
5320
- - ✅ Discovered root cause → \`topic="debug:<area>", outcome="partial"\`
5321
- - ✅ Tried something that failed → \`topic="<area>", outcome="failure", what_failed="..."\`
5322
- - ✅ Found a config quirk → \`topic="infra:<topic>", outcome="success"\`
5323
- - ✅ Ran a working command → store it in \`commands=[...]\`
5324
-
5325
- ### Rule 3 — Session Handoff (when closing window or task list > 3 items)
5326
- \`\`\`
5327
- session_handoff(
5328
- instance_id = "${instance_id}",
5329
- completed_tasks = ["what was done"],
5330
- remaining_tasks = ["what is still to do"],
5331
- files_changed = [{"path": "src/...", "status": "modified", "description": "what changed"}],
5332
- instructions = "IMPORTANT: what the next window MUST know to not break things",
5333
- context_summary = "current state of the system",
5334
- )
5335
- \`\`\`
3651
+ const copilotInstructions = `# cachly AI Memory — ${project_description}
5336
3652
 
5337
- The next \`session_start\` automatically includes this handoff. The new window sees exactly what's done, what remains, and what NOT to break.
3653
+ ## 3-Layer AI Memory System
5338
3654
 
5339
- ### Rule 4 — Session End (LAST thing before closing)
5340
- \`\`\`
5341
- session_end(
5342
- instance_id = "${instance_id}",
5343
- summary = "What was accomplished (2-3 sentences with specifics)",
5344
- files_changed = ["list", "of", "changed", "files"],
5345
- lessons_learned = <number of learn_from_attempts calls made>,
5346
- )
5347
- \`\`\`
5348
-
5349
- > \`session_end\` **automatically extracts and stores lessons** from the summary text — no manual calls needed.
3655
+ - **Layer 1 — Storage:** cachly instance \`${inst.name}\` (${tier}) — persistent Valkey brain, survives restarts
3656
+ - **Layer 2 — Tools:** learn_from_attempts · recall_best_solution · smart_recall — the memory API
3657
+ - **Layer 3 — Autopilot (this file):** instructs the AI to run memory tools automatically
5350
3658
 
5351
- ### Rule 5 — Code Index Invalidation (AUTOMATIC)
5352
- - \`index_project\` tracks MD5 hashes per file — **skips unchanged files automatically**
5353
- - After code changes: next \`index_project\` run re-indexes only modified files
5354
- - After delete/rename: run \`index_project\` again to sync
5355
- - No manual invalidation needed
3659
+ ## BEFORE every task — check memory first
5356
3660
 
5357
- ---
3661
+ 1. Call \`recall_best_solution\` with the task topic to get known solutions
3662
+ - \`recall_best_solution("deploy:web")\` → returns working deploy command
3663
+ - \`recall_best_solution("debug:redis-tls")\` → returns root cause + fix
3664
+ - If a solution exists, use it — do NOT re-discover from scratch
5358
3665
 
5359
- ## Task-type trigger table
3666
+ 2. Call \`recall_context\` with the area you're working in
3667
+ - \`recall_context("arch:*")\` → cached architecture decisions
3668
+ - \`recall_context("file:src/auth*")\` → cached file summaries
5360
3669
 
5361
- | You're about to... | Call BEFORE | Call AFTER |
5362
- |---|---|---|
5363
- | Deploy anything | \`recall_best_solution("deploy:<component>")\` | \`learn_from_attempts(topic="deploy:...")\` |
5364
- | Fix a bug | \`recall_best_solution("fix:<area>")\` | \`learn_from_attempts(topic="fix:...")\` |
5365
- | Add a feature | \`session_start(focus="feat:<area>")\` | \`learn_from_attempts(topic="feat:...")\` |
5366
- | Infra/server work | \`recall_best_solution("infra:<topic>")\` | \`learn_from_attempts(topic="infra:...")\` |
5367
- | Debug an issue | \`smart_recall("<error message or symptom>")\` | \`learn_from_attempts(topic="debug:...")\` |
3670
+ ${smartRecallNote}
3671
+ - \`smart_recall("how does authentication work")\` → returns cached auth notes
5368
3672
 
5369
- ---
3673
+ 4. If nothing is found, proceed normally — then save (see below)
5370
3674
 
5371
- ## Available Brain Tools
3675
+ ## AFTER every non-trivial task — save to memory
5372
3676
 
5373
- | Tool | When to use |
5374
- |------|-------------|
5375
- | \`session_start\` | **FIRST** — mandatory at start of every session |
5376
- | \`session_end\` | **LAST** — mandatory at end, auto-learns from summary |
5377
- | \`session_handoff\` | When closing window with remaining tasks |
5378
- | \`learn_from_attempts\` | **AUTOMATIC** after every fix/deploy/discovery |
5379
- | \`recall_best_solution\` | Before any non-trivial task |
5380
- | \`remember_context\` | After analyzing code — save findings for future sessions |
5381
- ${smartRecallNote ? `| \`smart_recall\` | Search brain by meaning/keywords |\n` : ''}\
5382
- | \`recall_context\` | Get exact key (supports glob: \`arch:*\`, \`file:*\`) |
5383
- | \`brain_search\` | BM25+ full-text search over all brain data |
5384
- | \`auto_learn_session\` | Batch-learn from a list of observations (optional) |
5385
- | \`index_project\` | Index source files (smart hash, skips unchanged files) |
5386
- | \`list_remembered\` | See what's cached in the brain |
5387
- | \`forget_context\` | Remove stale context |
3677
+ 1. Call \`learn_from_attempts\` for deploy/debug/fix tasks:
3678
+ - Always set outcome = "success" | "failure" | "partial"
3679
+ - Always include what_failed — it prevents future wasted time
3680
+ - Example: \`learn_from_attempts(topic="deploy:api", outcome="success", what_worked="nohup docker compose up -d --build", what_failed="direct docker compose hangs on SSH timeout")\`
5388
3681
 
5389
- ---
3682
+ 2. Call \`remember_context\` for code/architecture findings:
3683
+ - \`remember_context("arch:auth", "Uses Keycloak with JWT...", category="architecture")\`
3684
+ - \`remember_context("file:src/index.ts", "Entry point, registers routes...", category="file_summary")\`
5390
3685
 
5391
3686
  ## Instance Details
5392
3687
 
5393
- - **Instance ID:** \`${instance_id}\`
5394
- - **Instance name:** ${inst.name}
5395
- - **Tier:** ${tier}
5396
- - **${layerNote}**
5397
- - **Embedding provider:** ${providerArg}
5398
-
5399
- ---
3688
+ - Instance ID: \`${instance_id}\`
3689
+ - Instance name: ${inst.name}
3690
+ - Tier: ${tier}
3691
+ - ${layerNote}
3692
+ - Embedding provider: ${providerArg}
5400
3693
 
5401
- ## How the 3-layer system works
3694
+ ## Quick reference
5402
3695
 
5403
3696
  \`\`\`
5404
- Layer 1 — Storage: Your cachly Valkey instance (${inst.name}) — persists forever
5405
- Layer 2 — Tools: learn_from_attempts · recall_best_solution · brain_search · session_start/end
5406
- Layer 3 — Autopilot: This file — AI reads it and runs tools automatically every session
3697
+ recall_best_solution("topic") # before task
3698
+ smart_recall("natural language") # find by meaning
3699
+ learn_from_attempts(...) # after task
3700
+ remember_context("key", "content") # save analysis
5407
3701
  \`\`\`
5408
-
5409
- Result: Your AI **never solves the same problem twice** and always picks up exactly where it left off. 🚀
5410
3702
  `;
5411
3703
 
5412
-
5413
3704
  const lines: string[] = [
5414
3705
  `🧠 **cachly AI Memory Setup Complete**`,
5415
3706
  ``,
@@ -5433,8 +3724,8 @@ Result: Your AI **never solves the same problem twice** and always picks up exac
5433
3724
  `**Step 3 — copilot-instructions.md (Layer 3 Autopilot):**`,
5434
3725
  ``,
5435
3726
  ...(project_dir
5436
- ? [`🔍 Detecting IDEs in \`${project_dir}\`…`]
5437
- : [`Copy this to \`.github/copilot-instructions.md\` (Copilot), \`CLAUDE.md\` (Claude Code), or \`.cursor/rules\` (Cursor) in your project:`]),
3727
+ ? [`Writing to \`${project_dir}/.github/copilot-instructions.md\`…`]
3728
+ : [`Copy this to \`.github/copilot-instructions.md\` in your project:`]),
5438
3729
  ``,
5439
3730
  `\`\`\`markdown`,
5440
3731
  copilotInstructions,
@@ -5449,71 +3740,16 @@ Result: Your AI **never solves the same problem twice** and always picks up exac
5449
3740
  `Result: Your AI never solves the same problem twice. 🚀`,
5450
3741
  ];
5451
3742
 
5452
- // ── IDE auto-detection + file writing ────────────────────────────────
3743
+ // Optionally write the file
5453
3744
  if (project_dir) {
5454
- const { mkdir, writeFile, access } = await import('node:fs/promises');
5455
- const { constants } = await import('node:fs');
5456
-
5457
- const exists = async (p: string) => access(p, constants.F_OK).then(() => true).catch(() => false);
5458
-
5459
- // Detect which IDEs are present based on marker files/dirs
5460
- interface IdeTarget { ide: string; path: string; content: string }
5461
- const targets: IdeTarget[] = [];
5462
-
5463
- // GitHub Copilot — always write (universal fallback)
5464
- targets.push({
5465
- ide: 'GitHub Copilot',
5466
- path: join(project_dir, '.github', 'copilot-instructions.md'),
5467
- content: copilotInstructions,
5468
- });
5469
-
5470
- // Claude Code — CLAUDE.md in project root
5471
- if (await exists(join(project_dir, 'CLAUDE.md')) || await exists(join(project_dir, '.claude'))) {
5472
- targets.push({
5473
- ide: 'Claude Code',
5474
- path: join(project_dir, 'CLAUDE.md'),
5475
- content: copilotInstructions,
5476
- });
5477
- }
5478
-
5479
- // Cursor — .cursor/rules (new format) or .cursorrules (legacy)
5480
- if (
5481
- await exists(join(project_dir, '.cursor')) ||
5482
- await exists(join(project_dir, '.cursorrules'))
5483
- ) {
5484
- const cursorDir = join(project_dir, '.cursor');
5485
- await mkdir(cursorDir, { recursive: true });
5486
- targets.push({
5487
- ide: 'Cursor',
5488
- path: join(cursorDir, 'rules'),
5489
- content: copilotInstructions,
5490
- });
5491
- }
5492
-
5493
- // Windsurf — .windsurfrules
5494
- if (
5495
- await exists(join(project_dir, '.windsurfrules')) ||
5496
- await exists(join(project_dir, '.windsurf'))
5497
- ) {
5498
- targets.push({
5499
- ide: 'Windsurf',
5500
- path: join(project_dir, '.windsurfrules'),
5501
- content: copilotInstructions,
5502
- });
5503
- }
5504
-
5505
- // VS Code (Copilot) — already covered by .github/copilot-instructions.md above
5506
- // Continue.dev — .continue/config.json is JSON, not markdown — skip, copilot-instructions handles it
5507
-
5508
- const written: string[] = [];
5509
- for (const target of targets) {
5510
- const dir = target.path.substring(0, target.path.lastIndexOf('/'));
5511
- await mkdir(dir, { recursive: true });
5512
- await writeFile(target.path, target.content, 'utf-8');
5513
- written.push(`✅ [${target.ide}] → \`${target.path.replace(project_dir, '.')}\``);
5514
- }
5515
-
5516
- lines.push(...written);
3745
+ const { mkdir, writeFile } = await import('node:fs/promises');
3746
+ const githubDir = join(project_dir, '.github');
3747
+ await mkdir(githubDir, { recursive: true });
3748
+ const filePath = join(githubDir, 'copilot-instructions.md');
3749
+ await writeFile(filePath, copilotInstructions, 'utf-8');
3750
+ lines.splice(lines.indexOf(`Writing to \`${project_dir}/.github/copilot-instructions.md\`…`) + 1, 0,
3751
+ `✅ Written to \`${project_dir}/.github/copilot-instructions.md\``
3752
+ );
5517
3753
  }
5518
3754
 
5519
3755
  return lines.join('\n');
@@ -5641,178 +3877,6 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
5641
3877
  );
5642
3878
  }
5643
3879
 
5644
- // ── Roadmap ──────────────────────────────────────────────────────────────
5645
-
5646
- case 'roadmap_add': {
5647
- const {
5648
- instance_id: rid,
5649
- title,
5650
- description: desc = '',
5651
- priority = 'medium',
5652
- tags: rtags = [],
5653
- milestone = '',
5654
- } = args as {
5655
- instance_id: string; title: string; description?: string;
5656
- priority?: string; tags?: string[]; milestone?: string;
5657
- };
5658
- const redis = await getConnection(rid);
5659
- const id = `rm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
5660
- const item = {
5661
- id, title, description: desc, priority, tags: rtags, milestone,
5662
- status: 'planned',
5663
- created: new Date().toISOString(),
5664
- updated: new Date().toISOString(),
5665
- notes: '',
5666
- };
5667
- await redis.hset(`cachly:roadmap:${rid}`, id, JSON.stringify(item));
5668
- const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
5669
- return [
5670
- `📋 **Roadmap item added**`,
5671
- ``,
5672
- ` ID: \`${id}\``,
5673
- ` Title: ${title}`,
5674
- ` Priority: ${PRIORITY_ICON[priority] ?? '⚪'} ${priority}`,
5675
- ` Status: planned`,
5676
- milestone ? ` Milestone: ${milestone}` : '',
5677
- rtags.length ? ` Tags: ${rtags.join(', ')}` : '',
5678
- ``,
5679
- `💡 Use \`roadmap_update(id: "${id}", status: "in-progress")\` when you start working on it.`,
5680
- ].filter(Boolean).join('\n');
5681
- }
5682
-
5683
- case 'roadmap_update': {
5684
- const {
5685
- instance_id: rid,
5686
- id: itemId,
5687
- status: newStatus,
5688
- priority: newPriority,
5689
- notes: newNotes,
5690
- title: newTitle,
5691
- description: newDesc,
5692
- } = args as {
5693
- instance_id: string; id: string; status?: string; priority?: string;
5694
- notes?: string; title?: string; description?: string;
5695
- };
5696
- const redis = await getConnection(rid);
5697
- const raw = await redis.hget(`cachly:roadmap:${rid}`, itemId);
5698
- if (!raw) return `⚠️ **roadmap_update** – Item \`${itemId}\` not found. Use \`roadmap_list\` to see all items.`;
5699
- const item = JSON.parse(raw) as Record<string, unknown>;
5700
- const oldStatus = item.status as string;
5701
- if (newStatus) item.status = newStatus;
5702
- if (newPriority) item.priority = newPriority;
5703
- if (newTitle) item.title = newTitle;
5704
- if (newDesc) item.description = newDesc;
5705
- if (newNotes) item.notes = item.notes ? `${item.notes}\n[${new Date().toISOString().slice(0, 10)}] ${newNotes}` : `[${new Date().toISOString().slice(0, 10)}] ${newNotes}`;
5706
- item.updated = new Date().toISOString();
5707
- await redis.hset(`cachly:roadmap:${rid}`, itemId, JSON.stringify(item));
5708
- const statusEmoji: Record<string, string> = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
5709
- return [
5710
- `${statusEmoji[newStatus ?? oldStatus] ?? '📋'} **Roadmap updated** \`${itemId}\``,
5711
- ``,
5712
- ` Title: ${item.title}`,
5713
- oldStatus !== newStatus ? ` Status: ${oldStatus} → ${newStatus}` : ` Status: ${item.status}`,
5714
- newNotes ? ` Notes: ${newNotes}` : '',
5715
- ].filter(Boolean).join('\n');
5716
- }
5717
-
5718
- case 'roadmap_list': {
5719
- const {
5720
- instance_id: rid,
5721
- status: filterStatus = 'open',
5722
- tag: filterTag,
5723
- milestone: filterMilestone,
5724
- priority: filterPriority,
5725
- } = args as {
5726
- instance_id: string; status?: string; tag?: string;
5727
- milestone?: string; priority?: string;
5728
- };
5729
- const redis = await getConnection(rid);
5730
- const all = await redis.hgetall(`cachly:roadmap:${rid}`);
5731
- if (!all || Object.keys(all).length === 0) {
5732
- return '📋 **Roadmap is empty.**\n\nUse `roadmap_add` to create your first item.';
5733
- }
5734
- const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
5735
- const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
5736
- const STATUS_ICON: Record<string, string> = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
5737
- const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
5738
- let items = Object.values(all).map(v => JSON.parse(v as string) as Record<string, string | string[]>);
5739
- // Filter
5740
- if (filterStatus === 'open') items = items.filter(i => openStatuses.has(i.status as string));
5741
- else if (filterStatus) items = items.filter(i => i.status === filterStatus);
5742
- if (filterTag) items = items.filter(i => (i.tags as string[]).includes(filterTag));
5743
- if (filterMilestone) items = items.filter(i => i.milestone === filterMilestone);
5744
- if (filterPriority) items = items.filter(i => (PRIORITY_ORDER[i.priority as string] ?? 99) <= (PRIORITY_ORDER[filterPriority] ?? 99));
5745
- // Sort: priority asc, then created asc
5746
- items.sort((a, b) => {
5747
- const pa = PRIORITY_ORDER[a.priority as string] ?? 99;
5748
- const pb = PRIORITY_ORDER[b.priority as string] ?? 99;
5749
- return pa !== pb ? pa - pb : (a.created as string).localeCompare(b.created as string);
5750
- });
5751
- if (items.length === 0) return `📋 **No roadmap items** match the current filter (status: ${filterStatus}).`;
5752
- const grouped: Record<string, typeof items> = {};
5753
- for (const it of items) {
5754
- const st = it.status as string;
5755
- if (!grouped[st]) grouped[st] = [];
5756
- grouped[st].push(it);
5757
- }
5758
- const lines: string[] = [`📋 **Roadmap** (${items.length} item${items.length !== 1 ? 's' : ''})`, ''];
5759
- for (const [st, grp] of Object.entries(grouped)) {
5760
- lines.push(`**${STATUS_ICON[st] ?? '•'} ${st.toUpperCase()}** (${grp.length})`);
5761
- for (const it of grp) {
5762
- const tags = (it.tags as string[]).length ? ` [${(it.tags as string[]).join(', ')}]` : '';
5763
- const milestone = it.milestone ? ` · ${it.milestone}` : '';
5764
- lines.push(` ${PRIORITY_ICON[it.priority as string] ?? '⚪'} \`${it.id}\` **${it.title}**${tags}${milestone}`);
5765
- if (it.description) lines.push(` ${(it.description as string).slice(0, 120)}`);
5766
- if (it.notes) lines.push(` 📝 ${(it.notes as string).split('\n').pop()?.slice(0, 100)}`);
5767
- }
5768
- lines.push('');
5769
- }
5770
- lines.push(`💡 \`roadmap_update(id, status: "in-progress")\` to start · \`roadmap_next\` for top priority item`);
5771
- return lines.join('\n');
5772
- }
5773
-
5774
- case 'roadmap_next': {
5775
- const { instance_id: rid, tag: filterTag } = args as { instance_id: string; tag?: string };
5776
- const redis = await getConnection(rid);
5777
- const all = await redis.hgetall(`cachly:roadmap:${rid}`);
5778
- if (!all || Object.keys(all).length === 0) {
5779
- return '📋 **Roadmap is empty.** Use `roadmap_add` to plan your first task.';
5780
- }
5781
- const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
5782
- const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
5783
- let items = Object.values(all)
5784
- .map(v => JSON.parse(v as string) as Record<string, unknown>)
5785
- .filter(i => i.status === 'in-progress' || i.status === 'planned')
5786
- .filter(i => !filterTag || (i.tags as string[]).includes(filterTag));
5787
- if (items.length === 0) return '🎉 **No open roadmap items!** All tasks are done (or use `roadmap_list` to check).';
5788
- // in-progress first, then by priority
5789
- items.sort((a, b) => {
5790
- if (a.status === 'in-progress' && b.status !== 'in-progress') return -1;
5791
- if (b.status === 'in-progress' && a.status !== 'in-progress') return 1;
5792
- return (PRIORITY_ORDER[a.priority as string] ?? 99) - (PRIORITY_ORDER[b.priority as string] ?? 99);
5793
- });
5794
- const next = items[0];
5795
- const remaining = items.length - 1;
5796
- const tags = (next.tags as string[]).length ? `\nTags: ${(next.tags as string[]).join(', ')}` : '';
5797
- const milestone = next.milestone ? `\nMilestone: ${next.milestone}` : '';
5798
- const notes = next.notes ? `\nNotes: ${(next.notes as string).split('\n').pop()?.slice(0, 120)}` : '';
5799
- return [
5800
- `${next.status === 'in-progress' ? '⚡' : '📋'} **Next up: ${next.title}**`,
5801
- ``,
5802
- `ID: \`${next.id}\``,
5803
- `Priority: ${PRIORITY_ICON[next.priority as string] ?? '⚪'} ${next.priority}`,
5804
- `Status: ${next.status}`,
5805
- next.description ? `\nWhat to do:\n${next.description}` : '',
5806
- tags, milestone, notes,
5807
- ``,
5808
- remaining > 0 ? `(+${remaining} more open item${remaining !== 1 ? 's' : ''} in roadmap)` : '(last open item)',
5809
- ``,
5810
- next.status === 'planned'
5811
- ? `💡 Start with: \`roadmap_update(id: "${next.id}", status: "in-progress")\``
5812
- : `💡 Finish with: \`roadmap_update(id: "${next.id}", status: "done", notes: "...")\``,
5813
- ].filter(s => s !== undefined).join('\n');
5814
- }
5815
-
5816
3880
  default:
5817
3881
  return null;
5818
3882
  }
@@ -5821,81 +3885,14 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
5821
3885
  // ── Server setup ──────────────────────────────────────────────────────────────
5822
3886
 
5823
3887
  const server = new Server(
5824
- { name: 'cachly-mcp', version: CURRENT_VERSION },
3888
+ { name: 'cachly-mcp', version: '0.4.0' },
5825
3889
  { capabilities: { tools: {} } }
5826
3890
  );
5827
3891
 
5828
3892
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
5829
3893
 
5830
- // ── Auto-session management ───────────────────────────────────────────────────
5831
- // Transparently starts a Brain session on the first tool call of a process
5832
- // and saves a session_end summary on SIGTERM/exit so users never have to call
5833
- // session_start/session_end manually.
5834
-
5835
- let _autoSessionInstanceId: string | null = null;
5836
- let _autoSessionStarted = false;
5837
- let _autoSessionToolCount = 0;
5838
-
5839
- async function autoStartSession(instanceId: string): Promise<void> {
5840
- if (_autoSessionStarted) return;
5841
- _autoSessionStarted = true;
5842
- _autoSessionInstanceId = instanceId;
5843
- try {
5844
- await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)' });
5845
- } catch { /* non-fatal — session tracking is a best-effort feature */ }
5846
-
5847
- // Auto-index the project if it hasn't been indexed in the last 24h.
5848
- // This is the main lever for growing token savings: more indexed code →
5849
- // more semantic cache hits → fewer LLM calls for repeated questions.
5850
- if (process.env.CACHLY_AUTO_INDEX !== 'false') {
5851
- try {
5852
- const redis = await getConnection(instanceId);
5853
- const lastIndexed = await redis.get(`cachly:index:last_indexed:${instanceId}`);
5854
- const staleMs = 24 * 60 * 60 * 1000;
5855
- const isStale = !lastIndexed || (Date.now() - parseInt(lastIndexed, 10)) > staleMs;
5856
- if (isStale) {
5857
- // Mark as indexing now to prevent concurrent re-runs.
5858
- await redis.set(`cachly:index:last_indexed:${instanceId}`, String(Date.now()), 'EX', 90000);
5859
- // Run in background — don't block the first tool call.
5860
- handleTool('index_project', {
5861
- instance_id: instanceId,
5862
- dir: process.cwd(),
5863
- max_files: 150,
5864
- ttl: 86400 * 7, // cache for 7 days
5865
- namespace: 'cachly:sem:code',
5866
- }).catch(() => undefined);
5867
- }
5868
- } catch { /* never block the session on indexing errors */ }
5869
- }
5870
- }
5871
-
5872
- async function autoEndSession(): Promise<void> {
5873
- if (!_autoSessionStarted || !_autoSessionInstanceId) return;
5874
- _autoSessionStarted = false;
5875
- try {
5876
- await handleTool('session_end', {
5877
- instance_id: _autoSessionInstanceId,
5878
- summary: `Auto-ended after ${_autoSessionToolCount} tool call(s). Session was started automatically.`,
5879
- files_changed: [],
5880
- lessons_learned: 0,
5881
- });
5882
- } catch { /* non-fatal */ }
5883
- }
5884
-
5885
3894
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
5886
3895
  const { name, arguments: args } = request.params;
5887
-
5888
- // Auto-start brain session on first tool call that has an instance_id.
5889
- // Skip session management tools to avoid recursion.
5890
- const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
5891
- if (!sessionTools.has(name) && !_autoSessionStarted) {
5892
- const instanceId = (args as Record<string, unknown>)?.instance_id as string | undefined;
5893
- if (instanceId) {
5894
- await autoStartSession(instanceId).catch(() => undefined);
5895
- }
5896
- }
5897
- if (!sessionTools.has(name)) _autoSessionToolCount++;
5898
-
5899
3896
  try {
5900
3897
  const text = await handleTool(name, (args ?? {}) as Record<string, unknown>);
5901
3898
  return { content: [{ type: 'text', text }] };
@@ -5905,15 +3902,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5905
3902
  }
5906
3903
  });
5907
3904
 
5908
- // Graceful shutdown – close all Redis connections + auto-end brain session
3905
+ // Graceful shutdown – close all Redis connections
5909
3906
  process.on('SIGTERM', async () => {
5910
- await autoEndSession().catch(() => undefined);
5911
- for (const [, client] of pool) await client.quit().catch(() => undefined);
5912
- process.exit(0);
5913
- });
5914
-
5915
- process.on('SIGINT', async () => {
5916
- await autoEndSession().catch(() => undefined);
5917
3907
  for (const [, client] of pool) await client.quit().catch(() => undefined);
5918
3908
  process.exit(0);
5919
3909
  });
@@ -5934,8 +3924,6 @@ const EDITOR_FILES: Record<string, string> = {
5934
3924
  windsurf: '.windsurf/mcp.json',
5935
3925
  copilot: '.vscode/mcp.json',
5936
3926
  continue: '.continue/config.json',
5937
- cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
5938
- zed: '.zed/settings.json', // Zed project-level context_servers
5939
3927
  };
5940
3928
 
5941
3929
  const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
@@ -5952,20 +3940,6 @@ function buildMcpConfig(apiKey: string, instanceId: string, editor: string): str
5952
3940
  },
5953
3941
  }, null, 2);
5954
3942
  }
5955
- if (editor === 'zed') {
5956
- return JSON.stringify({
5957
- context_servers: {
5958
- cachly: {
5959
- command: {
5960
- path: 'npx',
5961
- args: ['-y', '@cachly-dev/mcp-server@latest'],
5962
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
5963
- },
5964
- settings: {},
5965
- },
5966
- },
5967
- }, null, 2);
5968
- }
5969
3943
  return JSON.stringify({
5970
3944
  mcpServers: {
5971
3945
  cachly: {
@@ -5979,54 +3953,48 @@ function buildMcpConfig(apiKey: string, instanceId: string, editor: string): str
5979
3953
 
5980
3954
  function buildClaudeMdBlock(instanceId: string): string {
5981
3955
  return `${CLAUDE_MD_MARKER_START}
5982
- ## Cachly AI Brain
3956
+ ## Cachly AI Brain — Persistent Memory
5983
3957
 
5984
- The Cachly MCP server is available as tool \`cachly\`.
5985
- Your instance ID: \`${instanceId}\`
3958
+ Your AI Brain instance ID: \`${instanceId}\`
5986
3959
 
5987
- > **Sessions are fully automatic** (MCP ≥ 0.5.3).
5988
- > The Brain starts tracking when you call any tool for the first time and saves a summary when your editor closes. You never need to call \`session_start\` or \`session_end\` manually.
3960
+ > The Brain gives you persistent memory across sessions. Every lesson you learn,
3961
+ > every bug you fix, every architecture decision — recalled instantly next time.
3962
+ > instance_id is pre-filled below. Do NOT ask the user for it.
5989
3963
 
5990
- ---
3964
+ ### MANDATORY: Call at the START of every session
3965
+ \`\`\`
3966
+ session_start(instance_id="${instanceId}", focus="<what you're working on today>")
3967
+ \`\`\`
3968
+ This loads your previous context, recent lessons, and open tasks automatically.
5991
3969
 
5992
- ### Before editing any file — WIP-registry (prevents half-finished code across sessions):
3970
+ ### MANDATORY: Call at the END of every session
5993
3971
  \`\`\`
5994
- remember_context(
5995
- instance_id = "${instanceId}",
5996
- key = "wip-registry",
5997
- content = "EDITING: <file:line> — goal: <what you are implementing>",
5998
- category = "bug",
5999
- ttl = 86400,
6000
- )
3972
+ session_end(instance_id="${instanceId}", summary="<2-3 sentences on what was done>", files_changed=["src/..."], lessons_learned=1)
6001
3973
  \`\`\`
6002
- After the edit is complete, update \`content\` to \`"DONE: <file> — <what was completed>"\`.
6003
3974
 
6004
- ### After fixing any bug or solving a tricky problem:
3975
+ ### After fixing ANY bug or solving a hard problem
6005
3976
  \`\`\`
6006
3977
  learn_from_attempts(
6007
- instance_id = "${instanceId}",
6008
- topic = "category:keyword",
6009
- outcome = "success",
6010
- what_worked = "...",
6011
- what_failed = "...",
6012
- severity = "critical" | "major" | "minor",
6013
- file_paths = ["path/to/file"],
6014
- commands = ["the command that worked"],
6015
- tags = ["tag1"],
3978
+ instance_id="${instanceId}",
3979
+ topic="category:keyword", # e.g. "deploy:web", "bug:redis-timeout", "arch:auth"
3980
+ outcome="success",
3981
+ what_worked="exact solution",
3982
+ what_failed="what didn't work (optional)",
3983
+ commands=["the command that worked"],
3984
+ file_paths=["relevant/file.ts"],
3985
+ severity="critical"|"major"|"minor",
6016
3986
  )
6017
3987
  \`\`\`
6018
3988
 
6019
- ### Before starting any task — recall relevant lessons first:
3989
+ ### Before starting any non-trivial task
6020
3990
  \`\`\`
6021
- smart_recall(
6022
- instance_id = "${instanceId}",
6023
- query = "<describe what you are about to do>",
6024
- )
3991
+ recall_best_solution(instance_id="${instanceId}", topic="relevant:topic")
6025
3992
  \`\`\`
6026
3993
 
6027
- ### Half-finished code rule:
6028
- Never commit code that does not compile. Run \`tsc --noEmit\` / \`go build ./...\` before every commit.
6029
- If a session ends mid-task, save the WIP-registry entry so the next session picks up exactly where you left off.
3994
+ ### If a stored lesson is wrong
3995
+ \`\`\`
3996
+ forget_lesson(instance_id="${instanceId}", topic="wrong:topic")
3997
+ \`\`\`
6030
3998
  ${CLAUDE_MD_MARKER_END}`;
6031
3999
  }
6032
4000
 
@@ -6117,9 +4085,8 @@ if (process.argv[2] === 'setup') {
6117
4085
  const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
6118
4086
  const CLIENT_ID = 'cachly-cli';
6119
4087
 
6120
- console.log('Step 1: Sign in to cachly (free, no credit card)\n');
4088
+ console.log('Step 1: Sign in to cachly (free — no credit card required)\n');
6121
4089
 
6122
- // Start device flow
6123
4090
  let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
6124
4091
  try {
6125
4092
  const deviceRes = await fetch(`${AUTH_BASE}/auth/device`, {
@@ -6127,91 +4094,92 @@ if (process.argv[2] === 'setup') {
6127
4094
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
6128
4095
  body: `client_id=${CLIENT_ID}&scope=openid`,
6129
4096
  });
6130
- if (!deviceRes.ok) throw new Error(`Device flow error: HTTP ${deviceRes.status}`);
4097
+ if (!deviceRes.ok) throw new Error(`HTTP ${deviceRes.status}`);
6131
4098
  const data = await deviceRes.json() as {
6132
4099
  device_code: string; user_code: string;
6133
4100
  verification_uri_complete: string; interval: number;
6134
4101
  };
6135
- deviceCode = data.device_code;
6136
- userCode = data.user_code;
6137
- verifyUri = data.verification_uri_complete;
6138
- pollInterval = (data.interval ?? 5) * 1000;
4102
+ deviceCode = data.device_code;
4103
+ userCode = data.user_code;
4104
+ verifyUri = data.verification_uri_complete;
4105
+ pollInterval = (data.interval ?? 5) * 1000;
6139
4106
  } catch (e) {
6140
- console.error(`\nFailed to start device flow: ${(e as Error).message}`);
6141
- console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
4107
+ console.error(`\nCould not reach auth service: ${(e as Error).message}`);
4108
+ console.error('Fallback: sign in at https://cachly.dev and paste your API token.\n');
6142
4109
  token = await ask(' Paste API token (cky_live_...): ');
6143
- if (!token) { console.error('\nToken is required. Aborting.\n'); rl.close(); process.exit(1); }
6144
- console.log('');
6145
- deviceCode = ''; // mark as fallback so we skip polling
4110
+ if (!token) { console.error('\nToken required. Aborting.\n'); rl.close(); process.exit(1); }
4111
+ deviceCode = '';
6146
4112
  }
6147
4113
 
6148
- if (deviceCode!) {
6149
- // Open browser
6150
- console.log(` Code: \x1b[1;33m${userCode!}\x1b[0m`);
6151
- console.log(` URL: ${verifyUri!}\n`);
4114
+ if (deviceCode) {
4115
+ console.log(` Code: \x1b[1;33m${userCode}\x1b[0m`);
4116
+ console.log(` URL: ${verifyUri}\n`);
6152
4117
  try {
6153
4118
  const { execSync } = await import('node:child_process');
6154
4119
  const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
6155
- execSync(`${openCmd} "${verifyUri!}"`, { stdio: 'ignore' });
4120
+ execSync(`${openCmd} "${verifyUri}"`, { stdio: 'ignore' });
6156
4121
  console.log(' ✓ Browser opened — confirm the code above to continue...\n');
6157
4122
  } catch {
6158
- console.log(' 👉 Open the URL above in your browser and enter the code.\n');
4123
+ console.log(' 👉 Open the URL above in your browser and confirm the code.\n');
6159
4124
  }
6160
4125
 
6161
- // Poll for token
6162
4126
  process.stdout.write(' Waiting for authorization');
6163
- const deadline = Date.now() + 10 * 60 * 1000; // 10 min
4127
+ const deadline = Date.now() + 10 * 60 * 1000;
6164
4128
  while (Date.now() < deadline) {
6165
- await new Promise(r => setTimeout(r, pollInterval!));
4129
+ await new Promise(r => setTimeout(r, pollInterval));
6166
4130
  process.stdout.write('.');
6167
4131
  try {
6168
4132
  const tokenRes = await fetch(`${AUTH_BASE}/token`, {
6169
4133
  method: 'POST',
6170
4134
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
6171
- body: `client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode!}`,
4135
+ body: `client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode}`,
6172
4136
  });
6173
- const tokenData = await tokenRes.json() as { access_token?: string; error?: string };
6174
- if (tokenData.access_token) {
6175
- token = tokenData.access_token;
4137
+ const td = await tokenRes.json() as { access_token?: string; error?: string };
4138
+ if (td.access_token) {
4139
+ token = td.access_token;
6176
4140
  console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
6177
4141
  break;
6178
4142
  }
6179
- // authorization_pending = keep polling; slow_down = increase interval
6180
- if (tokenData.error === 'slow_down') pollInterval = Math.min(pollInterval! + 2000, 15000);
6181
- else if (tokenData.error && tokenData.error !== 'authorization_pending') {
6182
- console.error(`\nAuth error: ${tokenData.error}. Aborting.\n`);
4143
+ if (td.error === 'slow_down') pollInterval = Math.min(pollInterval + 2000, 15000);
4144
+ else if (td.error && td.error !== 'authorization_pending') {
4145
+ console.error(`\nAuth error: ${td.error}. Aborting.\n`);
6183
4146
  rl.close(); process.exit(1);
6184
4147
  }
6185
4148
  } catch { /* network hiccup — keep polling */ }
6186
4149
  }
6187
- if (!token) { console.error('\nTimed out waiting for authorization. Aborting.\n'); rl.close(); process.exit(1); }
6188
- console.log('');
4150
+ if (!token) { console.error('\nTimed out. Aborting.\n'); rl.close(); process.exit(1); }
6189
4151
  }
6190
- }
6191
4152
 
6192
- // ── Step 1b: Exchange Keycloak JWT → long-lived cky_live_ API key ──────────
6193
- // Only do this when the token looks like a Keycloak JWT (starts with "eyJ"),
6194
- // not when the user already pasted a cky_live_ key directly.
6195
- if (token.startsWith('eyJ')) {
6196
- process.stdout.write('⏳ Generating your API key...');
6197
- try {
6198
- const keyRes = await fetch(`${API_URL}/api/v1/api-keys`, {
6199
- method: 'POST',
6200
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
6201
- body: JSON.stringify({ name: 'cachly-mcp-setup', scope: 'read_write' }),
6202
- });
6203
- if (!keyRes.ok) throw new Error(`HTTP ${keyRes.status}`);
6204
- const keyBody = await keyRes.json() as { key: string };
6205
- if (!keyBody.key) throw new Error('no key in response');
6206
- token = keyBody.key; // swap JWT → cky_live_...
6207
- console.log(' ✓\n');
6208
- } catch (e) {
6209
- console.log(' (skipped)\n');
6210
- // Non-fatal: fall back to using the Keycloak JWT directly.
6211
- // It will expire but setup still works for now.
4153
+ // Exchange short-lived Keycloak JWT → long-lived cky_live_ API key
4154
+ if (token.startsWith('eyJ')) {
4155
+ process.stdout.write('⏳ Generating your API key...');
4156
+ try {
4157
+ const keyRes = await fetch(`${API_URL}/api/v1/api-keys`, {
4158
+ method: 'POST',
4159
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
4160
+ body: JSON.stringify({ name: 'cachly-mcp-setup', scope: 'read_write' }),
4161
+ });
4162
+ if (!keyRes.ok) throw new Error(`HTTP ${keyRes.status}`);
4163
+ const keyBody = await keyRes.json() as { key?: string };
4164
+ if (keyBody.key) { token = keyBody.key; console.log(' ✓\n'); }
4165
+ else throw new Error('no key in response');
4166
+ } catch (e) {
4167
+ console.log(` (skipped — JWT will be used directly)\n`);
4168
+ }
6212
4169
  }
6213
4170
  }
6214
4171
 
4172
+ // ── Step 1b: Auto-provision a Brain instance if none exist ─────────────────
4173
+ // POST /api/v1/instances/auto is idempotent — returns existing or creates free Brain
4174
+ let autoProvisioned = false;
4175
+ try {
4176
+ const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
4177
+ method: 'POST',
4178
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
4179
+ });
4180
+ if (autoRes.ok) autoProvisioned = true;
4181
+ } catch { /* non-fatal */ }
4182
+
6215
4183
  // ── Step 2: Fetch & pick instance ─────────────────────────────────────────
6216
4184
  process.stdout.write('⏳ Fetching your instances...');
6217
4185
  let instances: Array<{ id: string; name: string; status: string; tier: string; region: string }> = [];
@@ -6223,62 +4191,31 @@ if (process.argv[2] === 'setup') {
6223
4191
  const body = await res.json() as { data: typeof instances };
6224
4192
  instances = (body.data ?? []).filter(i => i.status === 'running');
6225
4193
  } catch (e) {
6226
- const msg = (e as Error).message;
6227
- console.error(`\n\nFailed to fetch instances: ${msg}`);
6228
- if (msg.includes('401')) {
6229
- console.error('Token rejected. Get a valid token at https://cachly.dev/setup-ai\n');
6230
- try {
6231
- const { execSync } = await import('node:child_process');
6232
- const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
6233
- execSync(`${openCmd} https://cachly.dev/setup-ai`, { stdio: 'ignore' });
6234
- } catch { /* ignore */ }
6235
- }
4194
+ console.error(`\n\nFailed to fetch instances: ${(e as Error).message}`);
4195
+ console.error('Check your token and try again.\n');
6236
4196
  rl.close(); process.exit(1);
6237
4197
  }
6238
4198
  console.log(` found ${instances.length}\n`);
6239
4199
 
6240
4200
  if (instances.length === 0) {
6241
- // Auto-provision a free Brain instance so users don't have to visit the website.
6242
- process.stdout.write('⏳ Creating your free Brain instance...');
6243
- try {
6244
- const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
6245
- method: 'POST',
6246
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
6247
- });
6248
- if (!autoRes.ok) throw new Error(`HTTP ${autoRes.status}`);
6249
- const autoBody = await autoRes.json() as { instance?: { id: string; name: string; status: string; tier: string; region: string }; instance_id?: string; status?: string; created?: boolean };
6250
- if (autoBody.instance) {
6251
- // Returned an existing instance.
6252
- instances = [autoBody.instance];
6253
- } else if (autoBody.instance_id) {
6254
- // Newly created — poll until running or give up after 30 s.
6255
- const newId = autoBody.instance_id;
6256
- console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
6257
- process.stdout.write('⏳ Waiting for instance to start');
6258
- for (let attempt = 0; attempt < 15; attempt++) {
6259
- await new Promise(r => setTimeout(r, 2000));
6260
- process.stdout.write('.');
6261
- try {
6262
- const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
6263
- if (checkRes.ok) {
6264
- const inst = await checkRes.json() as { id: string; name: string; status: string; tier: string; region: string };
6265
- if (inst.status === 'running') { instances = [inst]; break; }
6266
- }
6267
- } catch { /* keep polling */ }
6268
- }
6269
- console.log('');
4201
+ if (autoProvisioned) {
4202
+ // Poll up to 10× every 3s (30s total) waiting for the Brain to start
4203
+ process.stdout.write(' waiting for Brain to start');
4204
+ for (let attempt = 0; attempt < 10 && instances.length === 0; attempt++) {
4205
+ await new Promise(r => setTimeout(r, 3000));
4206
+ process.stdout.write('.');
4207
+ try {
4208
+ const retry = await fetch(`${API_URL}/api/v1/instances`, { headers: { Authorization: `Bearer ${token}` } });
4209
+ if (retry.ok) {
4210
+ const b = await retry.json() as { data: typeof instances };
4211
+ instances = (b.data ?? []).filter(i => i.status === 'running');
4212
+ }
4213
+ } catch { /* ignore */ }
6270
4214
  }
6271
- } catch (e) {
6272
- console.log(` failed: ${(e as Error).message}\n`);
4215
+ process.stdout.write(instances.length > 0 ? ' ready!\n' : '\n');
6273
4216
  }
6274
-
6275
4217
  if (instances.length === 0) {
6276
- console.error('\nCould not create an instance automatically. Opening https://cachly.dev/instances …\n');
6277
- try {
6278
- const { execSync } = await import('node:child_process');
6279
- const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
6280
- execSync(`${openCmd} https://cachly.dev/instances`, { stdio: 'ignore' });
6281
- } catch { /* ignore */ }
4218
+ console.error('\nBrain provisioning is taking longer than expected. Run setup again in a minute.\n');
6282
4219
  rl.close(); process.exit(1);
6283
4220
  }
6284
4221
  }
@@ -6298,28 +4235,15 @@ if (process.argv[2] === 'setup') {
6298
4235
  // ── Step 3: Detect editors ────────────────────────────────────────────────
6299
4236
  const cwd = process.cwd();
6300
4237
  const detected: string[] = [];
6301
- const { homedir } = await import('node:os');
6302
- const home = homedir();
6303
4238
  // Claude Code: always include (CLAUDE.md is universal)
6304
4239
  detected.push('claude');
6305
4240
  if (existsSync(resolve(cwd, '.cursor'))) detected.push('cursor');
6306
4241
  if (existsSync(resolve(cwd, '.windsurf'))) detected.push('windsurf');
6307
4242
  if (existsSync(resolve(cwd, '.vscode'))) detected.push('copilot');
6308
4243
  if (existsSync(resolve(cwd, '.continue'))) detected.push('continue');
6309
- // Cline: VS Code extension — detect via globalStorage on the machine
6310
- const clineGlobalDir = process.platform === 'darwin'
6311
- ? resolve(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev')
6312
- : resolve(home, '.vscode/extensions');
6313
- if (existsSync(clineGlobalDir) && !detected.includes('copilot')) detected.push('cline');
6314
- else if (existsSync(clineGlobalDir)) detected.push('cline'); // both copilot + cline share .vscode/mcp.json — fine
6315
- // Zed editor — detect via app data dir
6316
- const zedDir = process.platform === 'darwin'
6317
- ? resolve(home, 'Library/Application Support/Zed')
6318
- : resolve(home, '.config/zed');
6319
- if (existsSync(zedDir)) detected.push('zed');
6320
4244
 
6321
4245
  const editorLabel = (e: string) =>
6322
- ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
4246
+ ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev' })[e] ?? e;
6323
4247
 
6324
4248
  console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
6325
4249
  const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
@@ -6328,21 +4252,7 @@ if (process.argv[2] === 'setup') {
6328
4252
  : editorsRaw.split(/[,\s]+/).map(s => s.toLowerCase()).filter(s => EDITOR_FILES[s]);
6329
4253
  console.log('');
6330
4254
 
6331
- // ── Step 4: Email opt-in ──────────────────────────────────────────────────
6332
- console.log('Step 4: Stay in the loop (optional)');
6333
- const email = await ask(' Email for release notes & tips? [Enter to skip]: ');
6334
- if (email && email.includes('@')) {
6335
- try {
6336
- await fetch(`${API_URL}/api/newsletter/subscribe`, {
6337
- method: 'POST', headers: { 'Content-Type': 'application/json' },
6338
- body: JSON.stringify({ email: email.trim().toLowerCase(), source: 'mcp-setup' }),
6339
- signal: AbortSignal.timeout(5000),
6340
- });
6341
- console.log('✅ Subscribed — you\'ll only hear from us when it matters.\n');
6342
- } catch { console.log('⚠️ Could not subscribe (no internet?). Continuing...\n'); }
6343
- } else { console.log(' Skipped.\n'); }
6344
-
6345
- // ── Step 5: Write editor configs ──────────────────────────────────────────
4255
+ // ── Step 4: Write editor configs ──────────────────────────────────────────
6346
4256
  for (const editor of editorsToSetup) {
6347
4257
  const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
6348
4258
  const configPath = resolve(cwd, configFile);
@@ -6351,7 +4261,7 @@ if (process.argv[2] === 'setup') {
6351
4261
  console.log(`✅ Written: ${configFile}`);
6352
4262
  }
6353
4263
 
6354
- // ── Step 6: CLAUDE.md (always — idempotent) ───────────────────────────────
4264
+ // ── Step 5: CLAUDE.md (always — idempotent) ───────────────────────────────
6355
4265
  const mdResult = await writeClaudeMd(cwd, instance.id);
6356
4266
  const mdLabel = mdResult === 'updated' ? '✅ Updated' : mdResult === 'appended' ? '✅ Appended to' : '✅ Written';
6357
4267
  console.log(`${mdLabel}: CLAUDE.md`);
@@ -6359,172 +4269,11 @@ if (process.argv[2] === 'setup') {
6359
4269
  rl.close();
6360
4270
  console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
6361
4271
  console.log(` Your AI now has persistent memory across every session.\n`);
6362
-
6363
- // ── Step 7: Show first session_start preview (Aha moment) ────────────────
6364
- // Fetch brain health from the API to show the user what their agent will see.
6365
- process.stdout.write('\n⏳ Fetching your Brain health preview...');
6366
- try {
6367
- const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
6368
- headers: { Authorization: `Bearer ${token}` },
6369
- signal: AbortSignal.timeout(6000),
6370
- });
6371
- if (brainRes.ok) {
6372
- const brainData = await brainRes.json() as {
6373
- lesson_count?: number; context_count?: number;
6374
- open_failures?: number; quality_score?: number;
6375
- };
6376
- const lessons = brainData.lesson_count ?? 0;
6377
- const contexts = brainData.context_count ?? 0;
6378
- const score = brainData.quality_score ?? 0;
6379
- const level = lessons === 0 ? 'Intern 🌱' :
6380
- lessons < 10 ? 'Junior Dev 🔧' :
6381
- lessons < 30 ? 'Mid Dev ⚡' :
6382
- lessons < 60 ? 'Senior Dev 🧠' :
6383
- lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
6384
- console.log(' ✓\n');
6385
- console.log('┌──────────────────────────────────────────────────────┐');
6386
- console.log(`│ 🧠 Brain Health Report │`);
6387
- console.log('├──────────────────────────────────────────────────────┤');
6388
- console.log(`│ Lessons stored : ${String(lessons).padEnd(6)} │ Level: ${level.padEnd(20)}│`);
6389
- console.log(`│ Context entries : ${String(contexts).padEnd(6)} │ Quality score: ${String(Math.round(score * 100)).padEnd(3)}% │`);
6390
- if (lessons === 0) {
6391
- console.log('├──────────────────────────────────────────────────────┤');
6392
- console.log('│ Your Brain is empty and ready to learn. │');
6393
- console.log('│ After your first coding session it will contain: │');
6394
- console.log('│ • Lessons from every bug you fixed │');
6395
- console.log('│ • Your project\'s indexed files │');
6396
- console.log('│ • A session summary for next time │');
6397
- }
6398
- console.log('└──────────────────────────────────────────────────────┘');
6399
- } else {
6400
- console.log(' (skipped)');
6401
- }
6402
- } catch { console.log(' (skipped)'); }
6403
-
6404
- console.log(`\n Your AI will call session_start automatically when it uses the first tool.`);
6405
- console.log(` To see your Brain at any time: https://cachly.dev/instances/${instance.id}\n`);
6406
- process.exit(0);
6407
- }
6408
-
6409
- // ── CLI: cachly index <dir> ───────────────────────────────────────────────────
6410
- // Usage: npx @cachly-dev/mcp-server@latest index [./path/to/project]
6411
- // Indexes the project directory into the Brain — perfect for CI/CD cron jobs.
6412
-
6413
- if (process.argv[2] === 'index') {
6414
- const { resolve } = await import('node:path');
6415
- const argv = process.argv.slice(3);
6416
- const flag = (name: string) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
6417
-
6418
- const dir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
6419
- const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
6420
- const maxFiles = parseInt(flag('max-files') ?? '500', 10);
6421
- const namespace = flag('namespace') ?? 'cachly:sem:code';
6422
-
6423
- if (!instanceId || !JWT) {
6424
- console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
6425
- console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
6426
- console.error(' export CACHLY_JWT=<cky_live_...>');
6427
- console.error(' npx @cachly-dev/mcp-server@latest index ./my-project\n');
6428
- process.exit(1);
6429
- }
6430
-
6431
- console.log(`\n📂 Indexing: ${dir}`);
6432
- console.log(` Instance: ${instanceId.slice(0, 8)}… Max files: ${maxFiles}\n`);
6433
-
6434
- try {
6435
- const result = await handleTool('index_project', {
6436
- instance_id: instanceId,
6437
- dir,
6438
- max_files: maxFiles,
6439
- ttl: 86400 * 7,
6440
- namespace,
6441
- });
6442
- console.log(result);
6443
- console.log('\n✅ Indexing complete.\n');
6444
- } catch (err) {
6445
- console.error(`\n❌ Indexing failed: ${(err as Error).message}\n`);
6446
- process.exit(1);
6447
- }
6448
4272
  process.exit(0);
6449
4273
  }
6450
4274
 
6451
4275
  // ── Start ─────────────────────────────────────────────────────────────────────
6452
4276
 
6453
- // Warn on stderr when credentials are missing so the user sees a clear
6454
- // actionable message in their editor's MCP log instead of silent failures.
6455
- if (!JWT) {
6456
- process.stderr.write(
6457
- '\n' +
6458
- '╔══════════════════════════════════════════════════════════════════╗\n' +
6459
- '║ 🧠 cachly AI Brain — Setup required ║\n' +
6460
- '╠══════════════════════════════════════════════════════════════════╣\n' +
6461
- '║ ║\n' +
6462
- '║ CACHLY_JWT is not set. Get your free credentials at: ║\n' +
6463
- '║ ║\n' +
6464
- '║ 👉 https://cachly.dev/setup-ai ║\n' +
6465
- '║ ║\n' +
6466
- '║ Then run the interactive setup wizard: ║\n' +
6467
- '║ ║\n' +
6468
- '║ npx @cachly-dev/mcp-server@latest setup ║\n' +
6469
- '║ ║\n' +
6470
- '║ Free tier — no credit card required. ║\n' +
6471
- '╚══════════════════════════════════════════════════════════════════╝\n' +
6472
- '\n',
6473
- );
6474
- }
6475
-
6476
- // ── Update nudge (non-blocking, fire-and-forget) ─────────────────────────────
6477
- // Check npm registry once per process start; if outdated, log to stderr so
6478
- // the editor's MCP log shows an actionable one-liner. Skipped if opted out.
6479
- if (!process.env.CACHLY_NO_UPDATE_CHECK) {
6480
- (async () => {
6481
- try {
6482
- const res = await fetch(
6483
- `https://registry.npmjs.org/@cachly-dev/mcp-server/latest`,
6484
- { signal: AbortSignal.timeout(4000) },
6485
- );
6486
- if (res.ok) {
6487
- const data = await res.json() as { version: string };
6488
- const latest = data?.version ?? '';
6489
- if (latest && latest !== CURRENT_VERSION) {
6490
- process.stderr.write(
6491
- `\n⚡ cachly update available: ${CURRENT_VERSION} → ${latest}\n` +
6492
- ` Run: npx @cachly-dev/mcp-server@latest setup\n\n`,
6493
- );
6494
- }
6495
- }
6496
- } catch { /* ignore – network unavailable or timeout */ }
6497
- })();
6498
- }
6499
-
6500
- const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
6501
-
6502
- if (httpPort) {
6503
- // ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
6504
- // Used for Smithery URL deployment: PORT=3000 node dist/index.js
6505
- const { createServer } = await import('node:http');
6506
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
6507
- await server.connect(transport);
6508
- const httpServer = createServer((req, res) => {
6509
- const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
6510
- if (url.pathname === '/health') {
6511
- res.writeHead(200, { 'Content-Type': 'application/json' });
6512
- res.end(JSON.stringify({ status: 'ok', version: CURRENT_VERSION }));
6513
- return;
6514
- }
6515
- if (url.pathname === '/mcp' || url.pathname === '/') {
6516
- transport.handleRequest(req, res);
6517
- return;
6518
- }
6519
- res.writeHead(404);
6520
- res.end('not found');
6521
- });
6522
- httpServer.listen(httpPort, () => {
6523
- process.stderr.write(`cachly-mcp HTTP server listening on :${httpPort}\n`);
6524
- });
6525
- } else {
6526
- // ── stdio mode (default for local editor use) ───────────────────────────
6527
- const transport = new StdioServerTransport();
6528
- await server.connect(transport);
6529
- }
4277
+ const transport = new StdioServerTransport();
4278
+ await server.connect(transport);
6530
4279