@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/Dockerfile CHANGED
@@ -1,7 +1,16 @@
1
+ FROM node:22-alpine AS builder
2
+ WORKDIR /app
3
+ COPY package*.json tsconfig*.json ./
4
+ RUN npm ci --ignore-scripts
5
+ COPY src/ ./src/
6
+ RUN npm run build
7
+
1
8
  FROM node:22-alpine
2
9
  WORKDIR /app
3
10
  COPY package*.json ./
4
- RUN npm ci --omit=dev
5
- COPY dist/ ./dist/
11
+ RUN npm ci --omit=dev --ignore-scripts
12
+ COPY --from=builder /app/dist/ ./dist/
13
+ ENV NODE_ENV=production
14
+ ENV PORT=3000
6
15
  EXPOSE 3000
7
16
  ENTRYPOINT ["node", "dist/index.js"]
package/dist/index.d.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Stopwords — filtered out during tokenization.
4
- * Covers: English, German, French, Spanish, Italian, Portuguese.
4
+ * Covers: English, German, French, Spanish, Italian, Portuguese,
5
+ * Chinese (Simplified + Traditional), Japanese, Korean.
5
6
  * Keeps the index small and scores meaningful.
6
7
  */
7
8
  declare const STOPWORDS: Set<string>;
8
- /** Tokenize text into meaningful keywords. Handles unicode (accented chars). */
9
+ /**
10
+ * Tokenize text into meaningful keywords.
11
+ * Handles accented Latin characters and CJK (Chinese, Japanese, Korean).
12
+ * CJK text is split into character bigrams (the standard for indexing logographic languages).
13
+ */
9
14
  declare function tokenize(text: string): string[];
10
15
  /**
11
16
  * Split a multi-topic query into sub-queries.
package/dist/index.js CHANGED
@@ -40,6 +40,7 @@ import { join, relative, extname } from 'node:path';
40
40
  */
41
41
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
42
42
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
43
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
43
44
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
44
45
  import { Redis } from 'ioredis';
45
46
  // ── Config ────────────────────────────────────────────────────────────────────
@@ -287,12 +288,14 @@ function embedProviderHint() {
287
288
  // • Recency boost — newer entries rank higher (exponential decay, 7-day half-life)
288
289
  // • Multi-query splitting — numbered lists, semicolons, conjunctions
289
290
  // • Fuzzy matching with Levenshtein distance ≤ 2
290
- // • Multilingual stopwords (EN, DE, FR, ES, IT, PT)
291
+ // • Multilingual stopwords (EN, DE, FR, ES, IT, PT, ZH, JA, KO)
292
+ // • CJK character bigram extraction — handles Chinese, Japanese, Korean
291
293
  // • Pipeline Redis reads for performance
292
294
  //
293
295
  /**
294
296
  * Stopwords — filtered out during tokenization.
295
- * Covers: English, German, French, Spanish, Italian, Portuguese.
297
+ * Covers: English, German, French, Spanish, Italian, Portuguese,
298
+ * Chinese (Simplified + Traditional), Japanese, Korean.
296
299
  * Keeps the index small and scores meaningful.
297
300
  */
298
301
  const STOPWORDS = new Set([
@@ -366,14 +369,61 @@ const STOPWORDS = new Set([
366
369
  'eu', 'tu', 'ele', 'ela', 'nós', 'eles', 'elas', 'você', 'vocês',
367
370
  'não', 'já', 'sim', 'bem', 'muito', 'também', 'ainda', 'sempre',
368
371
  'todo', 'toda', 'todos', 'cada', 'outro', 'outra', 'quando', 'porque',
372
+ // ── Chinese (Simplified + Traditional) — high-frequency function characters ──
373
+ '的', '了', '是', '在', '不', '和', '我', '他', '这', '中', '大', '为',
374
+ '上', '个', '国', '以', '要', '就', '出', '说', '们', '有', '来', '到',
375
+ '时', '地', '年', '得', '着', '那', '过', '后', '还', '与', '也', '可',
376
+ '于', '从', '但', '而', '被', '把', '让', '使', '对', '很', '都', '一',
377
+ '会', '没', '人', '它', '这个', '那个', '什么', '如果', '因为', '所以',
378
+ '已经', '可以', '这些', '那些', '我们', '他们', '她们', '它们',
379
+ // ── Japanese — common hiragana particles and auxiliary verbs ──
380
+ 'の', 'は', 'が', 'を', 'に', 'で', 'と', 'も', 'や', 'か', 'な', 'ね',
381
+ 'よ', 'わ', 'て', 'い', 'う', 'え', 'お', 'き', 'く', 'け', 'こ', 'さ',
382
+ 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'ぬ', 'ん', 'から', 'まで',
383
+ 'より', 'へ', 'です', 'ます', 'ない', 'ある', 'いる', 'する', 'こと',
384
+ 'もの', 'ので', 'では', 'には', 'との', 'への', 'から', 'まで',
385
+ // ── Korean — common particles and auxiliary forms ──
386
+ '이', '가', '은', '는', '을', '를', '의', '에', '와', '과', '도',
387
+ '로', '에서', '한', '하', '있', '없', '것', '수', '않', '들',
388
+ '이다', '하다', '이고', '이며', '라고', '에게', '에서', '으로',
369
389
  ]);
370
- /** Tokenize text into meaningful keywords. Handles unicode (accented chars). */
390
+ // CJK Unicode ranges used for bigram extraction
391
+ const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
392
+ const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+|[^\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+)/g;
393
+ /**
394
+ * Tokenize text into meaningful keywords.
395
+ * Handles accented Latin characters and CJK (Chinese, Japanese, Korean).
396
+ * CJK text is split into character bigrams (the standard for indexing logographic languages).
397
+ */
371
398
  function tokenize(text) {
372
- return text
373
- .toLowerCase()
374
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
375
- .split(/\s+/)
376
- .filter(w => w.length > 1 && !STOPWORDS.has(w));
399
+ const tokens = [];
400
+ const lower = text.toLowerCase();
401
+ for (const [seg] of lower.matchAll(SEGMENT_RE)) {
402
+ if (CJK_RE.test(seg)) {
403
+ // CJK segment — extract overlapping character bigrams
404
+ for (let i = 0; i < seg.length - 1; i++) {
405
+ const bigram = seg[i] + seg[i + 1];
406
+ if (!STOPWORDS.has(seg[i]) && !STOPWORDS.has(bigram)) {
407
+ tokens.push(bigram);
408
+ }
409
+ }
410
+ // Trailing single character (for 1-char CJK terms)
411
+ if (seg.length >= 1) {
412
+ const last = seg[seg.length - 1];
413
+ if (!STOPWORDS.has(last))
414
+ tokens.push(last);
415
+ }
416
+ }
417
+ else {
418
+ // Latin / other — existing logic
419
+ const words = seg
420
+ .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
421
+ .split(/\s+/)
422
+ .filter(w => w.length > 1 && !STOPWORDS.has(w));
423
+ tokens.push(...words);
424
+ }
425
+ }
426
+ return tokens;
377
427
  }
378
428
  /**
379
429
  * Split a multi-topic query into sub-queries.
@@ -4649,6 +4699,8 @@ const EDITOR_FILES = {
4649
4699
  windsurf: '.windsurf/mcp.json',
4650
4700
  copilot: '.vscode/mcp.json',
4651
4701
  continue: '.continue/config.json',
4702
+ cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
4703
+ zed: '.zed/settings.json', // Zed project-level context_servers
4652
4704
  };
4653
4705
  const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
4654
4706
  const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
@@ -4663,6 +4715,20 @@ function buildMcpConfig(apiKey, instanceId, editor) {
4663
4715
  },
4664
4716
  }, null, 2);
4665
4717
  }
4718
+ if (editor === 'zed') {
4719
+ return JSON.stringify({
4720
+ context_servers: {
4721
+ cachly: {
4722
+ command: {
4723
+ path: 'npx',
4724
+ args: ['-y', '@cachly-dev/mcp-server@latest'],
4725
+ env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
4726
+ },
4727
+ settings: {},
4728
+ },
4729
+ },
4730
+ }, null, 2);
4731
+ }
4666
4732
  return JSON.stringify({
4667
4733
  mcpServers: {
4668
4734
  cachly: {
@@ -4991,6 +5057,8 @@ if (process.argv[2] === 'setup') {
4991
5057
  // ── Step 3: Detect editors ────────────────────────────────────────────────
4992
5058
  const cwd = process.cwd();
4993
5059
  const detected = [];
5060
+ const { homedir } = await import('node:os');
5061
+ const home = homedir();
4994
5062
  // Claude Code: always include (CLAUDE.md is universal)
4995
5063
  detected.push('claude');
4996
5064
  if (existsSync(resolve(cwd, '.cursor')))
@@ -5001,7 +5069,21 @@ if (process.argv[2] === 'setup') {
5001
5069
  detected.push('copilot');
5002
5070
  if (existsSync(resolve(cwd, '.continue')))
5003
5071
  detected.push('continue');
5004
- const editorLabel = (e) => ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev' })[e] ?? e;
5072
+ // Cline: VS Code extension — detect via globalStorage on the machine
5073
+ const clineGlobalDir = process.platform === 'darwin'
5074
+ ? resolve(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev')
5075
+ : resolve(home, '.vscode/extensions');
5076
+ if (existsSync(clineGlobalDir) && !detected.includes('copilot'))
5077
+ detected.push('cline');
5078
+ else if (existsSync(clineGlobalDir))
5079
+ detected.push('cline'); // both copilot + cline share .vscode/mcp.json — fine
5080
+ // Zed editor — detect via app data dir
5081
+ const zedDir = process.platform === 'darwin'
5082
+ ? resolve(home, 'Library/Application Support/Zed')
5083
+ : resolve(home, '.config/zed');
5084
+ if (existsSync(zedDir))
5085
+ detected.push('zed');
5086
+ const editorLabel = (e) => ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
5005
5087
  console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
5006
5088
  const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
5007
5089
  const editorsToSetup = !editorsRaw || editorsRaw.toLowerCase() === 'all'
@@ -5042,6 +5124,86 @@ if (process.argv[2] === 'setup') {
5042
5124
  rl.close();
5043
5125
  console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
5044
5126
  console.log(` Your AI now has persistent memory across every session.\n`);
5127
+ // ── Step 7: Show first session_start preview (Aha moment) ────────────────
5128
+ // Fetch brain health from the API to show the user what their agent will see.
5129
+ process.stdout.write('\n⏳ Fetching your Brain health preview...');
5130
+ try {
5131
+ const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
5132
+ headers: { Authorization: `Bearer ${token}` },
5133
+ signal: AbortSignal.timeout(6000),
5134
+ });
5135
+ if (brainRes.ok) {
5136
+ const brainData = await brainRes.json();
5137
+ const lessons = brainData.lesson_count ?? 0;
5138
+ const contexts = brainData.context_count ?? 0;
5139
+ const score = brainData.quality_score ?? 0;
5140
+ const level = lessons === 0 ? 'Intern 🌱' :
5141
+ lessons < 10 ? 'Junior Dev 🔧' :
5142
+ lessons < 30 ? 'Mid Dev ⚡' :
5143
+ lessons < 60 ? 'Senior Dev 🧠' :
5144
+ lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
5145
+ console.log(' ✓\n');
5146
+ console.log('┌──────────────────────────────────────────────────────┐');
5147
+ console.log(`│ 🧠 Brain Health Report │`);
5148
+ console.log('├──────────────────────────────────────────────────────┤');
5149
+ console.log(`│ Lessons stored : ${String(lessons).padEnd(6)} │ Level: ${level.padEnd(20)}│`);
5150
+ console.log(`│ Context entries : ${String(contexts).padEnd(6)} │ Quality score: ${String(Math.round(score * 100)).padEnd(3)}% │`);
5151
+ if (lessons === 0) {
5152
+ console.log('├──────────────────────────────────────────────────────┤');
5153
+ console.log('│ Your Brain is empty and ready to learn. │');
5154
+ console.log('│ After your first coding session it will contain: │');
5155
+ console.log('│ • Lessons from every bug you fixed │');
5156
+ console.log('│ • Your project\'s indexed files │');
5157
+ console.log('│ • A session summary for next time │');
5158
+ }
5159
+ console.log('└──────────────────────────────────────────────────────┘');
5160
+ }
5161
+ else {
5162
+ console.log(' (skipped)');
5163
+ }
5164
+ }
5165
+ catch {
5166
+ console.log(' (skipped)');
5167
+ }
5168
+ console.log(`\n Your AI will call session_start automatically when it uses the first tool.`);
5169
+ console.log(` To see your Brain at any time: https://cachly.dev/instances/${instance.id}\n`);
5170
+ process.exit(0);
5171
+ }
5172
+ // ── CLI: cachly index <dir> ───────────────────────────────────────────────────
5173
+ // Usage: npx @cachly-dev/mcp-server@latest index [./path/to/project]
5174
+ // Indexes the project directory into the Brain — perfect for CI/CD cron jobs.
5175
+ if (process.argv[2] === 'index') {
5176
+ const { resolve } = await import('node:path');
5177
+ const argv = process.argv.slice(3);
5178
+ const flag = (name) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
5179
+ const dir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
5180
+ const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
5181
+ const maxFiles = parseInt(flag('max-files') ?? '500', 10);
5182
+ const namespace = flag('namespace') ?? 'cachly:sem:code';
5183
+ if (!instanceId || !JWT) {
5184
+ console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
5185
+ console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
5186
+ console.error(' export CACHLY_JWT=<cky_live_...>');
5187
+ console.error(' npx @cachly-dev/mcp-server@latest index ./my-project\n');
5188
+ process.exit(1);
5189
+ }
5190
+ console.log(`\n📂 Indexing: ${dir}`);
5191
+ console.log(` Instance: ${instanceId.slice(0, 8)}… Max files: ${maxFiles}\n`);
5192
+ try {
5193
+ const result = await handleTool('index_project', {
5194
+ instance_id: instanceId,
5195
+ dir,
5196
+ max_files: maxFiles,
5197
+ ttl: 86400 * 7,
5198
+ namespace,
5199
+ });
5200
+ console.log(result);
5201
+ console.log('\n✅ Indexing complete.\n');
5202
+ }
5203
+ catch (err) {
5204
+ console.error(`\n❌ Indexing failed: ${err.message}\n`);
5205
+ process.exit(1);
5206
+ }
5045
5207
  process.exit(0);
5046
5208
  }
5047
5209
  // ── Start ─────────────────────────────────────────────────────────────────────
@@ -5084,6 +5246,34 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
5084
5246
  catch { /* ignore – network unavailable or timeout */ }
5085
5247
  })();
5086
5248
  }
5087
- const transport = new StdioServerTransport();
5088
- await server.connect(transport);
5249
+ const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
5250
+ if (httpPort) {
5251
+ // ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
5252
+ // Used for Smithery URL deployment: PORT=3000 node dist/index.js
5253
+ const { createServer } = await import('node:http');
5254
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
5255
+ await server.connect(transport);
5256
+ const httpServer = createServer((req, res) => {
5257
+ const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
5258
+ if (url.pathname === '/health') {
5259
+ res.writeHead(200, { 'Content-Type': 'application/json' });
5260
+ res.end(JSON.stringify({ status: 'ok', version: CURRENT_VERSION }));
5261
+ return;
5262
+ }
5263
+ if (url.pathname === '/mcp' || url.pathname === '/') {
5264
+ transport.handleRequest(req, res);
5265
+ return;
5266
+ }
5267
+ res.writeHead(404);
5268
+ res.end('not found');
5269
+ });
5270
+ httpServer.listen(httpPort, () => {
5271
+ process.stderr.write(`cachly-mcp HTTP server listening on :${httpPort}\n`);
5272
+ });
5273
+ }
5274
+ else {
5275
+ // ── stdio mode (default for local editor use) ───────────────────────────
5276
+ const transport = new StdioServerTransport();
5277
+ await server.connect(transport);
5278
+ }
5089
5279
  //# sourceMappingURL=index.js.map