@cachly-dev/mcp-server 0.5.36 → 0.5.37

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/dist/index.d.ts CHANGED
@@ -6,10 +6,33 @@
6
6
  * Keeps the index small and scores meaningful.
7
7
  */
8
8
  declare const STOPWORDS: Set<string>;
9
+ /**
10
+ * Convert a katakana string to Hepburn romaji.
11
+ * Handles digraphs, geminate consonants (ッ), and long vowel marks (ー).
12
+ */
13
+ declare function katakanaToRomaji(kata: string): string;
14
+ /**
15
+ * Arabic light stemmer — strips definite article and common prefix particles.
16
+ * Handles: ال (al-), و (wa-), ب (bi-), ل (li-), ف (fa-), ك (ka-).
17
+ * Only strips when the result is still ≥ 3 chars to avoid over-stemming.
18
+ */
19
+ declare function arabicLightStem(word: string): string;
20
+ declare const CROSS_LINGUAL_MAP: Map<string, string[]>;
21
+ /**
22
+ * Expand a token to its cross-lingual synonyms.
23
+ * Returns synonyms to be added to the token stream (pre-tokenized).
24
+ * CJK synonyms are NOT pre-bigram'd here — caller handles that.
25
+ */
26
+ declare function expandCrossLingual(token: string): string[];
9
27
  /**
10
28
  * Tokenize text into meaningful keywords.
11
- * Handles accented Latin characters and CJK (Chinese, Japanese, Korean).
12
- * CJK text is split into character bigrams (the standard for indexing logographic languages).
29
+ * Handles accented Latin, RTL (Arabic, Hebrew), and CJK (Chinese, Japanese, Korean).
30
+ *
31
+ * Features:
32
+ * • CJK → character bigrams
33
+ * • Katakana → additionally emits Hepburn romaji tokens (enables romaji queries)
34
+ * • Arabic/Hebrew → word tokenization with light Arabic stemming
35
+ * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE)
13
36
  */
14
37
  declare function tokenize(text: string): string[];
15
38
  /**
@@ -33,4 +56,4 @@ declare function levenshtein(a: string, b: string): number;
33
56
  declare function extractTimestamp(content: string): number | undefined;
34
57
  /** Recency multiplier: 1.0 for now, 0.5 after half-life days, exponential decay. */
35
58
  declare function recencyBoost(timestampMs: number | undefined): number;
36
- export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS };
59
+ export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
package/dist/index.js CHANGED
@@ -288,8 +288,11 @@ function embedProviderHint() {
288
288
  // • Recency boost — newer entries rank higher (exponential decay, 7-day half-life)
289
289
  // • Multi-query splitting — numbered lists, semicolons, conjunctions
290
290
  // • Fuzzy matching with Levenshtein distance ≤ 2
291
- // • Multilingual stopwords (EN, DE, FR, ES, IT, PT, ZH, JA, KO)
291
+ // • Multilingual stopwords (EN, DE, FR, ES, IT, PT, ZH, JA, KO, AR, HE)
292
292
  // • CJK character bigram extraction — handles Chinese, Japanese, Korean
293
+ // • RTL language support (Arabic, Hebrew) — word tokenization + light stemming
294
+ // • Romanization matching — katakana → romaji tokens for romaji queries
295
+ // • Cross-language retrieval — tech term synonyms EN↔JA↔ZH↔KO↔AR↔HE
293
296
  // • Pipeline Redis reads for performance
294
297
  //
295
298
  /**
@@ -386,14 +389,330 @@ const STOPWORDS = new Set([
386
389
  '이', '가', '은', '는', '을', '를', '의', '에', '와', '과', '도',
387
390
  '로', '에서', '한', '하', '있', '없', '것', '수', '않', '들',
388
391
  '이다', '하다', '이고', '이며', '라고', '에게', '에서', '으로',
392
+ // ── Arabic — high-frequency function words & particles ──
393
+ 'في', 'من', 'إلى', 'على', 'عن', 'مع', 'هذا', 'هذه', 'ذلك', 'تلك',
394
+ 'الذي', 'التي', 'الذين', 'اللواتي', 'هو', 'هي', 'هم', 'هن', 'نحن',
395
+ 'أنت', 'أنتم', 'أنا', 'كان', 'كانت', 'كانوا', 'يكون', 'تكون',
396
+ 'أن', 'إن', 'لأن', 'لكن', 'أو', 'بل', 'ثم', 'حتى', 'إذا', 'كي',
397
+ 'قد', 'لقد', 'لم', 'لن', 'ما', 'لا', 'ليس', 'غير', 'بعض', 'كل',
398
+ 'جميع', 'أي', 'كيف', 'متى', 'أين', 'لماذا', 'ماذا', 'هل', 'ال',
399
+ 'و', 'ف', 'ب', 'ل', 'ك', 'يا', 'أم', 'إلا', 'عند', 'بين',
400
+ // ── Hebrew — common particles, pronouns, conjunctions ──
401
+ 'של', 'את', 'אל', 'על', 'עם', 'לא', 'הוא', 'היא', 'הם', 'הן',
402
+ 'אני', 'אתה', 'אנחנו', 'אתם', 'כי', 'אם', 'אבל', 'גם', 'כבר',
403
+ 'רק', 'עוד', 'יש', 'אין', 'מה', 'זה', 'זאת', 'אלה', 'כל', 'כן',
404
+ 'לו', 'לה', 'להם', 'בו', 'בה', 'בהם', 'שם', 'כך', 'כן', 'מי',
405
+ 'אשר', 'אחרי', 'לפני', 'תחת', 'בין', 'מאז', 'עד', 'כמו', 'אז',
389
406
  ]);
390
407
  // CJK Unicode ranges used for bigram extraction
391
408
  const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
392
409
  const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+|[^\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+)/g;
410
+ // RTL Unicode ranges: Arabic (U+0600–U+06FF + extended), Hebrew (U+0590–U+05FF)
411
+ const RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufb4f\ufb50-\ufdff\ufe70-\ufeff]/;
412
+ // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
413
+ // Digraphs must be listed before single chars so they match first.
414
+ const KATA_ROMAJI_TABLE = [
415
+ // Special combinations for loanwords
416
+ ['ファ', 'fa'], ['フィ', 'fi'], ['フェ', 'fe'], ['フォ', 'fo'],
417
+ ['ティ', 'ti'], ['ディ', 'di'], ['トゥ', 'tu'], ['ドゥ', 'du'],
418
+ ['ウィ', 'wi'], ['ウェ', 'we'], ['ウォ', 'wo'],
419
+ ['チェ', 'che'], ['ジェ', 'je'], ['シェ', 'she'],
420
+ ['イェ', 'ye'], ['ヴァ', 'va'], ['ヴィ', 'vi'], ['ヴェ', 've'], ['ヴォ', 'vo'],
421
+ // Digraphs (2-char → romaji)
422
+ ['キャ', 'kya'], ['キュ', 'kyu'], ['キョ', 'kyo'],
423
+ ['シャ', 'sha'], ['シュ', 'shu'], ['ショ', 'sho'],
424
+ ['チャ', 'cha'], ['チュ', 'chu'], ['チョ', 'cho'],
425
+ ['ニャ', 'nya'], ['ニュ', 'nyu'], ['ニョ', 'nyo'],
426
+ ['ヒャ', 'hya'], ['ヒュ', 'hyu'], ['ヒョ', 'hyo'],
427
+ ['ミャ', 'mya'], ['ミュ', 'myu'], ['ミョ', 'myo'],
428
+ ['リャ', 'rya'], ['リュ', 'ryu'], ['リョ', 'ryo'],
429
+ ['ギャ', 'gya'], ['ギュ', 'gyu'], ['ギョ', 'gyo'],
430
+ ['ジャ', 'ja'], ['ジュ', 'ju'], ['ジョ', 'jo'],
431
+ ['ビャ', 'bya'], ['ビュ', 'byu'], ['ビョ', 'byo'],
432
+ ['ピャ', 'pya'], ['ピュ', 'pyu'], ['ピョ', 'pyo'],
433
+ // Single chars
434
+ ['ア', 'a'], ['イ', 'i'], ['ウ', 'u'], ['エ', 'e'], ['オ', 'o'],
435
+ ['カ', 'ka'], ['キ', 'ki'], ['ク', 'ku'], ['ケ', 'ke'], ['コ', 'ko'],
436
+ ['サ', 'sa'], ['シ', 'shi'], ['ス', 'su'], ['セ', 'se'], ['ソ', 'so'],
437
+ ['タ', 'ta'], ['チ', 'chi'], ['ツ', 'tsu'], ['テ', 'te'], ['ト', 'to'],
438
+ ['ナ', 'na'], ['ニ', 'ni'], ['ヌ', 'nu'], ['ネ', 'ne'], ['ノ', 'no'],
439
+ ['ハ', 'ha'], ['ヒ', 'hi'], ['フ', 'fu'], ['ヘ', 'he'], ['ホ', 'ho'],
440
+ ['マ', 'ma'], ['ミ', 'mi'], ['ム', 'mu'], ['メ', 'me'], ['モ', 'mo'],
441
+ ['ヤ', 'ya'], ['ユ', 'yu'], ['ヨ', 'yo'],
442
+ ['ラ', 'ra'], ['リ', 'ri'], ['ル', 'ru'], ['レ', 're'], ['ロ', 'ro'],
443
+ ['ワ', 'wa'], ['ヲ', 'wo'], ['ン', 'n'],
444
+ // Voiced
445
+ ['ガ', 'ga'], ['ギ', 'gi'], ['グ', 'gu'], ['ゲ', 'ge'], ['ゴ', 'go'],
446
+ ['ザ', 'za'], ['ジ', 'ji'], ['ズ', 'zu'], ['ゼ', 'ze'], ['ゾ', 'zo'],
447
+ ['ダ', 'da'], ['ヂ', 'ji'], ['ヅ', 'zu'], ['デ', 'de'], ['ド', 'do'],
448
+ ['バ', 'ba'], ['ビ', 'bi'], ['ブ', 'bu'], ['ベ', 'be'], ['ボ', 'bo'],
449
+ ['パ', 'pa'], ['ピ', 'pi'], ['プ', 'pu'], ['ペ', 'pe'], ['ポ', 'po'],
450
+ ['ヴ', 'v'],
451
+ // Long vowel / small chars
452
+ ['ー', ''], ['ァ', 'a'], ['ィ', 'i'], ['ゥ', 'u'], ['ェ', 'e'], ['ォ', 'o'],
453
+ ['ッ', ''], // handled separately (doubles next consonant)
454
+ ];
455
+ const KATA_MAP = new Map(KATA_ROMAJI_TABLE);
456
+ /**
457
+ * Convert a katakana string to Hepburn romaji.
458
+ * Handles digraphs, geminate consonants (ッ), and long vowel marks (ー).
459
+ */
460
+ function katakanaToRomaji(kata) {
461
+ let result = '';
462
+ let i = 0;
463
+ while (i < kata.length) {
464
+ // Geminate consonant: ッ doubles the following consonant
465
+ if (kata[i] === 'ッ' && i + 1 < kata.length) {
466
+ const next = KATA_MAP.get(kata[i + 1]) ?? KATA_MAP.get(kata[i + 1] + kata[i + 2]) ?? '';
467
+ if (next.length > 0)
468
+ result += next[0]; // double first consonant
469
+ i++;
470
+ continue;
471
+ }
472
+ // Try 2-char digraph first
473
+ if (i + 1 < kata.length) {
474
+ const two = kata[i] + kata[i + 1];
475
+ const r2 = KATA_MAP.get(two);
476
+ if (r2 !== undefined) {
477
+ result += r2;
478
+ i += 2;
479
+ continue;
480
+ }
481
+ }
482
+ // Single char
483
+ const r1 = KATA_MAP.get(kata[i]);
484
+ if (r1 !== undefined)
485
+ result += r1;
486
+ i++;
487
+ }
488
+ return result;
489
+ }
490
+ /**
491
+ * Arabic light stemmer — strips definite article and common prefix particles.
492
+ * Handles: ال (al-), و (wa-), ب (bi-), ل (li-), ف (fa-), ك (ka-).
493
+ * Only strips when the result is still ≥ 3 chars to avoid over-stemming.
494
+ */
495
+ function arabicLightStem(word) {
496
+ if (word.startsWith('ال') && word.length > 4)
497
+ return word.slice(2);
498
+ if (word.length > 3 && 'وبلفك'.includes(word[0]) && RTL_RE.test(word[1]))
499
+ return word.slice(1);
500
+ return word;
501
+ }
502
+ // ── Cross-lingual tech term dictionary ────────────────────────────────────────
503
+ // Maps tokens (English, Japanese katakana, Chinese, Korean, Arabic, Hebrew)
504
+ // to their equivalents in other languages. Used to expand tokens at search time
505
+ // so "deploy" finds documents containing "デプロイ" / "部署" / "배포", and vice versa.
506
+ //
507
+ // Keys must be lowercase. CJK values will be bigram-expanded at use time.
508
+ const CROSS_LINGUAL_MAP = new Map([
509
+ // English → all
510
+ ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying']],
511
+ ['deployment', ['デプロイ', '部署', '배포', 'deploy']],
512
+ ['container', ['コンテナ', '容器', '컨테이너', 'docker']],
513
+ ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת']],
514
+ ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات']],
515
+ ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון']],
516
+ ['error', ['エラー', '错误', '오류', 'خطأ', 'שגיאה', 'exception', 'err']],
517
+ ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل']],
518
+ ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון']],
519
+ ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה']],
520
+ ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה']],
521
+ ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות']],
522
+ ['kubernetes', ['クベルネテス', 'k8s', 'kube']],
523
+ ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת']],
524
+ ['timeout', ['タイムアウト', '超时', '타임아웃', 'مهلة']],
525
+ ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון']],
526
+ ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות']],
527
+ ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה']],
528
+ ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון']],
529
+ ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן']],
530
+ ['port', ['ポート', '端口', '포트', 'منفذ', 'פורט']],
531
+ ['file', ['ファイル', '文件', '파일', 'ملف', 'קובץ']],
532
+ ['image', ['イメージ', '镜像', '이미지', 'صورة', 'תמונה']],
533
+ ['volume', ['ボリューム', '卷', '볼륨']],
534
+ ['cluster', ['クラスター', '集群', '클러스터']],
535
+ ['node', ['ノード', '节点', '노드']],
536
+ ['service', ['サービス', '服务', '서비스', 'svc', 'خدمة', 'שירות']],
537
+ ['certificate', ['証明書', '证书', '인증서', 'cert', 'ssl', 'tls', 'شهادة', 'תעודה']],
538
+ ['password', ['パスワード', '密码', '비밀번호', 'passwd', 'pwd', 'كلمة المرور', 'סיסמה']],
539
+ ['token', ['トークン', '令牌', '토큰', 'jwt', 'secret', 'رمز', 'אסימון']],
540
+ ['health', ['ヘルス', '健康', '헬스', 'healthcheck', 'probe', 'صحة']],
541
+ ['migration', ['マイグレーション', '迁移', '마이그레이션', 'migrate', 'هجرة']],
542
+ ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי']],
543
+ ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור']],
544
+ ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput']],
545
+ ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור']],
546
+ ['queue', ['キュー', '队列', '큐', 'طابور']],
547
+ ['redis', ['レディス', '레디스']],
548
+ ['nginx', ['エンジンエックス']],
549
+ ['linux', ['リナックス', 'لينكس']],
550
+ ['api', ['エーピーアイ', '接口', 'endpoint', 'واجهة برمجية', 'ממשק']],
551
+ ['healthcheck', ['ヘルスチェック', 'health', 'probe', 'فحص الصحة']],
552
+ ['loadbalancer', ['ロードバランサー', '负载均衡', '로드밸런서', 'lb']],
553
+ ['ssl', ['tls', 'https', 'certificate', 'cert']],
554
+ ['tls', ['ssl', 'https', 'certificate', 'cert']],
555
+ ['docker', ['container', 'コンテナ', '容器', '컨테이너', 'دوكر']],
556
+ ['git', ['version control', 'repo', 'repository', 'commit', 'push', 'pull']],
557
+ ['ci', ['pipeline', 'github actions', 'gitlab', 'jenkins', 'build']],
558
+ ['cd', ['deploy', 'deployment', 'release']],
559
+ ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים']],
560
+ ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار']],
561
+ ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש']],
562
+ ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה']],
563
+ // Japanese katakana → English
564
+ ['デプロイ', ['deploy', 'deployment']],
565
+ ['コンテナ', ['container', 'docker']],
566
+ ['サーバー', ['server']],
567
+ ['サーバ', ['server']],
568
+ ['データベース', ['database', 'db']],
569
+ ['キャッシュ', ['cache']],
570
+ ['エラー', ['error', 'err']],
571
+ ['バグ', ['bug', 'issue']],
572
+ ['ビルド', ['build']],
573
+ ['テスト', ['test']],
574
+ ['認証', ['auth', 'authentication']],
575
+ ['設定', ['config', 'configuration']],
576
+ ['インストール', ['install']],
577
+ ['アップデート', ['update', 'upgrade']],
578
+ ['ログ', ['log', 'logs']],
579
+ ['ポート', ['port']],
580
+ ['ファイル', ['file']],
581
+ ['イメージ', ['image']],
582
+ ['クラスター', ['cluster']],
583
+ ['ノード', ['node']],
584
+ ['サービス', ['service']],
585
+ ['パスワード', ['password']],
586
+ ['トークン', ['token']],
587
+ ['バックアップ', ['backup']],
588
+ ['モニター', ['monitor', 'monitoring']],
589
+ ['接続', ['connection']],
590
+ ['キュー', ['queue']],
591
+ ['ヘルスチェック', ['healthcheck', 'health check', 'health']],
592
+ ['ロードバランサー', ['loadbalancer', 'load balancer']],
593
+ ['デバッグ', ['debug']],
594
+ ['クラッシュ', ['crash', 'panic']],
595
+ ['再起動', ['restart', 'reboot']],
596
+ ['権限', ['permission', 'access']],
597
+ // Chinese → English
598
+ ['部署', ['deploy', 'deployment']],
599
+ ['容器', ['container', 'docker']],
600
+ ['服务器', ['server']],
601
+ ['数据库', ['database']],
602
+ ['缓存', ['cache']],
603
+ ['错误', ['error']],
604
+ ['修复', ['fix', 'bugfix']],
605
+ ['构建', ['build']],
606
+ ['测试', ['test']],
607
+ ['认证', ['auth']],
608
+ ['配置', ['config']],
609
+ ['安装', ['install']],
610
+ ['更新', ['update']],
611
+ ['日志', ['log']],
612
+ ['端口', ['port']],
613
+ ['镜像', ['image']],
614
+ ['集群', ['cluster']],
615
+ ['节点', ['node']],
616
+ ['服务', ['service']],
617
+ ['密码', ['password']],
618
+ ['令牌', ['token']],
619
+ ['备份', ['backup']],
620
+ ['监控', ['monitor']],
621
+ ['连接', ['connection']],
622
+ ['队列', ['queue']],
623
+ ['性能', ['performance']],
624
+ ['内存', ['memory']],
625
+ ['调试', ['debug']],
626
+ ['崩溃', ['crash']],
627
+ ['重启', ['restart']],
628
+ ['权限', ['permission']],
629
+ // Korean → English
630
+ ['배포', ['deploy', 'deployment']],
631
+ ['컨테이너', ['container']],
632
+ ['서버', ['server']],
633
+ ['데이터베이스', ['database']],
634
+ ['캐시', ['cache']],
635
+ ['오류', ['error']],
636
+ ['수정', ['fix']],
637
+ ['빌드', ['build']],
638
+ ['테스트', ['test']],
639
+ ['인증', ['auth']],
640
+ ['설정', ['config']],
641
+ ['설치', ['install']],
642
+ ['업데이트', ['update']],
643
+ ['로그', ['log']],
644
+ ['포트', ['port']],
645
+ ['이미지', ['image']],
646
+ ['클러스터', ['cluster']],
647
+ ['노드', ['node']],
648
+ ['서비스', ['service']],
649
+ ['토큰', ['token']],
650
+ ['백업', ['backup']],
651
+ ['모니터링', ['monitor']],
652
+ ['연결', ['connection']],
653
+ ['큐', ['queue']],
654
+ ['성능', ['performance']],
655
+ ['메모리', ['memory']],
656
+ ['디버그', ['debug']],
657
+ ['크래시', ['crash']],
658
+ ['재시작', ['restart']],
659
+ ['권한', ['permission']],
660
+ // Arabic → English
661
+ ['خطأ', ['error']],
662
+ ['إصلاح', ['fix']],
663
+ ['بناء', ['build']],
664
+ ['اختبار', ['test']],
665
+ ['مصادقة', ['auth']],
666
+ ['إعدادات', ['config']],
667
+ ['تثبيت', ['install']],
668
+ ['تحديث', ['update']],
669
+ ['سجل', ['log']],
670
+ ['صورة', ['image']],
671
+ ['خدمة', ['service']],
672
+ ['نسخ احتياطي', ['backup']],
673
+ ['مراقبة', ['monitor']],
674
+ ['اتصال', ['connection']],
675
+ ['ذاكرة', ['memory']],
676
+ ['تصحيح', ['debug']],
677
+ ['شبكة', ['network']],
678
+ // Hebrew → English
679
+ ['שגיאה', ['error']],
680
+ ['תיקון', ['fix']],
681
+ ['בנייה', ['build']],
682
+ ['בדיקה', ['test']],
683
+ ['אימות', ['auth']],
684
+ ['הגדרות', ['config']],
685
+ ['התקנה', ['install']],
686
+ ['עדכון', ['update']],
687
+ ['יומן', ['log']],
688
+ ['שרת', ['server']],
689
+ ['שירות', ['service']],
690
+ ['גיבוי', ['backup']],
691
+ ['ניטור', ['monitor']],
692
+ ['חיבור', ['connection']],
693
+ ['זיכרון', ['memory']],
694
+ ['רשת', ['network']],
695
+ ['תמונה', ['image']],
696
+ ['סיסמה', ['password']],
697
+ ['הרשאה', ['permission']],
698
+ ]);
699
+ /**
700
+ * Expand a token to its cross-lingual synonyms.
701
+ * Returns synonyms to be added to the token stream (pre-tokenized).
702
+ * CJK synonyms are NOT pre-bigram'd here — caller handles that.
703
+ */
704
+ function expandCrossLingual(token) {
705
+ return CROSS_LINGUAL_MAP.get(token) ?? [];
706
+ }
393
707
  /**
394
708
  * Tokenize text into meaningful keywords.
395
- * Handles accented Latin characters and CJK (Chinese, Japanese, Korean).
396
- * CJK text is split into character bigrams (the standard for indexing logographic languages).
709
+ * Handles accented Latin, RTL (Arabic, Hebrew), and CJK (Chinese, Japanese, Korean).
710
+ *
711
+ * Features:
712
+ * • CJK → character bigrams
713
+ * • Katakana → additionally emits Hepburn romaji tokens (enables romaji queries)
714
+ * • Arabic/Hebrew → word tokenization with light Arabic stemming
715
+ * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE)
397
716
  */
398
717
  function tokenize(text) {
399
718
  const tokens = [];
@@ -413,9 +732,37 @@ function tokenize(text) {
413
732
  if (!STOPWORDS.has(last))
414
733
  tokens.push(last);
415
734
  }
735
+ // Whole-segment cross-lingual lookup (for known tech terms like デプロイ, 部署, 배포)
736
+ // This ensures CJK full-words map to their English equivalents
737
+ const segSyns = expandCrossLingual(seg);
738
+ for (const syn of segSyns) {
739
+ if (!CJK_RE.test(syn)) {
740
+ syn.split(/\s+/).filter(w => w.length > 1 && !STOPWORDS.has(w)).forEach(w => tokens.push(w));
741
+ }
742
+ }
743
+ // Romanization: extract katakana-only portion and emit romaji tokens.
744
+ // This allows users to search "kontena" or "depuroi" to find katakana docs.
745
+ const kataOnly = seg.replace(/[^\u30a0-\u30ff]/g, '');
746
+ if (kataOnly.length >= 2) {
747
+ const romaji = katakanaToRomaji(kataOnly);
748
+ if (romaji.length >= 2 && romaji.length <= 40 && !STOPWORDS.has(romaji)) {
749
+ tokens.push(romaji);
750
+ }
751
+ }
752
+ }
753
+ else if (RTL_RE.test(seg)) {
754
+ // RTL segment (Arabic / Hebrew) — space-separated words with light stemming
755
+ const words = seg.trim().split(/\s+/);
756
+ for (const w of words) {
757
+ if (w.length <= 1 || STOPWORDS.has(w))
758
+ continue;
759
+ const stemmed = arabicLightStem(w);
760
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
761
+ tokens.push(stemmed);
762
+ }
416
763
  }
417
764
  else {
418
- // Latin / other — existing logic
765
+ // Latin / other — existing logic preserved exactly
419
766
  const words = seg
420
767
  .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
421
768
  .split(/\s+/)
@@ -423,6 +770,44 @@ function tokenize(text) {
423
770
  tokens.push(...words);
424
771
  }
425
772
  }
773
+ // ── Cross-lingual expansion ──────────────────────────────────────────────
774
+ // For each token, look up synonyms in other languages and add them.
775
+ // CJK synonyms are bigram-expanded inline; Latin/RTL synonyms added as-is.
776
+ const expansions = [];
777
+ const seen = new Set(tokens);
778
+ for (const tok of tokens) {
779
+ const syns = expandCrossLingual(tok);
780
+ for (const syn of syns) {
781
+ if (seen.has(syn))
782
+ continue;
783
+ seen.add(syn);
784
+ if (CJK_RE.test(syn)) {
785
+ // CJK synonym → expand to character bigrams
786
+ for (let i = 0; i < syn.length - 1; i++) {
787
+ const bg = syn[i] + syn[i + 1];
788
+ if (!STOPWORDS.has(syn[i]) && !STOPWORDS.has(bg))
789
+ expansions.push(bg);
790
+ }
791
+ if (syn.length >= 1) {
792
+ const last = syn[syn.length - 1];
793
+ if (!STOPWORDS.has(last))
794
+ expansions.push(last);
795
+ }
796
+ }
797
+ else if (RTL_RE.test(syn)) {
798
+ // RTL synonym
799
+ const stemmed = arabicLightStem(syn);
800
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
801
+ expansions.push(stemmed);
802
+ }
803
+ else {
804
+ // Latin synonym — split in case of multi-word (e.g. "load balancer")
805
+ const parts = syn.split(/\s+/).filter(w => w.length > 1 && !STOPWORDS.has(w));
806
+ expansions.push(...parts);
807
+ }
808
+ }
809
+ }
810
+ tokens.push(...expansions);
426
811
  return tokens;
427
812
  }
428
813
  /**
@@ -696,7 +1081,7 @@ async function keywordSearch(redis, patterns, query, topK = 10) {
696
1081
  return sorted.slice(0, topK);
697
1082
  }
698
1083
  // ── Exported for testing ──────────────────────────────────────────────────────
699
- export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS };
1084
+ export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
700
1085
  // ── Connection pool ───────────────────────────────────────────────────────────
701
1086
  /** Reuse Redis connections across tool calls (keyed by instance_id). */
702
1087
  const pool = new Map();