@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.5.60",
3
+ "version": "0.5.70",
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.60';
58
+ const CURRENT_VERSION = '0.5.70';
59
59
  /**
60
60
  * ┌──────────────────────────────────────────────────────────────────────┐
61
61
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -451,6 +451,23 @@ const STOPWORDS = new Set([
451
451
  'çok', 'en', 'her', 'hiç', 'bazı', 'tüm', 'bütün', 'hem', 'veya',
452
452
  'bu', 'şu', 'ben', 'sen', 'biz', 'siz', 'onlar', 'benim', 'senin',
453
453
  'nasıl', 'neden', 'niçin', 'nerede', 'ne', 'hangi', 'kaç',
454
+ // ── Polish — common prepositions, pronouns, particles ─────────────────────
455
+ 'w', 'na', 'z', 'do', 'się', 'że', 'to', 'jest', 'i', 'a', 'nie',
456
+ 'tak', 'jak', 'czy', 'po', 'o', 'ale', 'go', 'mu', 'jej', 'ich',
457
+ 'tego', 'tej', 'te', 'ten', 'ta', 'są', 'był', 'była', 'było', 'byli',
458
+ 'będzie', 'będą', 'ma', 'mam', 'masz', 'mają', 'ze', 'co', 'już',
459
+ 'przez', 'przy', 'za', 'bez', 'nad', 'pod', 'przed', 'po', 'między',
460
+ 'kiedy', 'gdzie', 'dlaczego', 'który', 'która', 'które', 'tylko', 'też',
461
+ // ── Czech — common words ──────────────────────────────────────────────────
462
+ 'v', 'na', 'z', 'do', 'se', 'že', 'to', 'je', 'a', 'ne', 'pro',
463
+ 'ale', 'jak', 'by', 'byl', 'být', 'jsem', 'jsou', 'má', 'mám',
464
+ 'ten', 'ta', 'ty', 'tato', 'jeho', 'její', 'jejich', 'také', 'jen',
465
+ 'kde', 'kdy', 'proč', 'který', 'která', 'které', 'co', 'při', 'bez',
466
+ // ── Bengali — common particles, pronouns, verbs ───────────────────────────
467
+ 'এই', 'এটি', 'এটা', 'এর', 'তার', 'তারা', 'আমি', 'তুমি', 'সে', 'আমরা',
468
+ 'এবং', 'কিন্তু', 'বা', 'না', 'হ্যাঁ', 'যে', 'কি', 'কে', 'কী', 'থেকে',
469
+ 'দিয়ে', 'জন্য', 'সাথে', 'মধ্যে', 'উপর', 'নিচে', 'আছে', 'ছিল', 'হয়',
470
+ 'করা', 'করে', 'করেছে', 'হবে', 'হয়েছে', 'একটি', 'একটা', 'অনেক', 'সব',
454
471
  ]);
455
472
 
456
473
 
@@ -471,6 +488,8 @@ const DEVANAGARI_RE = /[\u0900-\u097f]/;
471
488
  const CYRILLIC_RE = /[\u0400-\u04ff]/;
472
489
  // Turkish uses Latin alphabet but has unique chars (ğ, ş, ı, ö, ü, ç) — detected by these
473
490
  const TURKISH_CHAR_RE = /[\u011f\u015f\u0131\u0130\u00e7]/; // ğ ş ı İ ç
491
+ // Bengali (Bangla) — U+0980–U+09FF
492
+ const BENGALI_RE = /[\u0980-\u09ff]/;
474
493
 
475
494
  // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
476
495
  // Digraphs must be listed before single chars so they match first.
@@ -728,6 +747,59 @@ function turkishLightStem(word: string): string {
728
747
  return w;
729
748
  }
730
749
 
750
+ /**
751
+ * Bengali (Bangla) light stemmer — strips common verbal and nominal suffixes.
752
+ * Bengali is an Indo-Aryan language with moderate morphological complexity.
753
+ * Only strips when result is still ≥ 2 chars.
754
+ *
755
+ * Covers:
756
+ * করছে/করেছে → কর (progressive/perfect aspect)
757
+ * সার্ভারগুলো → সার্ভার (-গুলো plural)
758
+ * সমস্যাটি → সমস্যা (-টি singular marker)
759
+ */
760
+ function bengaliLightStem(word: string): string {
761
+ let w = word;
762
+ // Plural / collective markers (longest first)
763
+ if (w.endsWith('\u0997\u09c1\u09b2\u09cb') && w.length > 5) return w.slice(0, -4); // গুলো (-gulo plural)
764
+ if (w.endsWith('\u0997\u09c1\u09b2\u09bf') && w.length > 5) return w.slice(0, -4); // গুলি (-guli plural)
765
+ if (w.endsWith('\u09a6\u09c7\u09b0') && w.length > 4) return w.slice(0, -3); // দের (genitive plural)
766
+ if (w.endsWith('\u09a6\u09bf\u0997\u09c7') && w.length > 5) return w.slice(0, -4); // দিগে
767
+ // Definiteness / case markers
768
+ if (w.endsWith('\u099f\u09bf') && w.length > 3) return w.slice(0, -2); // টি (singular definite)
769
+ if (w.endsWith('\u099f\u09be') && w.length > 3) return w.slice(0, -2); // টা (singular definite)
770
+ if (w.endsWith('\u0996\u09be\u09a8\u09be') && w.length > 5) return w.slice(0, -4); // খানা
771
+ // Verbal suffixes
772
+ if (w.endsWith('\u099b\u09c7') && w.length > 3) return w.slice(0, -2); // ছে (progressive)
773
+ if (w.endsWith('\u099b\u09bf\u09b2') && w.length > 4) return w.slice(0, -3); // ছিল (past progressive)
774
+ if (w.endsWith('\u09af\u09be\u09ac\u09c7') && w.length > 5) return w.slice(0, -4); // যাবে (future)
775
+ if (w.endsWith('\u0995\u09b0\u09be') && w.length > 4) return w.slice(0, -3); // করা (infinitive)
776
+ if (w.endsWith('\u0995\u09b0\u09c7') && w.length > 4) return w.slice(0, -3); // করে (present)
777
+ if (w.endsWith('\u09b9\u09df') && w.length > 3) return w.slice(0, -2); // হয় (is/becomes)
778
+ if (w.endsWith('\u09b9\u09ac\u09c7') && w.length > 4) return w.slice(0, -3); // হবে (will be)
779
+ return w;
780
+ }
781
+
782
+ /**
783
+ * Pre-process text BEFORE tokenization to split code identifiers.
784
+ * Handles camelCase, PascalCase, snake_case, and kebab-case.
785
+ *
786
+ * Examples:
787
+ * deployServer → deploy Server (then lowercased → deploy server)
788
+ * HTMLParser → HTML Parser
789
+ * my_api_key → my api key
790
+ * get-user-by-id → get user by id (hyphens preserved in regex, spaces also fine)
791
+ * BackendServiceImpl → Backend Service Impl
792
+ */
793
+ function preprocessText(text: string): string {
794
+ return text
795
+ // camelCase boundary: lowercase/digit → uppercase
796
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
797
+ // Consecutive uppercase → uppercase+lowercase boundary: HTMLParser → HTML Parser
798
+ .replace(/([A-Z]{2,})([A-Z][a-z])/g, '$1 $2')
799
+ // snake_case: replace underscores with spaces
800
+ .replace(/_/g, ' ');
801
+ }
802
+
731
803
 
732
804
  // Maps tokens (English, Japanese katakana, Chinese, Korean, Arabic, Hebrew)
733
805
  // to their equivalents in other languages. Used to expand tokens at search time
@@ -735,6 +807,64 @@ function turkishLightStem(word: string): string {
735
807
  //
736
808
  // Keys must be lowercase. CJK values will be bigram-expanded at use time.
737
809
  const CROSS_LINGUAL_MAP = new Map<string, string[]>([
810
+
811
+ // ── Tech / Ecosystem synonyms (language-agnostic) ─────────────────────────
812
+ ['javascript', ['js', 'node', 'nodejs', 'ecmascript']],
813
+ ['js', ['javascript', 'node', 'nodejs']],
814
+ ['typescript', ['ts', 'javascript', 'js', 'tsc']],
815
+ ['ts', ['typescript', 'javascript', 'js']],
816
+ ['python', ['py', 'pip', 'django', 'flask', 'fastapi']],
817
+ ['py', ['python', 'pip']],
818
+ ['golang', ['go', 'gopher', 'goroutine']],
819
+ ['go', ['golang', 'goroutine']],
820
+ ['rust', ['cargo', 'rustlang', 'crates']],
821
+ ['cargo', ['rust', 'rustlang']],
822
+ ['java', ['jvm', 'maven', 'gradle', 'spring']],
823
+ ['kotlin', ['jvm', 'android', 'coroutine']],
824
+ ['dotnet', ['csharp', 'asp', 'nuget', 'aspnet']],
825
+ ['postgresql', ['postgres', 'pg', 'psql']],
826
+ ['postgres', ['postgresql', 'pg', 'psql']],
827
+ ['mysql', ['mariadb', 'sql']],
828
+ ['mongodb', ['mongo', 'nosql', 'bson', 'mongoose']],
829
+ ['mongo', ['mongodb', 'nosql', 'bson']],
830
+ ['elasticsearch', ['elastic', 'opensearch', 'lucene', 'kibana']],
831
+ ['elastic', ['elasticsearch', 'opensearch']],
832
+ ['kubernetes', ['k8s', 'kube', 'kubectl', 'helm', 'k3s']],
833
+ ['k8s', ['kubernetes', 'kube', 'kubectl', 'helm']],
834
+ ['helm', ['kubernetes', 'k8s', 'chart']],
835
+ ['terraform', ['tf', 'hcl', 'iac', 'opentofu']],
836
+ ['ansible', ['playbook', 'automation']],
837
+ ['grafana', ['dashboard', 'visualization', 'metrics']],
838
+ ['prometheus', ['metrics', 'alert', 'scrape', 'grafana']],
839
+ ['nginx', ['proxy', 'webserver', 'ingress']],
840
+ ['traefik', ['proxy', 'ingress', 'router']],
841
+ ['rabbitmq', ['amqp', 'queue', 'broker', 'messaging']],
842
+ ['kafka', ['messaging', 'stream', 'broker']],
843
+ ['grpc', ['protobuf', 'proto', 'rpc']],
844
+ ['graphql', ['gql', 'resolver', 'schema']],
845
+ ['react', ['jsx', 'hooks', 'component', 'redux']],
846
+ ['nextjs', ['next', 'react', 'ssr', 'vercel']],
847
+ ['vue', ['vuejs', 'vite', 'nuxt']],
848
+ ['angular', ['ng', 'rxjs', 'typescript']],
849
+ ['aws', ['amazon', 'ec2', 's3', 'lambda', 'cloudwatch', 'ecs', 'eks']],
850
+ ['gcp', ['google cloud', 'gke', 'bigquery']],
851
+ ['azure', ['microsoft cloud', 'aks', 'devops']],
852
+ ['s3', ['bucket', 'object storage', 'aws']],
853
+ ['lambda', ['serverless', 'function', 'faas', 'aws']],
854
+ ['jwt', ['token', 'bearer', 'oauth', 'auth']],
855
+ ['oauth', ['oidc', 'auth', 'token', 'sso']],
856
+ ['webhook', ['callback', 'event', 'trigger']],
857
+ ['cron', ['schedule', 'job', 'timer']],
858
+ ['yaml', ['yml', 'config', 'manifest']],
859
+ ['dotenv', ['env', 'environment', 'envfile']],
860
+ ['github', ['git', 'actions', 'repo', 'ci']],
861
+ ['gitlab', ['git', 'pipeline', 'runner']],
862
+ ['wireguard', ['vpn', 'tunnel', 'wg']],
863
+ ['prisma', ['orm', 'database', 'schema', 'migration']],
864
+ ['npm', ['node', 'package', 'registry', 'yarn', 'pnpm']],
865
+ ['yarn', ['npm', 'package', 'node']],
866
+ ['pip', ['python', 'package', 'pypi']],
867
+ ['brew', ['homebrew', 'macos', 'package']],
738
868
  // English → all
739
869
  ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação', 'развёртывание', 'развертывание', 'dağıtım', 'dağıtmak']],
740
870
  ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação', 'развёртывание', 'dağıtım']],
@@ -1352,7 +1482,8 @@ function expandCrossLingual(token: string): string[] {
1352
1482
  */
1353
1483
  function tokenize(text: string): string[] {
1354
1484
  const tokens: string[] = [];
1355
- const lower = text.toLowerCase();
1485
+ // Pre-process: split camelCase/PascalCase identifiers and snake_case before lowercasing
1486
+ const lower = preprocessText(text).toLowerCase();
1356
1487
 
1357
1488
  for (const [seg] of lower.matchAll(SEGMENT_RE)) {
1358
1489
  if (CJK_RE.test(seg)) {
@@ -1399,15 +1530,22 @@ function tokenize(text: string): string[] {
1399
1530
  }
1400
1531
  } else if (DEVANAGARI_RE.test(seg)) {
1401
1532
  // Devanagari segment (Hindi / Marathi) — naturally space-separated
1402
- // Words don't need bigram expansion; stems reduce inflected forms.
1403
1533
  const words = seg.trim().split(/[\s\u200c\u200d]+/); // handle zero-width joiners
1404
1534
  for (const w of words) {
1405
- // Keep Devanagari chars + digits; discard pure Latin portions (e.g. embedded numbers)
1406
1535
  const deva = w.replace(/[^\u0900-\u097f0-9]/g, '');
1407
1536
  if (deva.length <= 1 || STOPWORDS.has(deva)) continue;
1408
1537
  const stemmed = hindiLightStem(deva);
1409
1538
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1410
1539
  }
1540
+ } else if (BENGALI_RE.test(seg)) {
1541
+ // Bengali (Bangla) segment — space-separated words, Bengali script
1542
+ const words = seg.trim().split(/[\s\u200c\u200d]+/);
1543
+ for (const w of words) {
1544
+ const bn = w.replace(/[^\u0980-\u09ff0-9]/g, '');
1545
+ if (bn.length <= 1 || STOPWORDS.has(bn)) continue;
1546
+ const stemmed = bengaliLightStem(bn);
1547
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1548
+ }
1411
1549
  } else if (CYRILLIC_RE.test(seg)) {
1412
1550
  // Cyrillic segment (Russian, Bulgarian, Ukrainian, Serbian …)
1413
1551
  const words = seg.trim().split(/\s+/);
@@ -1418,9 +1556,10 @@ function tokenize(text: string): string[] {
1418
1556
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) tokens.push(stemmed);
1419
1557
  }
1420
1558
  } else {
1421
- // Latin / other (includes Turkish with special chars ğ/ş/ı)
1559
+ // Latin / other — includes Turkish (ğ/ş/ı), Polish (ą/ę/ł/ń/ó/ś/ź/ż/ć),
1560
+ // Czech (č/š/ž/ě/ů/ř), Hungarian (ő/ű), Romanian (ș/ț)
1422
1561
  const words = seg
1423
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœğşıİ0-9:_\-./]/g, ' ')
1562
+ .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœğşıİąćęłńśźżčšžěůřőűșț0-9:\-./]/g, ' ')
1424
1563
  .split(/\s+/)
1425
1564
  .filter(w => w.length > 1 && !STOPWORDS.has(w));
1426
1565
  // Turkish: apply light stemmer to words with Turkish-specific chars
@@ -1467,6 +1606,10 @@ function tokenize(text: string): string[] {
1467
1606
  // Devanagari synonym (Hindi)
1468
1607
  const stemmed = hindiLightStem(syn);
1469
1608
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1609
+ } else if (BENGALI_RE.test(syn)) {
1610
+ // Bengali synonym
1611
+ const stemmed = bengaliLightStem(syn);
1612
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed)) expansions.push(stemmed);
1470
1613
  } else if (CYRILLIC_RE.test(syn)) {
1471
1614
  // Cyrillic synonym (Russian)
1472
1615
  const cyr = syn.replace(/[^\u0400-\u04ff]/g, '');
@@ -1555,6 +1698,7 @@ interface DocEntry {
1555
1698
  tokens: string[];
1556
1699
  tokenFreq: Map<string, number>; // term → count in this doc
1557
1700
  bigrams: Set<string>; // "term1|term2" adjacency pairs
1701
+ keyTokens: Set<string>; // tokens from key/title only (for title boost)
1558
1702
  timestamp?: number; // epoch ms, extracted from content if present
1559
1703
  }
1560
1704
 
@@ -1674,7 +1818,9 @@ async function keywordSearch(
1674
1818
  }
1675
1819
 
1676
1820
  const timestamp = extractTimestamp(content);
1677
- docs.push({ key: allKeys[i], content, tokens, tokenFreq, bigrams, timestamp });
1821
+ // Key tokens for title-boost: terms appearing in the Redis key get extra weight
1822
+ const keyTokens = new Set(tokenize(allKeys[i]));
1823
+ docs.push({ key: allKeys[i], content, tokens, tokenFreq, bigrams, keyTokens, timestamp });
1678
1824
  totalTokens += tokens.length;
1679
1825
  }
1680
1826
 
@@ -1788,6 +1934,18 @@ async function keywordSearch(
1788
1934
  score *= 2.0;
1789
1935
  }
1790
1936
 
1937
+ // Key/title boost: if query terms appear in the Redis key (=title), 1.5× boost
1938
+ // Keys often encode the primary topic (e.g. "deploy:api:server"), so key hits are high-precision.
1939
+ if (score > 0) {
1940
+ let keyHits = 0;
1941
+ for (const qt of queryTokens) {
1942
+ if (doc.keyTokens.has(qt)) keyHits++;
1943
+ }
1944
+ if (keyHits > 0) {
1945
+ score *= 1 + 0.5 * (keyHits / queryTokens.length);
1946
+ }
1947
+ }
1948
+
1791
1949
  // Recency boost
1792
1950
  score *= recencyBoost(doc.timestamp);
1793
1951