@cachly-dev/mcp-server 0.5.60 → 0.5.70

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.js CHANGED
@@ -47,7 +47,7 @@ import { Redis } from 'ioredis';
47
47
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
48
48
  const JWT = process.env.CACHLY_JWT ?? '';
49
49
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
50
- const CURRENT_VERSION = '0.5.60';
50
+ const CURRENT_VERSION = '0.5.70';
51
51
  /**
52
52
  * ┌──────────────────────────────────────────────────────────────────────┐
53
53
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -437,6 +437,23 @@ const STOPWORDS = new Set([
437
437
  'çok', 'en', 'her', 'hiç', 'bazı', 'tüm', 'bütün', 'hem', 'veya',
438
438
  'bu', 'şu', 'ben', 'sen', 'biz', 'siz', 'onlar', 'benim', 'senin',
439
439
  'nasıl', 'neden', 'niçin', 'nerede', 'ne', 'hangi', 'kaç',
440
+ // ── Polish — common prepositions, pronouns, particles ─────────────────────
441
+ 'w', 'na', 'z', 'do', 'się', 'że', 'to', 'jest', 'i', 'a', 'nie',
442
+ 'tak', 'jak', 'czy', 'po', 'o', 'ale', 'go', 'mu', 'jej', 'ich',
443
+ 'tego', 'tej', 'te', 'ten', 'ta', 'są', 'był', 'była', 'było', 'byli',
444
+ 'będzie', 'będą', 'ma', 'mam', 'masz', 'mają', 'ze', 'co', 'już',
445
+ 'przez', 'przy', 'za', 'bez', 'nad', 'pod', 'przed', 'po', 'między',
446
+ 'kiedy', 'gdzie', 'dlaczego', 'który', 'która', 'które', 'tylko', 'też',
447
+ // ── Czech — common words ──────────────────────────────────────────────────
448
+ 'v', 'na', 'z', 'do', 'se', 'že', 'to', 'je', 'a', 'ne', 'pro',
449
+ 'ale', 'jak', 'by', 'byl', 'být', 'jsem', 'jsou', 'má', 'mám',
450
+ 'ten', 'ta', 'ty', 'tato', 'jeho', 'její', 'jejich', 'také', 'jen',
451
+ 'kde', 'kdy', 'proč', 'který', 'která', 'které', 'co', 'při', 'bez',
452
+ // ── Bengali — common particles, pronouns, verbs ───────────────────────────
453
+ 'এই', 'এটি', 'এটা', 'এর', 'তার', 'তারা', 'আমি', 'তুমি', 'সে', 'আমরা',
454
+ 'এবং', 'কিন্তু', 'বা', 'না', 'হ্যাঁ', 'যে', 'কি', 'কে', 'কী', 'থেকে',
455
+ 'দিয়ে', 'জন্য', 'সাথে', 'মধ্যে', 'উপর', 'নিচে', 'আছে', 'ছিল', 'হয়',
456
+ 'করা', 'করে', 'করেছে', 'হবে', 'হয়েছে', 'একটি', 'একটা', 'অনেক', 'সব',
440
457
  ]);
441
458
  // CJK Unicode ranges used for bigram extraction
442
459
  const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
@@ -454,6 +471,8 @@ const DEVANAGARI_RE = /[\u0900-\u097f]/;
454
471
  const CYRILLIC_RE = /[\u0400-\u04ff]/;
455
472
  // Turkish uses Latin alphabet but has unique chars (ğ, ş, ı, ö, ü, ç) — detected by these
456
473
  const TURKISH_CHAR_RE = /[\u011f\u015f\u0131\u0130\u00e7]/; // ğ ş ı İ ç
474
+ // Bengali (Bangla) — U+0980–U+09FF
475
+ const BENGALI_RE = /[\u0980-\u09ff]/;
457
476
  // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
458
477
  // Digraphs must be listed before single chars so they match first.
459
478
  const KATA_ROMAJI_TABLE = [
@@ -780,12 +799,134 @@ function turkishLightStem(word) {
780
799
  return w.slice(0, -1);
781
800
  return w;
782
801
  }
802
+ /**
803
+ * Bengali (Bangla) light stemmer — strips common verbal and nominal suffixes.
804
+ * Bengali is an Indo-Aryan language with moderate morphological complexity.
805
+ * Only strips when result is still ≥ 2 chars.
806
+ *
807
+ * Covers:
808
+ * করছে/করেছে → কর (progressive/perfect aspect)
809
+ * সার্ভারগুলো → সার্ভার (-গুলো plural)
810
+ * সমস্যাটি → সমস্যা (-টি singular marker)
811
+ */
812
+ function bengaliLightStem(word) {
813
+ let w = word;
814
+ // Plural / collective markers (longest first)
815
+ if (w.endsWith('\u0997\u09c1\u09b2\u09cb') && w.length > 5)
816
+ return w.slice(0, -4); // গুলো (-gulo plural)
817
+ if (w.endsWith('\u0997\u09c1\u09b2\u09bf') && w.length > 5)
818
+ return w.slice(0, -4); // গুলি (-guli plural)
819
+ if (w.endsWith('\u09a6\u09c7\u09b0') && w.length > 4)
820
+ return w.slice(0, -3); // দের (genitive plural)
821
+ if (w.endsWith('\u09a6\u09bf\u0997\u09c7') && w.length > 5)
822
+ return w.slice(0, -4); // দিগে
823
+ // Definiteness / case markers
824
+ if (w.endsWith('\u099f\u09bf') && w.length > 3)
825
+ return w.slice(0, -2); // টি (singular definite)
826
+ if (w.endsWith('\u099f\u09be') && w.length > 3)
827
+ return w.slice(0, -2); // টা (singular definite)
828
+ if (w.endsWith('\u0996\u09be\u09a8\u09be') && w.length > 5)
829
+ return w.slice(0, -4); // খানা
830
+ // Verbal suffixes
831
+ if (w.endsWith('\u099b\u09c7') && w.length > 3)
832
+ return w.slice(0, -2); // ছে (progressive)
833
+ if (w.endsWith('\u099b\u09bf\u09b2') && w.length > 4)
834
+ return w.slice(0, -3); // ছিল (past progressive)
835
+ if (w.endsWith('\u09af\u09be\u09ac\u09c7') && w.length > 5)
836
+ return w.slice(0, -4); // যাবে (future)
837
+ if (w.endsWith('\u0995\u09b0\u09be') && w.length > 4)
838
+ return w.slice(0, -3); // করা (infinitive)
839
+ if (w.endsWith('\u0995\u09b0\u09c7') && w.length > 4)
840
+ return w.slice(0, -3); // করে (present)
841
+ if (w.endsWith('\u09b9\u09df') && w.length > 3)
842
+ return w.slice(0, -2); // হয় (is/becomes)
843
+ if (w.endsWith('\u09b9\u09ac\u09c7') && w.length > 4)
844
+ return w.slice(0, -3); // হবে (will be)
845
+ return w;
846
+ }
847
+ /**
848
+ * Pre-process text BEFORE tokenization to split code identifiers.
849
+ * Handles camelCase, PascalCase, snake_case, and kebab-case.
850
+ *
851
+ * Examples:
852
+ * deployServer → deploy Server (then lowercased → deploy server)
853
+ * HTMLParser → HTML Parser
854
+ * my_api_key → my api key
855
+ * get-user-by-id → get user by id (hyphens preserved in regex, spaces also fine)
856
+ * BackendServiceImpl → Backend Service Impl
857
+ */
858
+ function preprocessText(text) {
859
+ return text
860
+ // camelCase boundary: lowercase/digit → uppercase
861
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
862
+ // Consecutive uppercase → uppercase+lowercase boundary: HTMLParser → HTML Parser
863
+ .replace(/([A-Z]{2,})([A-Z][a-z])/g, '$1 $2')
864
+ // snake_case: replace underscores with spaces
865
+ .replace(/_/g, ' ');
866
+ }
783
867
  // Maps tokens (English, Japanese katakana, Chinese, Korean, Arabic, Hebrew)
784
868
  // to their equivalents in other languages. Used to expand tokens at search time
785
869
  // so "deploy" finds documents containing "デプロイ" / "部署" / "배포", and vice versa.
786
870
  //
787
871
  // Keys must be lowercase. CJK values will be bigram-expanded at use time.
788
872
  const CROSS_LINGUAL_MAP = new Map([
873
+ // ── Tech / Ecosystem synonyms (language-agnostic) ─────────────────────────
874
+ ['javascript', ['js', 'node', 'nodejs', 'ecmascript']],
875
+ ['js', ['javascript', 'node', 'nodejs']],
876
+ ['typescript', ['ts', 'javascript', 'js', 'tsc']],
877
+ ['ts', ['typescript', 'javascript', 'js']],
878
+ ['python', ['py', 'pip', 'django', 'flask', 'fastapi']],
879
+ ['py', ['python', 'pip']],
880
+ ['golang', ['go', 'gopher', 'goroutine']],
881
+ ['go', ['golang', 'goroutine']],
882
+ ['rust', ['cargo', 'rustlang', 'crates']],
883
+ ['cargo', ['rust', 'rustlang']],
884
+ ['java', ['jvm', 'maven', 'gradle', 'spring']],
885
+ ['kotlin', ['jvm', 'android', 'coroutine']],
886
+ ['dotnet', ['csharp', 'asp', 'nuget', 'aspnet']],
887
+ ['postgresql', ['postgres', 'pg', 'psql']],
888
+ ['postgres', ['postgresql', 'pg', 'psql']],
889
+ ['mysql', ['mariadb', 'sql']],
890
+ ['mongodb', ['mongo', 'nosql', 'bson', 'mongoose']],
891
+ ['mongo', ['mongodb', 'nosql', 'bson']],
892
+ ['elasticsearch', ['elastic', 'opensearch', 'lucene', 'kibana']],
893
+ ['elastic', ['elasticsearch', 'opensearch']],
894
+ ['kubernetes', ['k8s', 'kube', 'kubectl', 'helm', 'k3s']],
895
+ ['k8s', ['kubernetes', 'kube', 'kubectl', 'helm']],
896
+ ['helm', ['kubernetes', 'k8s', 'chart']],
897
+ ['terraform', ['tf', 'hcl', 'iac', 'opentofu']],
898
+ ['ansible', ['playbook', 'automation']],
899
+ ['grafana', ['dashboard', 'visualization', 'metrics']],
900
+ ['prometheus', ['metrics', 'alert', 'scrape', 'grafana']],
901
+ ['nginx', ['proxy', 'webserver', 'ingress']],
902
+ ['traefik', ['proxy', 'ingress', 'router']],
903
+ ['rabbitmq', ['amqp', 'queue', 'broker', 'messaging']],
904
+ ['kafka', ['messaging', 'stream', 'broker']],
905
+ ['grpc', ['protobuf', 'proto', 'rpc']],
906
+ ['graphql', ['gql', 'resolver', 'schema']],
907
+ ['react', ['jsx', 'hooks', 'component', 'redux']],
908
+ ['nextjs', ['next', 'react', 'ssr', 'vercel']],
909
+ ['vue', ['vuejs', 'vite', 'nuxt']],
910
+ ['angular', ['ng', 'rxjs', 'typescript']],
911
+ ['aws', ['amazon', 'ec2', 's3', 'lambda', 'cloudwatch', 'ecs', 'eks']],
912
+ ['gcp', ['google cloud', 'gke', 'bigquery']],
913
+ ['azure', ['microsoft cloud', 'aks', 'devops']],
914
+ ['s3', ['bucket', 'object storage', 'aws']],
915
+ ['lambda', ['serverless', 'function', 'faas', 'aws']],
916
+ ['jwt', ['token', 'bearer', 'oauth', 'auth']],
917
+ ['oauth', ['oidc', 'auth', 'token', 'sso']],
918
+ ['webhook', ['callback', 'event', 'trigger']],
919
+ ['cron', ['schedule', 'job', 'timer']],
920
+ ['yaml', ['yml', 'config', 'manifest']],
921
+ ['dotenv', ['env', 'environment', 'envfile']],
922
+ ['github', ['git', 'actions', 'repo', 'ci']],
923
+ ['gitlab', ['git', 'pipeline', 'runner']],
924
+ ['wireguard', ['vpn', 'tunnel', 'wg']],
925
+ ['prisma', ['orm', 'database', 'schema', 'migration']],
926
+ ['npm', ['node', 'package', 'registry', 'yarn', 'pnpm']],
927
+ ['yarn', ['npm', 'package', 'node']],
928
+ ['pip', ['python', 'package', 'pypi']],
929
+ ['brew', ['homebrew', 'macos', 'package']],
789
930
  // English → all
790
931
  ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação', 'развёртывание', 'развертывание', 'dağıtım', 'dağıtmak']],
791
932
  ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação', 'развёртывание', 'dağıtım']],
@@ -1392,7 +1533,8 @@ function expandCrossLingual(token) {
1392
1533
  */
1393
1534
  function tokenize(text) {
1394
1535
  const tokens = [];
1395
- const lower = text.toLowerCase();
1536
+ // Pre-process: split camelCase/PascalCase identifiers and snake_case before lowercasing
1537
+ const lower = preprocessText(text).toLowerCase();
1396
1538
  for (const [seg] of lower.matchAll(SEGMENT_RE)) {
1397
1539
  if (CJK_RE.test(seg)) {
1398
1540
  // CJK segment — extract overlapping character bigrams
@@ -1446,10 +1588,8 @@ function tokenize(text) {
1446
1588
  }
1447
1589
  else if (DEVANAGARI_RE.test(seg)) {
1448
1590
  // Devanagari segment (Hindi / Marathi) — naturally space-separated
1449
- // Words don't need bigram expansion; stems reduce inflected forms.
1450
1591
  const words = seg.trim().split(/[\s\u200c\u200d]+/); // handle zero-width joiners
1451
1592
  for (const w of words) {
1452
- // Keep Devanagari chars + digits; discard pure Latin portions (e.g. embedded numbers)
1453
1593
  const deva = w.replace(/[^\u0900-\u097f0-9]/g, '');
1454
1594
  if (deva.length <= 1 || STOPWORDS.has(deva))
1455
1595
  continue;
@@ -1458,6 +1598,18 @@ function tokenize(text) {
1458
1598
  tokens.push(stemmed);
1459
1599
  }
1460
1600
  }
1601
+ else if (BENGALI_RE.test(seg)) {
1602
+ // Bengali (Bangla) segment — space-separated words, Bengali script
1603
+ const words = seg.trim().split(/[\s\u200c\u200d]+/);
1604
+ for (const w of words) {
1605
+ const bn = w.replace(/[^\u0980-\u09ff0-9]/g, '');
1606
+ if (bn.length <= 1 || STOPWORDS.has(bn))
1607
+ continue;
1608
+ const stemmed = bengaliLightStem(bn);
1609
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1610
+ tokens.push(stemmed);
1611
+ }
1612
+ }
1461
1613
  else if (CYRILLIC_RE.test(seg)) {
1462
1614
  // Cyrillic segment (Russian, Bulgarian, Ukrainian, Serbian …)
1463
1615
  const words = seg.trim().split(/\s+/);
@@ -1471,9 +1623,10 @@ function tokenize(text) {
1471
1623
  }
1472
1624
  }
1473
1625
  else {
1474
- // Latin / other (includes Turkish with special chars ğ/ş/ı)
1626
+ // Latin / other — includes Turkish (ğ/ş/ı), Polish (ą/ę/ł/ń/ó/ś/ź/ż/ć),
1627
+ // Czech (č/š/ž/ě/ů/ř), Hungarian (ő/ű), Romanian (ș/ț)
1475
1628
  const words = seg
1476
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœğşıİ0-9:_\-./]/g, ' ')
1629
+ .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœğşıİąćęłńśźżčšžěůřőűșț0-9:\-./]/g, ' ')
1477
1630
  .split(/\s+/)
1478
1631
  .filter(w => w.length > 1 && !STOPWORDS.has(w));
1479
1632
  // Turkish: apply light stemmer to words with Turkish-specific chars
@@ -1527,6 +1680,12 @@ function tokenize(text) {
1527
1680
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1528
1681
  expansions.push(stemmed);
1529
1682
  }
1683
+ else if (BENGALI_RE.test(syn)) {
1684
+ // Bengali synonym
1685
+ const stemmed = bengaliLightStem(syn);
1686
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1687
+ expansions.push(stemmed);
1688
+ }
1530
1689
  else if (CYRILLIC_RE.test(syn)) {
1531
1690
  // Cyrillic synonym (Russian)
1532
1691
  const cyr = syn.replace(/[^\u0400-\u04ff]/g, '');
@@ -1706,7 +1865,9 @@ async function keywordSearch(redis, patterns, query, topK = 10) {
1706
1865
  bigrams.add(`${tokens[j]}|${tokens[j + 1]}`);
1707
1866
  }
1708
1867
  const timestamp = extractTimestamp(content);
1709
- docs.push({ key: allKeys[i], content, tokens, tokenFreq, bigrams, timestamp });
1868
+ // Key tokens for title-boost: terms appearing in the Redis key get extra weight
1869
+ const keyTokens = new Set(tokenize(allKeys[i]));
1870
+ docs.push({ key: allKeys[i], content, tokens, tokenFreq, bigrams, keyTokens, timestamp });
1710
1871
  totalTokens += tokens.length;
1711
1872
  }
1712
1873
  if (docs.length === 0)
@@ -1812,6 +1973,18 @@ async function keywordSearch(redis, patterns, query, topK = 10) {
1812
1973
  if (sq.length >= 4 && doc.content.toLowerCase().includes(sq.toLowerCase())) {
1813
1974
  score *= 2.0;
1814
1975
  }
1976
+ // Key/title boost: if query terms appear in the Redis key (=title), 1.5× boost
1977
+ // Keys often encode the primary topic (e.g. "deploy:api:server"), so key hits are high-precision.
1978
+ if (score > 0) {
1979
+ let keyHits = 0;
1980
+ for (const qt of queryTokens) {
1981
+ if (doc.keyTokens.has(qt))
1982
+ keyHits++;
1983
+ }
1984
+ if (keyHits > 0) {
1985
+ score *= 1 + 0.5 * (keyHits / queryTokens.length);
1986
+ }
1987
+ }
1815
1988
  // Recency boost
1816
1989
  score *= recencyBoost(doc.timestamp);
1817
1990
  if (score > 0) {