@cachly-dev/mcp-server 0.5.34 → 0.5.35
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 +7 -2
- package/dist/index.js +170 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/smithery.yaml +10 -5
- package/src/index.ts +174 -9
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.35",
|
|
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/smithery.yaml
CHANGED
|
@@ -5,16 +5,21 @@ startCommand:
|
|
|
5
5
|
properties:
|
|
6
6
|
CACHLY_JWT:
|
|
7
7
|
type: string
|
|
8
|
-
title: "API Key
|
|
9
|
-
description: "Your Cachly API key
|
|
8
|
+
title: "API Key"
|
|
9
|
+
description: "Your Cachly API key. Get it free at cachly.dev/setup-ai → Dashboard → API Keys. Looks like: cky_live_..."
|
|
10
|
+
examples:
|
|
11
|
+
- "cky_live_abc123..."
|
|
10
12
|
CACHLY_BRAIN_INSTANCE_ID:
|
|
11
13
|
type: string
|
|
12
14
|
title: "Brain Instance ID"
|
|
13
|
-
description: "UUID of your Cachly Brain instance
|
|
15
|
+
description: "UUID of your Cachly Brain instance. Find it at cachly.dev/instances — click your instance and copy the ID from the URL or details panel. Looks like: 9d4077aa-bfa2-468b-89cd-0a8d8f3ec483"
|
|
16
|
+
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
17
|
+
examples:
|
|
18
|
+
- "9d4077aa-bfa2-468b-89cd-0a8d8f3ec483"
|
|
14
19
|
CACHLY_API_URL:
|
|
15
20
|
type: string
|
|
16
|
-
title: "API URL"
|
|
17
|
-
description: "Cachly API endpoint — leave as default"
|
|
21
|
+
title: "API URL (optional)"
|
|
22
|
+
description: "Cachly API endpoint — leave as default unless you self-host"
|
|
18
23
|
default: "https://api.cachly.dev"
|
|
19
24
|
required:
|
|
20
25
|
- CACHLY_JWT
|
package/src/index.ts
CHANGED
|
@@ -301,13 +301,15 @@ function embedProviderHint(): string {
|
|
|
301
301
|
// • Recency boost — newer entries rank higher (exponential decay, 7-day half-life)
|
|
302
302
|
// • Multi-query splitting — numbered lists, semicolons, conjunctions
|
|
303
303
|
// • Fuzzy matching with Levenshtein distance ≤ 2
|
|
304
|
-
// • Multilingual stopwords (EN, DE, FR, ES, IT, PT)
|
|
304
|
+
// • Multilingual stopwords (EN, DE, FR, ES, IT, PT, ZH, JA, KO)
|
|
305
|
+
// • CJK character bigram extraction — handles Chinese, Japanese, Korean
|
|
305
306
|
// • Pipeline Redis reads for performance
|
|
306
307
|
//
|
|
307
308
|
|
|
308
309
|
/**
|
|
309
310
|
* Stopwords — filtered out during tokenization.
|
|
310
|
-
* Covers: English, German, French, Spanish, Italian, Portuguese
|
|
311
|
+
* Covers: English, German, French, Spanish, Italian, Portuguese,
|
|
312
|
+
* Chinese (Simplified + Traditional), Japanese, Korean.
|
|
311
313
|
* Keeps the index small and scores meaningful.
|
|
312
314
|
*/
|
|
313
315
|
const STOPWORDS = new Set([
|
|
@@ -381,15 +383,63 @@ const STOPWORDS = new Set([
|
|
|
381
383
|
'eu', 'tu', 'ele', 'ela', 'nós', 'eles', 'elas', 'você', 'vocês',
|
|
382
384
|
'não', 'já', 'sim', 'bem', 'muito', 'também', 'ainda', 'sempre',
|
|
383
385
|
'todo', 'toda', 'todos', 'cada', 'outro', 'outra', 'quando', 'porque',
|
|
386
|
+
// ── Chinese (Simplified + Traditional) — high-frequency function characters ──
|
|
387
|
+
'的', '了', '是', '在', '不', '和', '我', '他', '这', '中', '大', '为',
|
|
388
|
+
'上', '个', '国', '以', '要', '就', '出', '说', '们', '有', '来', '到',
|
|
389
|
+
'时', '地', '年', '得', '着', '那', '过', '后', '还', '与', '也', '可',
|
|
390
|
+
'于', '从', '但', '而', '被', '把', '让', '使', '对', '很', '都', '一',
|
|
391
|
+
'会', '没', '人', '它', '这个', '那个', '什么', '如果', '因为', '所以',
|
|
392
|
+
'已经', '可以', '这些', '那些', '我们', '他们', '她们', '它们',
|
|
393
|
+
// ── Japanese — common hiragana particles and auxiliary verbs ──
|
|
394
|
+
'の', 'は', 'が', 'を', 'に', 'で', 'と', 'も', 'や', 'か', 'な', 'ね',
|
|
395
|
+
'よ', 'わ', 'て', 'い', 'う', 'え', 'お', 'き', 'く', 'け', 'こ', 'さ',
|
|
396
|
+
'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'ぬ', 'ん', 'から', 'まで',
|
|
397
|
+
'より', 'へ', 'です', 'ます', 'ない', 'ある', 'いる', 'する', 'こと',
|
|
398
|
+
'もの', 'ので', 'では', 'には', 'との', 'への', 'から', 'まで',
|
|
399
|
+
// ── Korean — common particles and auxiliary forms ──
|
|
400
|
+
'이', '가', '은', '는', '을', '를', '의', '에', '와', '과', '도',
|
|
401
|
+
'로', '에서', '한', '하', '있', '없', '것', '수', '않', '들',
|
|
402
|
+
'이다', '하다', '이고', '이며', '라고', '에게', '에서', '으로',
|
|
384
403
|
]);
|
|
385
404
|
|
|
386
|
-
|
|
405
|
+
// CJK Unicode ranges used for bigram extraction
|
|
406
|
+
const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
|
|
407
|
+
const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+|[^\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+)/g;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Tokenize text into meaningful keywords.
|
|
411
|
+
* Handles accented Latin characters and CJK (Chinese, Japanese, Korean).
|
|
412
|
+
* CJK text is split into character bigrams (the standard for indexing logographic languages).
|
|
413
|
+
*/
|
|
387
414
|
function tokenize(text: string): string[] {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
415
|
+
const tokens: string[] = [];
|
|
416
|
+
const lower = text.toLowerCase();
|
|
417
|
+
|
|
418
|
+
for (const [seg] of lower.matchAll(SEGMENT_RE)) {
|
|
419
|
+
if (CJK_RE.test(seg)) {
|
|
420
|
+
// CJK segment — extract overlapping character bigrams
|
|
421
|
+
for (let i = 0; i < seg.length - 1; i++) {
|
|
422
|
+
const bigram = seg[i] + seg[i + 1];
|
|
423
|
+
if (!STOPWORDS.has(seg[i]) && !STOPWORDS.has(bigram)) {
|
|
424
|
+
tokens.push(bigram);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Trailing single character (for 1-char CJK terms)
|
|
428
|
+
if (seg.length >= 1) {
|
|
429
|
+
const last = seg[seg.length - 1];
|
|
430
|
+
if (!STOPWORDS.has(last)) tokens.push(last);
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
// Latin / other — existing logic
|
|
434
|
+
const words = seg
|
|
435
|
+
.replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
|
|
436
|
+
.split(/\s+/)
|
|
437
|
+
.filter(w => w.length > 1 && !STOPWORDS.has(w));
|
|
438
|
+
tokens.push(...words);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return tokens;
|
|
393
443
|
}
|
|
394
444
|
|
|
395
445
|
/**
|
|
@@ -5208,6 +5258,8 @@ const EDITOR_FILES: Record<string, string> = {
|
|
|
5208
5258
|
windsurf: '.windsurf/mcp.json',
|
|
5209
5259
|
copilot: '.vscode/mcp.json',
|
|
5210
5260
|
continue: '.continue/config.json',
|
|
5261
|
+
cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
|
|
5262
|
+
zed: '.zed/settings.json', // Zed project-level context_servers
|
|
5211
5263
|
};
|
|
5212
5264
|
|
|
5213
5265
|
const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
|
|
@@ -5224,6 +5276,20 @@ function buildMcpConfig(apiKey: string, instanceId: string, editor: string): str
|
|
|
5224
5276
|
},
|
|
5225
5277
|
}, null, 2);
|
|
5226
5278
|
}
|
|
5279
|
+
if (editor === 'zed') {
|
|
5280
|
+
return JSON.stringify({
|
|
5281
|
+
context_servers: {
|
|
5282
|
+
cachly: {
|
|
5283
|
+
command: {
|
|
5284
|
+
path: 'npx',
|
|
5285
|
+
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
5286
|
+
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
5287
|
+
},
|
|
5288
|
+
settings: {},
|
|
5289
|
+
},
|
|
5290
|
+
},
|
|
5291
|
+
}, null, 2);
|
|
5292
|
+
}
|
|
5227
5293
|
return JSON.stringify({
|
|
5228
5294
|
mcpServers: {
|
|
5229
5295
|
cachly: {
|
|
@@ -5556,15 +5622,28 @@ if (process.argv[2] === 'setup') {
|
|
|
5556
5622
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
5557
5623
|
const cwd = process.cwd();
|
|
5558
5624
|
const detected: string[] = [];
|
|
5625
|
+
const { homedir } = await import('node:os');
|
|
5626
|
+
const home = homedir();
|
|
5559
5627
|
// Claude Code: always include (CLAUDE.md is universal)
|
|
5560
5628
|
detected.push('claude');
|
|
5561
5629
|
if (existsSync(resolve(cwd, '.cursor'))) detected.push('cursor');
|
|
5562
5630
|
if (existsSync(resolve(cwd, '.windsurf'))) detected.push('windsurf');
|
|
5563
5631
|
if (existsSync(resolve(cwd, '.vscode'))) detected.push('copilot');
|
|
5564
5632
|
if (existsSync(resolve(cwd, '.continue'))) detected.push('continue');
|
|
5633
|
+
// Cline: VS Code extension — detect via globalStorage on the machine
|
|
5634
|
+
const clineGlobalDir = process.platform === 'darwin'
|
|
5635
|
+
? resolve(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev')
|
|
5636
|
+
: resolve(home, '.vscode/extensions');
|
|
5637
|
+
if (existsSync(clineGlobalDir) && !detected.includes('copilot')) detected.push('cline');
|
|
5638
|
+
else if (existsSync(clineGlobalDir)) detected.push('cline'); // both copilot + cline share .vscode/mcp.json — fine
|
|
5639
|
+
// Zed editor — detect via app data dir
|
|
5640
|
+
const zedDir = process.platform === 'darwin'
|
|
5641
|
+
? resolve(home, 'Library/Application Support/Zed')
|
|
5642
|
+
: resolve(home, '.config/zed');
|
|
5643
|
+
if (existsSync(zedDir)) detected.push('zed');
|
|
5565
5644
|
|
|
5566
5645
|
const editorLabel = (e: string) =>
|
|
5567
|
-
({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev' })[e] ?? e;
|
|
5646
|
+
({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
|
|
5568
5647
|
|
|
5569
5648
|
console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
|
|
5570
5649
|
const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
|
|
@@ -5604,6 +5683,92 @@ if (process.argv[2] === 'setup') {
|
|
|
5604
5683
|
rl.close();
|
|
5605
5684
|
console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
|
|
5606
5685
|
console.log(` Your AI now has persistent memory across every session.\n`);
|
|
5686
|
+
|
|
5687
|
+
// ── Step 7: Show first session_start preview (Aha moment) ────────────────
|
|
5688
|
+
// Fetch brain health from the API to show the user what their agent will see.
|
|
5689
|
+
process.stdout.write('\n⏳ Fetching your Brain health preview...');
|
|
5690
|
+
try {
|
|
5691
|
+
const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
|
|
5692
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
5693
|
+
signal: AbortSignal.timeout(6000),
|
|
5694
|
+
});
|
|
5695
|
+
if (brainRes.ok) {
|
|
5696
|
+
const brainData = await brainRes.json() as {
|
|
5697
|
+
lesson_count?: number; context_count?: number;
|
|
5698
|
+
open_failures?: number; quality_score?: number;
|
|
5699
|
+
};
|
|
5700
|
+
const lessons = brainData.lesson_count ?? 0;
|
|
5701
|
+
const contexts = brainData.context_count ?? 0;
|
|
5702
|
+
const score = brainData.quality_score ?? 0;
|
|
5703
|
+
const level = lessons === 0 ? 'Intern 🌱' :
|
|
5704
|
+
lessons < 10 ? 'Junior Dev 🔧' :
|
|
5705
|
+
lessons < 30 ? 'Mid Dev ⚡' :
|
|
5706
|
+
lessons < 60 ? 'Senior Dev 🧠' :
|
|
5707
|
+
lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
5708
|
+
console.log(' ✓\n');
|
|
5709
|
+
console.log('┌──────────────────────────────────────────────────────┐');
|
|
5710
|
+
console.log(`│ 🧠 Brain Health Report │`);
|
|
5711
|
+
console.log('├──────────────────────────────────────────────────────┤');
|
|
5712
|
+
console.log(`│ Lessons stored : ${String(lessons).padEnd(6)} │ Level: ${level.padEnd(20)}│`);
|
|
5713
|
+
console.log(`│ Context entries : ${String(contexts).padEnd(6)} │ Quality score: ${String(Math.round(score * 100)).padEnd(3)}% │`);
|
|
5714
|
+
if (lessons === 0) {
|
|
5715
|
+
console.log('├──────────────────────────────────────────────────────┤');
|
|
5716
|
+
console.log('│ Your Brain is empty and ready to learn. │');
|
|
5717
|
+
console.log('│ After your first coding session it will contain: │');
|
|
5718
|
+
console.log('│ • Lessons from every bug you fixed │');
|
|
5719
|
+
console.log('│ • Your project\'s indexed files │');
|
|
5720
|
+
console.log('│ • A session summary for next time │');
|
|
5721
|
+
}
|
|
5722
|
+
console.log('└──────────────────────────────────────────────────────┘');
|
|
5723
|
+
} else {
|
|
5724
|
+
console.log(' (skipped)');
|
|
5725
|
+
}
|
|
5726
|
+
} catch { console.log(' (skipped)'); }
|
|
5727
|
+
|
|
5728
|
+
console.log(`\n Your AI will call session_start automatically when it uses the first tool.`);
|
|
5729
|
+
console.log(` To see your Brain at any time: https://cachly.dev/instances/${instance.id}\n`);
|
|
5730
|
+
process.exit(0);
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5733
|
+
// ── CLI: cachly index <dir> ───────────────────────────────────────────────────
|
|
5734
|
+
// Usage: npx @cachly-dev/mcp-server@latest index [./path/to/project]
|
|
5735
|
+
// Indexes the project directory into the Brain — perfect for CI/CD cron jobs.
|
|
5736
|
+
|
|
5737
|
+
if (process.argv[2] === 'index') {
|
|
5738
|
+
const { resolve } = await import('node:path');
|
|
5739
|
+
const argv = process.argv.slice(3);
|
|
5740
|
+
const flag = (name: string) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
|
|
5741
|
+
|
|
5742
|
+
const dir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
5743
|
+
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
5744
|
+
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
5745
|
+
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
5746
|
+
|
|
5747
|
+
if (!instanceId || !JWT) {
|
|
5748
|
+
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
5749
|
+
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
5750
|
+
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
5751
|
+
console.error(' npx @cachly-dev/mcp-server@latest index ./my-project\n');
|
|
5752
|
+
process.exit(1);
|
|
5753
|
+
}
|
|
5754
|
+
|
|
5755
|
+
console.log(`\n📂 Indexing: ${dir}`);
|
|
5756
|
+
console.log(` Instance: ${instanceId.slice(0, 8)}… Max files: ${maxFiles}\n`);
|
|
5757
|
+
|
|
5758
|
+
try {
|
|
5759
|
+
const result = await handleTool('index_project', {
|
|
5760
|
+
instance_id: instanceId,
|
|
5761
|
+
dir,
|
|
5762
|
+
max_files: maxFiles,
|
|
5763
|
+
ttl: 86400 * 7,
|
|
5764
|
+
namespace,
|
|
5765
|
+
});
|
|
5766
|
+
console.log(result);
|
|
5767
|
+
console.log('\n✅ Indexing complete.\n');
|
|
5768
|
+
} catch (err) {
|
|
5769
|
+
console.error(`\n❌ Indexing failed: ${(err as Error).message}\n`);
|
|
5770
|
+
process.exit(1);
|
|
5771
|
+
}
|
|
5607
5772
|
process.exit(0);
|
|
5608
5773
|
}
|
|
5609
5774
|
|