@cachly-dev/mcp-server 0.5.48 → 0.5.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.5.48",
3
+ "version": "0.5.50",
4
4
  "mcpName": "io.github.cachly-dev/mcp-server",
5
5
  "description": "AI memory brain for Claude Code, Cursor, Copilot & Windsurf — persistent sessions, lessons learned, semantic search, team brain. 51 MCP tools.",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -55,7 +55,7 @@ import { Redis } from 'ioredis';
55
55
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
56
56
  const JWT = process.env.CACHLY_JWT ?? '';
57
57
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
58
- const CURRENT_VERSION = '0.5.48';
58
+ const CURRENT_VERSION = '0.5.50';
59
59
  /**
60
60
  * ┌──────────────────────────────────────────────────────────────────────┐
61
61
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -417,14 +417,39 @@ const STOPWORDS = new Set([
417
417
  'רק', 'עוד', 'יש', 'אין', 'מה', 'זה', 'זאת', 'אלה', 'כל', 'כן',
418
418
  'לו', 'לה', 'להם', 'בו', 'בה', 'בהם', 'שם', 'כך', 'כן', 'מי',
419
419
  'אשר', 'אחרי', 'לפני', 'תחת', 'בין', 'מאז', 'עד', 'כמו', 'אז',
420
+ // ── Farsi/Persian — common function words, prepositions, pronouns ──
421
+ 'در', 'از', 'به', 'با', 'که', 'این', 'آن', 'را', 'است',
422
+ 'بود', 'باشد', 'شد', 'شده', 'می', 'نه', 'نیست', 'هم', 'هر',
423
+ 'یک', 'اما', 'و', 'یا', 'تا', 'اگر', 'برای', 'چون', 'چه',
424
+ 'کجا', 'کی', 'چطور', 'وقتی', 'بعد', 'قبل', 'مثل', 'همه', 'بعضی',
425
+ 'هیچ', 'آیا', 'خود', 'چند', 'دیگر', 'هنوز', 'همان', 'آنها', 'اینها',
426
+ 'ما', 'شما', 'آنان', 'من', 'تو', 'او', 'آنان', 'ایشان',
427
+ 'روی', 'زیر', 'بین', 'پیش', 'پس', 'طرف', 'داخل', 'خارج', 'کنار',
428
+ 'همچنین', 'مگر', 'ولی', 'وگرنه', 'چرا', 'چگونه', 'کدام',
429
+ // ── Hindi — common particles, postpositions, pronouns, auxiliary verbs ──
430
+ 'है', 'में', 'से', 'को', 'का', 'की', 'के', 'पर', 'और', 'या',
431
+ 'नहीं', 'यह', 'वह', 'एक', 'इस', 'उस', 'भी', 'हो', 'था', 'थी',
432
+ 'थे', 'हैं', 'हूँ', 'लिए', 'तक', 'साथ', 'बाद', 'पहले', 'जो',
433
+ 'जब', 'कैसे', 'क्यों', 'क्या', 'कहाँ', 'कौन', 'हम', 'आप', 'वे',
434
+ 'मैं', 'तुम', 'उन', 'इन', 'ने', 'बहुत', 'सब', 'कुछ', 'फिर',
435
+ 'अब', 'तो', 'ही', 'तरह', 'जैसे', 'करना', 'होना', 'रहा', 'रही',
436
+ 'रहे', 'गया', 'गई', 'गए', 'किया', 'किए', 'कर', 'हुआ', 'हुई',
420
437
  ]);
421
438
 
439
+
422
440
  // CJK Unicode ranges used for bigram extraction
423
441
  const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
424
442
  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
443
 
426
444
  // RTL Unicode ranges: Arabic (U+0600–U+06FF + extended), Hebrew (U+0590–U+05FF)
427
445
  const RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufb4f\ufb50-\ufdff\ufe70-\ufeff]/;
446
+ // Distinguish Arabic from Hebrew within RTL segments
447
+ const HEBREW_CHAR_RE = /[\u0590-\u05ff\ufb1d-\ufb4f]/;
448
+ const ARABIC_CHAR_RE = /[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb50-\ufdff\ufe70-\ufeff]/;
449
+ // Persian/Farsi: exclusive chars not in Arabic (U+067E=پ, U+0686=چ, U+0698=ژ, U+06AF=گ, U+06CC=ی)
450
+ const FARSI_CHAR_RE = /[\u067e\u0686\u0698\u06af\u06cc]/;
451
+ // Devanagari (Hindi, Sanskrit, Marathi …) — U+0900–U+097F + extended
452
+ const DEVANAGARI_RE = /[\u0900-\u097f]/;
428
453
 
429
454
  // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
430
455
  // Digraphs must be listed before single chars so they match first.
@@ -522,7 +547,75 @@ function arabicLightStem(word: string): string {
522
547
  return result;
523
548
  }
524
549
 
525
- // ── Cross-lingual tech term dictionary ────────────────────────────────────────
550
+ /**
551
+ * Hebrew light stemmer — strips the definite article and common prefix particles
552
+ * that attach directly to words (no space) in Hebrew.
553
+ *
554
+ * Handles:
555
+ * ה (ha-) — definite article: הפריסה → פריסה
556
+ * ו (ve-/u-) — conjunction “and”: ופריסה → פריסה
557
+ * ב (be-/bi-) — preposition “in”: בסביבה → סביבה
558
+ * ל (le-/li-) — preposition “for”: לשרת → שרת
559
+ * מ (mi-/me-) — preposition “from”: מהשרת → השרת → שרת
560
+ * כ (ke-/ki-) — preposition “like”: כשרת → שרת
561
+ *
562
+ * Iterative (max 3 passes): מהפריסה → הפריסה → פריסה.
563
+ * Only strips when result is still ≥ 3 chars.
564
+ */
565
+ function hebrewLightStem(word: string): string {
566
+ let result = word;
567
+ for (let i = 0; i < 3; i++) {
568
+ const prev = result;
569
+ if (result.length > 3 && 'הובלמכ'.includes(result[0]) && HEBREW_CHAR_RE.test(result[1])) {
570
+ result = result.slice(1);
571
+ }
572
+ if (result === prev) break;
573
+ }
574
+ return result;
575
+ }
576
+
577
+ /**
578
+ * Farsi/Persian light stemmer — handles Persian morphology.
579
+ * Strips:
580
+ * \u0647\u0627 / \u0647\u0627\u06cc (ha/haye) — plural suffixes: \u0633\u0631\u0648\u0631\u0647\u0627 \u2192 \u0633\u0631\u0648\u0631
581
+ * \u0645\u06cc\u200c / \u0645\u06cc (mi-) — present-tense prefix: \u0645\u06cc\u200c\u06a9\u0646\u062f \u2192 \u06a9\u0646\u062f
582
+ * \u0646\u0645\u06cc\u200c (nami-) — negated present: \u0646\u0645\u06cc\u200c\u0634\u0648\u062f \u2192 \u0634\u0648\u062f
583
+ * Only strips when the result is still \u2265 3 chars.
584
+ */
585
+ function farsiLightStem(word: string): string {
586
+ let result = word;
587
+ // Suffixes first (longest first)
588
+ if (result.endsWith('\u0647\u0627\u06cc') && result.length > 5) result = result.slice(0, -3);
589
+ else if (result.endsWith('\u0647\u0627') && result.length > 4) result = result.slice(0, -2);
590
+ // Prefixes
591
+ if (result.startsWith('\u0646\u0645\u06cc\u200c') && result.length > 5) result = result.slice(4);
592
+ else if (result.startsWith('\u0645\u06cc\u200c') && result.length > 4) result = result.slice(3);
593
+ else if (result.startsWith('\u0645\u06cc') && result.length > 4) result = result.slice(2);
594
+ return result;
595
+ }
596
+
597
+ /**
598
+ * Hindi light stemmer — strips the most common inflectional suffixes.
599
+ * Handles basic verb forms and oblique plural markers.
600
+ * Avoids over-stemming loanwords (most tech terms in Hindi are English loanwords).
601
+ * Only strips when result is still \u2265 3 chars.
602
+ */
603
+ function hindiLightStem(word: string): string {
604
+ const result = word;
605
+ // Infinitive / verb suffixes (longest first to avoid partial match)
606
+ if (result.endsWith('\u0928\u093e') && result.length > 4) return result.slice(0, -2); // \u0928\u093e (nā) infinitive
607
+ if (result.endsWith('\u0928\u0947') && result.length > 4) return result.slice(0, -2); // \u0928\u0947 (ne) ergative
608
+ if (result.endsWith('\u0928\u0940') && result.length > 4) return result.slice(0, -2); // \u0928\u0940 (nī) fem infinitive
609
+ if (result.endsWith('\u0924\u093e') && result.length > 4) return result.slice(0, -2); // \u0924\u093e (tā) m present participle
610
+ if (result.endsWith('\u0924\u0940') && result.length > 4) return result.slice(0, -2); // \u0924\u0940 (tī) f present participle
611
+ if (result.endsWith('\u0915\u0930') && result.length > 4) return result.slice(0, -2); // \u0915\u0930 (kar) conjunctive
612
+ if (result.endsWith('\u0913\u0902') && result.length > 4) return result.slice(0, -2); // \u0913\u0902 (oṃ) oblique plural
613
+ if (result.endsWith('\u0907\u092f\u093e\u0902') && result.length > 5) return result.slice(0, -4); // \u0907\u092f\u093e\u0902 (iyāṃ) f plural
614
+ if (result.endsWith('\u0940\u092f\u093e\u0902') && result.length > 5) return result.slice(0, -4); // \u0940\u092f\u093e\u0902 (īyāṃ)
615
+ return result;
616
+ }
617
+
618
+
526
619
  // Maps tokens (English, Japanese katakana, Chinese, Korean, Arabic, Hebrew)
527
620
  // to their equivalents in other languages. Used to expand tokens at search time
528
621
  // so "deploy" finds documents containing "デプロイ" / "部署" / "배포", and vice versa.
@@ -530,27 +623,30 @@ function arabicLightStem(word: string): string {
530
623
  // Keys must be lowercase. CJK values will be bigram-expanded at use time.
531
624
  const CROSS_LINGUAL_MAP = new Map<string, string[]>([
532
625
  // English → all
533
- ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação']],
534
- ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação']],
626
+ ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação']],
627
+ ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação']],
535
628
  ['container', ['コンテナ', '容器', '컨테이너', 'docker']],
536
- ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת']],
537
- ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات', 'datenbank', 'base de données', 'base de datos', 'banco de dados']],
538
- ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון']],
539
- ['error', ['エラー', '错误', '오류', 'خطأ', 'שגיאה', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro']],
540
- ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل']],
541
- ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir']],
542
- ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'bauen', 'construire', 'construir', 'costruire']],
543
- ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'testen', 'tester', 'probar', 'testare', 'testar']],
544
- ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação']],
545
- ['authentication', ['auth', 'مصادقة', 'אימות', '認証', '认证', '인증', 'login', 'oauth']],
629
+ ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת', 'سرور', 'सर्वर']],
630
+ ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات', 'datenbank', 'پایگاه داده', 'डेटाबेس', 'base de données', 'base de datos', 'banco de dados']],
631
+ ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון', 'کش', 'कैश']],
632
+ ['error', ['エラー', '错误', '오류', 'خطأ', 'شגיאה', 'خطا', 'त्रुटि', 'गलती', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro']],
633
+ ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل', 'اشکال', 'बग']],
634
+ ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'رفع', 'सुधार', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir']],
635
+ ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'ساخت', 'निर्माण', 'bauen', 'construire', 'construir', 'costruire']],
636
+ ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'آزمایش', 'परीक्षण', 'testen', 'tester', 'probar', 'testare', 'testar']],
637
+ ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação']],
638
+ ['authentication', ['auth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', '認証', '认证', '인증', 'login', 'oauth']],
639
+ ['environment', ['環境', '环境', '환경', 'env', 'بيئة', 'סביבה', 'umgebung', 'environnement', 'entorno', 'ambiente']],
640
+ ['secret', ['key', 'token', 'password', 'jwt', 'مفتاح', 'מפתח', 'geheimnis', 'schlüssel', 'clave', 'clé', 'chiave', 'chave']],
641
+ ['problem', ['error', 'issue', 'bug', 'failure', 'مشكلة', 'بعيا', '問題', '問題', '오류', 'fehler', 'problème', 'problema']],
546
642
  ['kubernetes', ['クベルネテス', 'k8s', 'kube']],
547
- ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'netzwerk', 'réseau', 'red', 'rete', 'rede']],
643
+ ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'شبکه', 'नेटवर्क', 'netzwerk', 'réseau', 'red', 'rete', 'rede']],
548
644
  ['timeout', ['タイムアウト', '超时', '타임아웃', 'مهلة']],
549
- ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'speicher', 'mémoire', 'memoria', 'memória']],
550
- ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração']],
551
- ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'installieren', 'installer', 'instalar', 'installare']],
552
- ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar']],
553
- ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'protokoll', 'protokolle', 'journal', 'registro']],
645
+ ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'حافظه', 'मेमोरी', 'speicher', 'mémoire', 'memoria', 'memória']],
646
+ ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'پیکربندی', 'कॉन्फ़िग', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração']],
647
+ ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'نصب', 'इंस्टॉल', 'installieren', 'installer', 'instalar', 'installare']],
648
+ ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'به‌روزرسانی', 'अपडेट', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar']],
649
+ ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'ثبت', 'लॉग', 'protokoll', 'protokolle', 'journal', 'registro']],
554
650
  ['port', ['ポート', '端口', '포트', 'منفذ', 'פורט']],
555
651
  ['file', ['ファイル', '文件', '파일', 'ملف', 'קובץ']],
556
652
  ['image', ['イメージ', '镜像', '이미지', 'صورة', 'תמונה', 'abbild', 'imagen', 'imagem']],
@@ -563,10 +659,10 @@ const CROSS_LINGUAL_MAP = new Map<string, string[]>([
563
659
  ['token', ['トークン', '令牌', '토큰', 'jwt', 'secret', 'رمز', 'אסימון']],
564
660
  ['health', ['ヘルス', '健康', '헬스', 'healthcheck', 'probe', 'صحة']],
565
661
  ['migration', ['マイグレーション', '迁移', '마이그레이션', 'migrate', 'هجرة']],
566
- ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança']],
567
- ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar']],
568
- ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho']],
569
- ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור', 'verbindung', 'connexion', 'conexión', 'connessione', 'conexão']],
662
+ ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'پشتیبان‌گیری', 'बैकअप', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança']],
663
+ ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'نظارت', 'निगरानी', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar']],
664
+ ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'عملکرد', 'प्रदर्शन', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho']],
665
+ ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור', 'اتصال', 'कनेक्शन', 'verbindung', 'connexion', 'conexión', 'connessione', 'conexão']],
570
666
  ['queue', ['キュー', '队列', '큐', 'طابور', 'warteschlange', 'file d\'attente', 'cola', 'coda', 'fila']],
571
667
  ['redis', ['レディス', '레디스']],
572
668
  ['nginx', ['エンジンエックス']],
@@ -580,10 +676,10 @@ const CROSS_LINGUAL_MAP = new Map<string, string[]>([
580
676
  ['git', ['version control', 'repo', 'repository', 'commit', 'push', 'pull']],
581
677
  ['ci', ['pipeline', 'github actions', 'gitlab', 'jenkins', 'build']],
582
678
  ['cd', ['deploy', 'deployment', 'release']],
583
- ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'debuggen', 'déboguer', 'depurar']],
584
- ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'absturz', 'panne', 'plantage', 'caída', 'falha']],
585
- ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש', 'neustart', 'redémarrer', 'reiniciar', 'riavviare']],
586
- ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão']],
679
+ ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'اشکال‌زدایی', 'डीबग', 'debuggen', 'déboguer', 'depurar']],
680
+ ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'خرابی', 'क्रैश', 'absturz', 'panne', 'plantage', 'caída', 'falha']],
681
+ ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש', 'راه‌اندازی مجدد', 'पुنरारंभ', 'neustart', 'redémarrer', 'reiniciar', 'riavviare']],
682
+ ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'مجوز', 'अनुमति', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão']],
587
683
  // Japanese katakana → English
588
684
  ['デプロイ', ['deploy', 'deployment']],
589
685
  ['コンテナ', ['container', 'docker']],
@@ -720,6 +816,13 @@ const CROSS_LINGUAL_MAP = new Map<string, string[]>([
720
816
  ['قرص', ['disk', 'storage']],
721
817
  ['خادم', ['server']],
722
818
  ['برنامج', ['application', 'software', 'app']],
819
+ // Arabic terms from blog examples
820
+ ['مشكلة', ['error', 'issue', 'bug', 'problem', 'failure']],
821
+ ['بيئة', ['environment', 'env']],
822
+ ['متغيرات', ['variables', 'env', 'environment']],
823
+ ['إنتاج', ['production', 'prod']],
824
+ ['سري', ['secret', 'key', 'token']],
825
+ ['مفتاح سري', ['secret', 'key']],
723
826
  // Hebrew → English
724
827
  ['שגיאה', ['error']],
725
828
  ['תיקון', ['fix']],
@@ -755,6 +858,76 @@ const CROSS_LINGUAL_MAP = new Map<string, string[]>([
755
858
  ['ממשק תכנות', ['api', 'interface']],
756
859
  ['ביצועים', ['performance']],
757
860
  ['אחסון', ['storage', 'disk']],
861
+ // Hebrew terms from blog examples
862
+ ['פריסה', ['deploy', 'deployment']],
863
+ ['בעיה', ['error', 'issue', 'bug', 'problem']],
864
+ ['סביבה', ['environment', 'env']],
865
+ ['ייצור', ['production', 'prod']],
866
+ ['סודי', ['secret', 'key', 'token']],
867
+ ['מפתח סודי', ['secret', 'key']],
868
+
869
+ // ── Farsi/Persian → English ───────────────────────────────────────────────
870
+ ['استقرار', ['deploy', 'deployment']],
871
+ ['خطا', ['error', 'err']], // Farsi: خطا vs Arabic: خطأ
872
+ ['اشکال', ['bug', 'issue', 'error']],
873
+ ['رفع اشکال', ['fix', 'debug', 'debugging']],
874
+ ['ساخت', ['build']],
875
+ ['آزمایش', ['test']],
876
+ ['احراز هویت', ['auth', 'authentication']],
877
+ ['پیکربندی', ['config', 'configuration']],
878
+ ['نصب', ['install']],
879
+ ['به‌روزرسانی', ['update', 'upgrade']],
880
+ ['ثبت', ['log']],
881
+ ['سرور', ['server']],
882
+ ['پایگاه داده', ['database', 'db']],
883
+ ['شبکه', ['network']], // Farsi شبکه vs Arabic شبكة
884
+ ['حافظه', ['memory', 'ram']], // Farsi حافظه vs Arabic ذاكرة
885
+ ['کش', ['cache']],
886
+ ['اشکال‌زدایی', ['debug', 'debugging']],
887
+ ['خرابی', ['crash', 'failure', 'error']],
888
+ ['راه‌اندازی مجدد', ['restart', 'reboot']],
889
+ ['مجوز', ['permission', 'access']],
890
+ ['پشتیبان‌گیری', ['backup']],
891
+ ['عملکرد', ['performance']],
892
+ ['مشکل', ['problem', 'issue', 'error']],
893
+ ['سرویس', ['service']],
894
+ ['کلید', ['key', 'token', 'secret']],
895
+ ['محیط', ['environment', 'env']],
896
+ ['تولید', ['production', 'prod']],
897
+ ['رمز', ['secret', 'key', 'token']],
898
+ ['اتصال', ['connection', 'conn']],
899
+
900
+ // ── Hindi (Devanagari) → English ──────────────────────────────────────────
901
+ ['तैनाती', ['deploy', 'deployment']],
902
+ ['त्रुटि', ['error', 'err']],
903
+ ['गलती', ['error', 'bug', 'issue']],
904
+ ['सुधार', ['fix', 'bugfix']],
905
+ ['निर्माण', ['build']],
906
+ ['परीक्षण', ['test']],
907
+ ['प्रमाणीकरण', ['auth', 'authentication']],
908
+ ['विन्यास', ['config', 'configuration']],
909
+ ['कॉन्फ़िगरेशन', ['config', 'configuration']],
910
+ ['इंस्टॉल', ['install']],
911
+ ['अपडेट', ['update', 'upgrade']],
912
+ ['लॉग', ['log']],
913
+ ['सर्वर', ['server']],
914
+ ['डेटाबेस', ['database', 'db']],
915
+ ['नेटवर्क', ['network']],
916
+ ['मेमोरी', ['memory', 'ram']],
917
+ ['कैश', ['cache']],
918
+ ['डीबग', ['debug', 'debugging']],
919
+ ['क्रैश', ['crash', 'panic']],
920
+ ['पुनरारंभ', ['restart', 'reboot']],
921
+ ['अनुमति', ['permission', 'access']],
922
+ ['बैकअप', ['backup']],
923
+ ['प्रदर्शन', ['performance']],
924
+ ['समस्या', ['problem', 'issue', 'error']],
925
+ ['सेवा', ['service']],
926
+ ['कनेक्शन', ['connection', 'conn']],
927
+ ['पासवर्ड', ['password']],
928
+ ['टोकन', ['token', 'secret']],
929
+ ['वातावरण', ['environment', 'env']],
930
+ ['उत्पादन', ['production', 'prod']],
758
931
 
759
932
  // ── European languages → English ─────────────────────────────────────────
760
933
  // German (Deutsch) → English
@@ -959,8 +1132,11 @@ function expandCrossLingual(token: string): string[] {
959
1132
  * Features:
960
1133
  * • CJK → character bigrams
961
1134
  * • Katakana → additionally emits Hepburn romaji tokens (enables romaji queries)
962
- * • Arabic/Hebrew → word tokenization with light Arabic stemming
963
- * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE)
1135
+ * • Arabic → word tokenization + iterative prefix stemming (ال/و/ب/ل/ف/ك)
1136
+ * • Hebrew → word tokenization + iterative prefix stemming (ה/ו/ב/ל/מ/כ)
1137
+ * • Farsi → word tokenization + suffix/prefix stemming (ها/های plural, می prefix)
1138
+ * • Hindi → Devanagari word tokenization + light suffix stemming
1139
+ * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE↔FA↔HI↔DE↔FR↔ES↔IT↔PT)
964
1140
  */
965
1141
  function tokenize(text: string): string[] {
966
1142
  const tokens: string[] = [];
@@ -998,11 +1174,26 @@ function tokenize(text: string): string[] {
998
1174
  }
999
1175
  }
1000
1176
  } else if (RTL_RE.test(seg)) {
1001
- // RTL segment (Arabic / Hebrew) — space-separated words with light stemming
1177
+ // RTL segment — 3-way branch: Farsi / Hebrew / Arabic
1002
1178
  const words = seg.trim().split(/\s+/);
1179
+ let stemFn: (w: string) => string;
1180
+ if (FARSI_CHAR_RE.test(seg)) stemFn = farsiLightStem;
1181
+ else if (HEBREW_CHAR_RE.test(seg)) stemFn = hebrewLightStem;
1182
+ else stemFn = arabicLightStem;
1003
1183
  for (const w of words) {
1004
1184
  if (w.length <= 1 || STOPWORDS.has(w)) continue;
1005
- const stemmed = arabicLightStem(w);
1185
+ const stemmed = stemFn(w);
1186
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1187
+ }
1188
+ } else if (DEVANAGARI_RE.test(seg)) {
1189
+ // Devanagari segment (Hindi / Marathi) — naturally space-separated
1190
+ // Words don't need bigram expansion; stems reduce inflected forms.
1191
+ const words = seg.trim().split(/[\s\u200c\u200d]+/); // handle zero-width joiners
1192
+ for (const w of words) {
1193
+ // Keep Devanagari chars + digits; discard pure Latin portions (e.g. embedded numbers)
1194
+ const deva = w.replace(/[^\u0900-\u097f0-9]/g, '');
1195
+ if (deva.length <= 1 || STOPWORDS.has(deva)) continue;
1196
+ const stemmed = hindiLightStem(deva);
1006
1197
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1007
1198
  }
1008
1199
  } else {
@@ -1036,8 +1227,15 @@ function tokenize(text: string): string[] {
1036
1227
  if (!STOPWORDS.has(last)) expansions.push(last);
1037
1228
  }
1038
1229
  } else if (RTL_RE.test(syn)) {
1039
- // RTL synonym
1040
- const stemmed = arabicLightStem(syn);
1230
+ // RTL synonym — apply correct stemmer per language
1231
+ const rtlStem = FARSI_CHAR_RE.test(syn) ? farsiLightStem
1232
+ : HEBREW_CHAR_RE.test(syn) ? hebrewLightStem
1233
+ : arabicLightStem;
1234
+ const stemmed = rtlStem(syn);
1235
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1236
+ } else if (DEVANAGARI_RE.test(syn)) {
1237
+ // Devanagari synonym (Hindi)
1238
+ const stemmed = hindiLightStem(syn);
1041
1239
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1042
1240
  } else {
1043
1241
  // Latin synonym — split in case of multi-word (e.g. "load balancer")