@cachly-dev/mcp-server 0.5.33 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.5.33",
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 (JWT)"
9
- description: "Your Cachly API key from cachly.dev/dashboard → API Keys"
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 (e.g. 9d4077aa-bfa2-468b-89cd-0a8d8f3ec483)"
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
@@ -41,6 +41,7 @@ import { join, relative, extname } from 'node:path';
41
41
 
42
42
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
43
43
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
44
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
44
45
  import {
45
46
  CallToolRequestSchema,
46
47
  ErrorCode,
@@ -300,13 +301,15 @@ function embedProviderHint(): string {
300
301
  // • Recency boost — newer entries rank higher (exponential decay, 7-day half-life)
301
302
  // • Multi-query splitting — numbered lists, semicolons, conjunctions
302
303
  // • Fuzzy matching with Levenshtein distance ≤ 2
303
- // • 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
304
306
  // • Pipeline Redis reads for performance
305
307
  //
306
308
 
307
309
  /**
308
310
  * Stopwords — filtered out during tokenization.
309
- * Covers: English, German, French, Spanish, Italian, Portuguese.
311
+ * Covers: English, German, French, Spanish, Italian, Portuguese,
312
+ * Chinese (Simplified + Traditional), Japanese, Korean.
310
313
  * Keeps the index small and scores meaningful.
311
314
  */
312
315
  const STOPWORDS = new Set([
@@ -380,15 +383,63 @@ const STOPWORDS = new Set([
380
383
  'eu', 'tu', 'ele', 'ela', 'nós', 'eles', 'elas', 'você', 'vocês',
381
384
  'não', 'já', 'sim', 'bem', 'muito', 'também', 'ainda', 'sempre',
382
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
+ '이다', '하다', '이고', '이며', '라고', '에게', '에서', '으로',
383
403
  ]);
384
404
 
385
- /** Tokenize text into meaningful keywords. Handles unicode (accented chars). */
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
+ */
386
414
  function tokenize(text: string): string[] {
387
- return text
388
- .toLowerCase()
389
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
390
- .split(/\s+/)
391
- .filter(w => w.length > 1 && !STOPWORDS.has(w));
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;
392
443
  }
393
444
 
394
445
  /**
@@ -5207,6 +5258,8 @@ const EDITOR_FILES: Record<string, string> = {
5207
5258
  windsurf: '.windsurf/mcp.json',
5208
5259
  copilot: '.vscode/mcp.json',
5209
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
5210
5263
  };
5211
5264
 
5212
5265
  const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
@@ -5223,6 +5276,20 @@ function buildMcpConfig(apiKey: string, instanceId: string, editor: string): str
5223
5276
  },
5224
5277
  }, null, 2);
5225
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
+ }
5226
5293
  return JSON.stringify({
5227
5294
  mcpServers: {
5228
5295
  cachly: {
@@ -5555,15 +5622,28 @@ if (process.argv[2] === 'setup') {
5555
5622
  // ── Step 3: Detect editors ────────────────────────────────────────────────
5556
5623
  const cwd = process.cwd();
5557
5624
  const detected: string[] = [];
5625
+ const { homedir } = await import('node:os');
5626
+ const home = homedir();
5558
5627
  // Claude Code: always include (CLAUDE.md is universal)
5559
5628
  detected.push('claude');
5560
5629
  if (existsSync(resolve(cwd, '.cursor'))) detected.push('cursor');
5561
5630
  if (existsSync(resolve(cwd, '.windsurf'))) detected.push('windsurf');
5562
5631
  if (existsSync(resolve(cwd, '.vscode'))) detected.push('copilot');
5563
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');
5564
5644
 
5565
5645
  const editorLabel = (e: string) =>
5566
- ({ 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;
5567
5647
 
5568
5648
  console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
5569
5649
  const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
@@ -5603,6 +5683,92 @@ if (process.argv[2] === 'setup') {
5603
5683
  rl.close();
5604
5684
  console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
5605
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
+ }
5606
5772
  process.exit(0);
5607
5773
  }
5608
5774
 
@@ -5655,6 +5821,34 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
5655
5821
  })();
5656
5822
  }
5657
5823
 
5658
- const transport = new StdioServerTransport();
5659
- await server.connect(transport);
5824
+ const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
5825
+
5826
+ if (httpPort) {
5827
+ // ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
5828
+ // Used for Smithery URL deployment: PORT=3000 node dist/index.js
5829
+ const { createServer } = await import('node:http');
5830
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
5831
+ await server.connect(transport);
5832
+ const httpServer = createServer((req, res) => {
5833
+ const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
5834
+ if (url.pathname === '/health') {
5835
+ res.writeHead(200, { 'Content-Type': 'application/json' });
5836
+ res.end(JSON.stringify({ status: 'ok', version: CURRENT_VERSION }));
5837
+ return;
5838
+ }
5839
+ if (url.pathname === '/mcp' || url.pathname === '/') {
5840
+ transport.handleRequest(req, res);
5841
+ return;
5842
+ }
5843
+ res.writeHead(404);
5844
+ res.end('not found');
5845
+ });
5846
+ httpServer.listen(httpPort, () => {
5847
+ process.stderr.write(`cachly-mcp HTTP server listening on :${httpPort}\n`);
5848
+ });
5849
+ } else {
5850
+ // ── stdio mode (default for local editor use) ───────────────────────────
5851
+ const transport = new StdioServerTransport();
5852
+ await server.connect(transport);
5853
+ }
5660
5854