@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 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
@@ -288,12 +288,14 @@ function embedProviderHint() {
288
288
  // • Recency boost — newer entries rank higher (exponential decay, 7-day half-life)
289
289
  // • Multi-query splitting — numbered lists, semicolons, conjunctions
290
290
  // • Fuzzy matching with Levenshtein distance ≤ 2
291
- // • 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
292
293
  // • Pipeline Redis reads for performance
293
294
  //
294
295
  /**
295
296
  * Stopwords — filtered out during tokenization.
296
- * Covers: English, German, French, Spanish, Italian, Portuguese.
297
+ * Covers: English, German, French, Spanish, Italian, Portuguese,
298
+ * Chinese (Simplified + Traditional), Japanese, Korean.
297
299
  * Keeps the index small and scores meaningful.
298
300
  */
299
301
  const STOPWORDS = new Set([
@@ -367,14 +369,61 @@ const STOPWORDS = new Set([
367
369
  'eu', 'tu', 'ele', 'ela', 'nós', 'eles', 'elas', 'você', 'vocês',
368
370
  'não', 'já', 'sim', 'bem', 'muito', 'também', 'ainda', 'sempre',
369
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
+ '이다', '하다', '이고', '이며', '라고', '에게', '에서', '으로',
370
389
  ]);
371
- /** 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
+ */
372
398
  function tokenize(text) {
373
- return text
374
- .toLowerCase()
375
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
376
- .split(/\s+/)
377
- .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;
378
427
  }
379
428
  /**
380
429
  * Split a multi-topic query into sub-queries.
@@ -4650,6 +4699,8 @@ const EDITOR_FILES = {
4650
4699
  windsurf: '.windsurf/mcp.json',
4651
4700
  copilot: '.vscode/mcp.json',
4652
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
4653
4704
  };
4654
4705
  const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
4655
4706
  const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
@@ -4664,6 +4715,20 @@ function buildMcpConfig(apiKey, instanceId, editor) {
4664
4715
  },
4665
4716
  }, null, 2);
4666
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
+ }
4667
4732
  return JSON.stringify({
4668
4733
  mcpServers: {
4669
4734
  cachly: {
@@ -4992,6 +5057,8 @@ if (process.argv[2] === 'setup') {
4992
5057
  // ── Step 3: Detect editors ────────────────────────────────────────────────
4993
5058
  const cwd = process.cwd();
4994
5059
  const detected = [];
5060
+ const { homedir } = await import('node:os');
5061
+ const home = homedir();
4995
5062
  // Claude Code: always include (CLAUDE.md is universal)
4996
5063
  detected.push('claude');
4997
5064
  if (existsSync(resolve(cwd, '.cursor')))
@@ -5002,7 +5069,21 @@ if (process.argv[2] === 'setup') {
5002
5069
  detected.push('copilot');
5003
5070
  if (existsSync(resolve(cwd, '.continue')))
5004
5071
  detected.push('continue');
5005
- 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;
5006
5087
  console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
5007
5088
  const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
5008
5089
  const editorsToSetup = !editorsRaw || editorsRaw.toLowerCase() === 'all'
@@ -5043,6 +5124,86 @@ if (process.argv[2] === 'setup') {
5043
5124
  rl.close();
5044
5125
  console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
5045
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
+ }
5046
5207
  process.exit(0);
5047
5208
  }
5048
5209
  // ── Start ─────────────────────────────────────────────────────────────────────