@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 +180 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +165 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cachly-dev/mcp-server",
|
|
3
|
-
"version": "0.5.
|
|
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.
|
|
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
|
-
|
|
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
|
|
1559
|
+
// Latin / other — includes Turkish (ğ/ş/ı), Polish (ą/ę/ł/ń/ó/ś/ź/ż/ć),
|
|
1560
|
+
// Czech (č/š/ž/ě/ů/ř), Hungarian (ő/ű), Romanian (ș/ț)
|
|
1422
1561
|
const words = seg
|
|
1423
|
-
.replace(/[^a-
|
|
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
|
-
|
|
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
|
|