@cachly-dev/mcp-server 0.5.34 โ†’ 0.5.36

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ๐Ÿง  cachly AI Brain โ€” MCP Server
2
2
 
3
- > **Persistent memory for Claude Code, Cursor, GitHub Copilot & Windsurf.**
3
+ > **Persistent memory for Claude Code, Cursor, GitHub Copilot, Windsurf, Cline & Zed.**
4
4
  > Your AI remembers every lesson, every fix, every architecture decision โ€” forever.
5
5
 
6
6
  <p align="center">
@@ -45,9 +45,15 @@ npx @cachly-dev/mcp-server@latest setup
45
45
  The interactive wizard:
46
46
  1. Signs you in (free, no credit card required)
47
47
  2. Picks or creates your AI Brain instance
48
- 3. **Auto-detects** Cursor, Windsurf, VS Code, Claude Code, Continue.dev
48
+ 3. **Auto-detects** Cursor, Windsurf, VS Code, Claude Code, Continue.dev, **Cline & Zed**
49
49
  4. Writes the correct MCP config for every detected editor
50
50
  5. Creates `CLAUDE.md` with memory rules pre-filled
51
+ 6. Shows your Brain Health snapshot right in the terminal
52
+
53
+ **Index your project** into the Brain for instant semantic search:
54
+ ```bash
55
+ npx @cachly-dev/mcp-server@latest index .
56
+ ```
51
57
 
52
58
  **Result:** Your AI remembers everything. Always.
53
59
 
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.
@@ -1485,6 +1534,100 @@ const TOOLS = [
1485
1534
  required: ['instance_id'],
1486
1535
  },
1487
1536
  },
1537
+ // โ”€โ”€ Roadmap โ€” Persistent project plan tracker โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1538
+ {
1539
+ name: 'roadmap_add',
1540
+ description: 'Add a new item to the persistent project roadmap stored in the Brain. ' +
1541
+ 'Items survive across sessions and editors โ€” the roadmap is always up to date. ' +
1542
+ 'Use for features, bugs, refactors, or any planned work. ' +
1543
+ 'Call roadmap_list to see all open items, roadmap_next to get the next actionable item.',
1544
+ inputSchema: {
1545
+ type: 'object',
1546
+ properties: {
1547
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1548
+ title: { type: 'string', description: 'Short title of the task/feature (3โ€“10 words)' },
1549
+ description: { type: 'string', description: 'What needs to be done, acceptance criteria, context' },
1550
+ priority: {
1551
+ type: 'string',
1552
+ enum: ['critical', 'high', 'medium', 'low'],
1553
+ description: 'Priority level (default: medium)',
1554
+ },
1555
+ tags: {
1556
+ type: 'array',
1557
+ items: { type: 'string' },
1558
+ description: 'Tags for filtering (e.g. ["api", "web", "sdk", "infra"])',
1559
+ },
1560
+ milestone: { type: 'string', description: 'Milestone/epic this belongs to (optional)' },
1561
+ },
1562
+ required: ['instance_id', 'title'],
1563
+ },
1564
+ },
1565
+ {
1566
+ name: 'roadmap_update',
1567
+ description: 'Update the status, priority, or details of a roadmap item. ' +
1568
+ 'Use to move items through the lifecycle: planned โ†’ in-progress โ†’ done (or blocked/cancelled). ' +
1569
+ 'Also use to add notes/findings while working on an item.',
1570
+ inputSchema: {
1571
+ type: 'object',
1572
+ properties: {
1573
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1574
+ id: { type: 'string', description: 'Item ID returned by roadmap_add or roadmap_list' },
1575
+ status: {
1576
+ type: 'string',
1577
+ enum: ['planned', 'in-progress', 'done', 'blocked', 'cancelled'],
1578
+ description: 'New status for the item',
1579
+ },
1580
+ priority: {
1581
+ type: 'string',
1582
+ enum: ['critical', 'high', 'medium', 'low'],
1583
+ description: 'Updated priority (optional)',
1584
+ },
1585
+ notes: { type: 'string', description: 'Progress notes, findings, or blockers (appended to existing notes)' },
1586
+ title: { type: 'string', description: 'Updated title (optional)' },
1587
+ description: { type: 'string', description: 'Updated description (optional)' },
1588
+ },
1589
+ required: ['instance_id', 'id'],
1590
+ },
1591
+ },
1592
+ {
1593
+ name: 'roadmap_list',
1594
+ description: 'List all roadmap items, optionally filtered by status, priority, tag, or milestone. ' +
1595
+ 'Returns items sorted by priority then creation date. ' +
1596
+ 'Called automatically by session_start to show open work.',
1597
+ inputSchema: {
1598
+ type: 'object',
1599
+ properties: {
1600
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1601
+ status: {
1602
+ type: 'string',
1603
+ enum: ['planned', 'in-progress', 'done', 'blocked', 'cancelled', 'open'],
1604
+ description: 'Filter by status. Use \'open\' to see planned+in-progress+blocked (default: open)',
1605
+ },
1606
+ tag: { type: 'string', description: 'Filter by tag (optional)' },
1607
+ milestone: { type: 'string', description: 'Filter by milestone (optional)' },
1608
+ priority: {
1609
+ type: 'string',
1610
+ enum: ['critical', 'high', 'medium', 'low'],
1611
+ description: 'Filter by minimum priority (optional)',
1612
+ },
1613
+ },
1614
+ required: ['instance_id'],
1615
+ },
1616
+ },
1617
+ {
1618
+ name: 'roadmap_next',
1619
+ description: 'Get the single most important next actionable roadmap item. ' +
1620
+ 'Returns the highest-priority in-progress item first, then planned items, sorted by priority. ' +
1621
+ 'Call at session start to immediately know what to work on next.',
1622
+ inputSchema: {
1623
+ type: 'object',
1624
+ properties: {
1625
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1626
+ tag: { type: 'string', description: 'Filter by tag (optional)' },
1627
+ },
1628
+ required: ['instance_id'],
1629
+ },
1630
+ },
1488
1631
  {
1489
1632
  name: 'brain_doctor',
1490
1633
  description: 'Check the health of your AI Brain and get actionable recommendations. ' +
@@ -3152,6 +3295,38 @@ async function handleTool(name, args) {
3152
3295
  }
3153
3296
  }
3154
3297
  catch { /* non-critical */ }
3298
+ // โ”€โ”€ ๐Ÿ—บ๏ธ Roadmap โ€” open items at session start โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
3299
+ try {
3300
+ const roadmapAll = await redis.hgetall(`cachly:roadmap:${instance_id}`);
3301
+ if (roadmapAll && Object.keys(roadmapAll).length > 0) {
3302
+ const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
3303
+ const PRIORITY_ICON = { critical: '๐Ÿ”ด', high: '๐ŸŸ ', medium: '๐ŸŸก', low: '๐Ÿ”ต' };
3304
+ const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
3305
+ const allItems = Object.values(roadmapAll).map(v => JSON.parse(v));
3306
+ const openItems = allItems
3307
+ .filter(i => openStatuses.has(i.status))
3308
+ .sort((a, b) => {
3309
+ if (a.status === 'in-progress' && b.status !== 'in-progress')
3310
+ return -1;
3311
+ if (b.status === 'in-progress' && a.status !== 'in-progress')
3312
+ return 1;
3313
+ return (PRIORITY_ORDER[a.priority] ?? 99) - (PRIORITY_ORDER[b.priority] ?? 99);
3314
+ });
3315
+ const doneCount = allItems.filter(i => i.status === 'done').length;
3316
+ if (openItems.length > 0) {
3317
+ lines.push(`๐Ÿ—บ๏ธ **Roadmap** (${openItems.length} open ยท ${doneCount} done):`);
3318
+ for (const it of openItems.slice(0, 5)) {
3319
+ const statusIcon = it.status === 'in-progress' ? 'โšก' : it.status === 'blocked' ? '๐Ÿšซ' : '๐Ÿ“‹';
3320
+ lines.push(` ${statusIcon} ${PRIORITY_ICON[it.priority] ?? 'โšช'} \`${it.id}\` **${it.title}**`);
3321
+ }
3322
+ if (openItems.length > 5)
3323
+ lines.push(` โ€ฆ and ${openItems.length - 5} more`);
3324
+ lines.push(` _Use \`roadmap_next\` for the top priority item ยท \`roadmap_list\` for full view_`);
3325
+ lines.push('');
3326
+ }
3327
+ }
3328
+ }
3329
+ catch { /* non-critical */ }
3155
3330
  return lines.join('\n');
3156
3331
  }
3157
3332
  // โ”€โ”€ session_end โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -4538,6 +4713,161 @@ async function handleBulkLockStream(name, args) {
4538
4713
  return (`โœ… **cache_stream_get** โ€“ ${len} chunk(s) retrieved for \`${key}\`.\n\n` +
4539
4714
  `**Preview** (first 500 chars):\n\`\`\`\n${preview}${preview.length < chunks.join('').length ? 'โ€ฆ' : ''}\n\`\`\``);
4540
4715
  }
4716
+ // โ”€โ”€ Roadmap โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
4717
+ case 'roadmap_add': {
4718
+ const { instance_id: rid, title, description: desc = '', priority = 'medium', tags: rtags = [], milestone = '', } = args;
4719
+ const redis = await getConnection(rid);
4720
+ const id = `rm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
4721
+ const item = {
4722
+ id, title, description: desc, priority, tags: rtags, milestone,
4723
+ status: 'planned',
4724
+ created: new Date().toISOString(),
4725
+ updated: new Date().toISOString(),
4726
+ notes: '',
4727
+ };
4728
+ await redis.hset(`cachly:roadmap:${rid}`, id, JSON.stringify(item));
4729
+ const PRIORITY_ICON = { critical: '๐Ÿ”ด', high: '๐ŸŸ ', medium: '๐ŸŸก', low: '๐Ÿ”ต' };
4730
+ return [
4731
+ `๐Ÿ“‹ **Roadmap item added**`,
4732
+ ``,
4733
+ ` ID: \`${id}\``,
4734
+ ` Title: ${title}`,
4735
+ ` Priority: ${PRIORITY_ICON[priority] ?? 'โšช'} ${priority}`,
4736
+ ` Status: planned`,
4737
+ milestone ? ` Milestone: ${milestone}` : '',
4738
+ rtags.length ? ` Tags: ${rtags.join(', ')}` : '',
4739
+ ``,
4740
+ `๐Ÿ’ก Use \`roadmap_update(id: "${id}", status: "in-progress")\` when you start working on it.`,
4741
+ ].filter(Boolean).join('\n');
4742
+ }
4743
+ case 'roadmap_update': {
4744
+ const { instance_id: rid, id: itemId, status: newStatus, priority: newPriority, notes: newNotes, title: newTitle, description: newDesc, } = args;
4745
+ const redis = await getConnection(rid);
4746
+ const raw = await redis.hget(`cachly:roadmap:${rid}`, itemId);
4747
+ if (!raw)
4748
+ return `โš ๏ธ **roadmap_update** โ€“ Item \`${itemId}\` not found. Use \`roadmap_list\` to see all items.`;
4749
+ const item = JSON.parse(raw);
4750
+ const oldStatus = item.status;
4751
+ if (newStatus)
4752
+ item.status = newStatus;
4753
+ if (newPriority)
4754
+ item.priority = newPriority;
4755
+ if (newTitle)
4756
+ item.title = newTitle;
4757
+ if (newDesc)
4758
+ item.description = newDesc;
4759
+ if (newNotes)
4760
+ item.notes = item.notes ? `${item.notes}\n[${new Date().toISOString().slice(0, 10)}] ${newNotes}` : `[${new Date().toISOString().slice(0, 10)}] ${newNotes}`;
4761
+ item.updated = new Date().toISOString();
4762
+ await redis.hset(`cachly:roadmap:${rid}`, itemId, JSON.stringify(item));
4763
+ const statusEmoji = { planned: '๐Ÿ“‹', 'in-progress': 'โšก', done: 'โœ…', blocked: '๐Ÿšซ', cancelled: '๐Ÿ—‘๏ธ' };
4764
+ return [
4765
+ `${statusEmoji[newStatus ?? oldStatus] ?? '๐Ÿ“‹'} **Roadmap updated** \`${itemId}\``,
4766
+ ``,
4767
+ ` Title: ${item.title}`,
4768
+ oldStatus !== newStatus ? ` Status: ${oldStatus} โ†’ ${newStatus}` : ` Status: ${item.status}`,
4769
+ newNotes ? ` Notes: ${newNotes}` : '',
4770
+ ].filter(Boolean).join('\n');
4771
+ }
4772
+ case 'roadmap_list': {
4773
+ const { instance_id: rid, status: filterStatus = 'open', tag: filterTag, milestone: filterMilestone, priority: filterPriority, } = args;
4774
+ const redis = await getConnection(rid);
4775
+ const all = await redis.hgetall(`cachly:roadmap:${rid}`);
4776
+ if (!all || Object.keys(all).length === 0) {
4777
+ return '๐Ÿ“‹ **Roadmap is empty.**\n\nUse `roadmap_add` to create your first item.';
4778
+ }
4779
+ const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
4780
+ const PRIORITY_ICON = { critical: '๐Ÿ”ด', high: '๐ŸŸ ', medium: '๐ŸŸก', low: '๐Ÿ”ต' };
4781
+ const STATUS_ICON = { planned: '๐Ÿ“‹', 'in-progress': 'โšก', done: 'โœ…', blocked: '๐Ÿšซ', cancelled: '๐Ÿ—‘๏ธ' };
4782
+ const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
4783
+ let items = Object.values(all).map(v => JSON.parse(v));
4784
+ // Filter
4785
+ if (filterStatus === 'open')
4786
+ items = items.filter(i => openStatuses.has(i.status));
4787
+ else if (filterStatus)
4788
+ items = items.filter(i => i.status === filterStatus);
4789
+ if (filterTag)
4790
+ items = items.filter(i => i.tags.includes(filterTag));
4791
+ if (filterMilestone)
4792
+ items = items.filter(i => i.milestone === filterMilestone);
4793
+ if (filterPriority)
4794
+ items = items.filter(i => (PRIORITY_ORDER[i.priority] ?? 99) <= (PRIORITY_ORDER[filterPriority] ?? 99));
4795
+ // Sort: priority asc, then created asc
4796
+ items.sort((a, b) => {
4797
+ const pa = PRIORITY_ORDER[a.priority] ?? 99;
4798
+ const pb = PRIORITY_ORDER[b.priority] ?? 99;
4799
+ return pa !== pb ? pa - pb : a.created.localeCompare(b.created);
4800
+ });
4801
+ if (items.length === 0)
4802
+ return `๐Ÿ“‹ **No roadmap items** match the current filter (status: ${filterStatus}).`;
4803
+ const grouped = {};
4804
+ for (const it of items) {
4805
+ const st = it.status;
4806
+ if (!grouped[st])
4807
+ grouped[st] = [];
4808
+ grouped[st].push(it);
4809
+ }
4810
+ const lines = [`๐Ÿ“‹ **Roadmap** (${items.length} item${items.length !== 1 ? 's' : ''})`, ''];
4811
+ for (const [st, grp] of Object.entries(grouped)) {
4812
+ lines.push(`**${STATUS_ICON[st] ?? 'โ€ข'} ${st.toUpperCase()}** (${grp.length})`);
4813
+ for (const it of grp) {
4814
+ const tags = it.tags.length ? ` [${it.tags.join(', ')}]` : '';
4815
+ const milestone = it.milestone ? ` ยท ${it.milestone}` : '';
4816
+ lines.push(` ${PRIORITY_ICON[it.priority] ?? 'โšช'} \`${it.id}\` **${it.title}**${tags}${milestone}`);
4817
+ if (it.description)
4818
+ lines.push(` ${it.description.slice(0, 120)}`);
4819
+ if (it.notes)
4820
+ lines.push(` ๐Ÿ“ ${it.notes.split('\n').pop()?.slice(0, 100)}`);
4821
+ }
4822
+ lines.push('');
4823
+ }
4824
+ lines.push(`๐Ÿ’ก \`roadmap_update(id, status: "in-progress")\` to start ยท \`roadmap_next\` for top priority item`);
4825
+ return lines.join('\n');
4826
+ }
4827
+ case 'roadmap_next': {
4828
+ const { instance_id: rid, tag: filterTag } = args;
4829
+ const redis = await getConnection(rid);
4830
+ const all = await redis.hgetall(`cachly:roadmap:${rid}`);
4831
+ if (!all || Object.keys(all).length === 0) {
4832
+ return '๐Ÿ“‹ **Roadmap is empty.** Use `roadmap_add` to plan your first task.';
4833
+ }
4834
+ const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
4835
+ const PRIORITY_ICON = { critical: '๐Ÿ”ด', high: '๐ŸŸ ', medium: '๐ŸŸก', low: '๐Ÿ”ต' };
4836
+ let items = Object.values(all)
4837
+ .map(v => JSON.parse(v))
4838
+ .filter(i => i.status === 'in-progress' || i.status === 'planned')
4839
+ .filter(i => !filterTag || i.tags.includes(filterTag));
4840
+ if (items.length === 0)
4841
+ return '๐ŸŽ‰ **No open roadmap items!** All tasks are done (or use `roadmap_list` to check).';
4842
+ // in-progress first, then by priority
4843
+ items.sort((a, b) => {
4844
+ if (a.status === 'in-progress' && b.status !== 'in-progress')
4845
+ return -1;
4846
+ if (b.status === 'in-progress' && a.status !== 'in-progress')
4847
+ return 1;
4848
+ return (PRIORITY_ORDER[a.priority] ?? 99) - (PRIORITY_ORDER[b.priority] ?? 99);
4849
+ });
4850
+ const next = items[0];
4851
+ const remaining = items.length - 1;
4852
+ const tags = next.tags.length ? `\nTags: ${next.tags.join(', ')}` : '';
4853
+ const milestone = next.milestone ? `\nMilestone: ${next.milestone}` : '';
4854
+ const notes = next.notes ? `\nNotes: ${next.notes.split('\n').pop()?.slice(0, 120)}` : '';
4855
+ return [
4856
+ `${next.status === 'in-progress' ? 'โšก' : '๐Ÿ“‹'} **Next up: ${next.title}**`,
4857
+ ``,
4858
+ `ID: \`${next.id}\``,
4859
+ `Priority: ${PRIORITY_ICON[next.priority] ?? 'โšช'} ${next.priority}`,
4860
+ `Status: ${next.status}`,
4861
+ next.description ? `\nWhat to do:\n${next.description}` : '',
4862
+ tags, milestone, notes,
4863
+ ``,
4864
+ remaining > 0 ? `(+${remaining} more open item${remaining !== 1 ? 's' : ''} in roadmap)` : '(last open item)',
4865
+ ``,
4866
+ next.status === 'planned'
4867
+ ? `๐Ÿ’ก Start with: \`roadmap_update(id: "${next.id}", status: "in-progress")\``
4868
+ : `๐Ÿ’ก Finish with: \`roadmap_update(id: "${next.id}", status: "done", notes: "...")\``,
4869
+ ].filter(s => s !== undefined).join('\n');
4870
+ }
4541
4871
  default:
4542
4872
  return null;
4543
4873
  }
@@ -4650,6 +4980,8 @@ const EDITOR_FILES = {
4650
4980
  windsurf: '.windsurf/mcp.json',
4651
4981
  copilot: '.vscode/mcp.json',
4652
4982
  continue: '.continue/config.json',
4983
+ cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
4984
+ zed: '.zed/settings.json', // Zed project-level context_servers
4653
4985
  };
4654
4986
  const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
4655
4987
  const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
@@ -4664,6 +4996,20 @@ function buildMcpConfig(apiKey, instanceId, editor) {
4664
4996
  },
4665
4997
  }, null, 2);
4666
4998
  }
4999
+ if (editor === 'zed') {
5000
+ return JSON.stringify({
5001
+ context_servers: {
5002
+ cachly: {
5003
+ command: {
5004
+ path: 'npx',
5005
+ args: ['-y', '@cachly-dev/mcp-server@latest'],
5006
+ env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
5007
+ },
5008
+ settings: {},
5009
+ },
5010
+ },
5011
+ }, null, 2);
5012
+ }
4667
5013
  return JSON.stringify({
4668
5014
  mcpServers: {
4669
5015
  cachly: {
@@ -4992,6 +5338,8 @@ if (process.argv[2] === 'setup') {
4992
5338
  // โ”€โ”€ Step 3: Detect editors โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
4993
5339
  const cwd = process.cwd();
4994
5340
  const detected = [];
5341
+ const { homedir } = await import('node:os');
5342
+ const home = homedir();
4995
5343
  // Claude Code: always include (CLAUDE.md is universal)
4996
5344
  detected.push('claude');
4997
5345
  if (existsSync(resolve(cwd, '.cursor')))
@@ -5002,7 +5350,21 @@ if (process.argv[2] === 'setup') {
5002
5350
  detected.push('copilot');
5003
5351
  if (existsSync(resolve(cwd, '.continue')))
5004
5352
  detected.push('continue');
5005
- const editorLabel = (e) => ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev' })[e] ?? e;
5353
+ // Cline: VS Code extension โ€” detect via globalStorage on the machine
5354
+ const clineGlobalDir = process.platform === 'darwin'
5355
+ ? resolve(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev')
5356
+ : resolve(home, '.vscode/extensions');
5357
+ if (existsSync(clineGlobalDir) && !detected.includes('copilot'))
5358
+ detected.push('cline');
5359
+ else if (existsSync(clineGlobalDir))
5360
+ detected.push('cline'); // both copilot + cline share .vscode/mcp.json โ€” fine
5361
+ // Zed editor โ€” detect via app data dir
5362
+ const zedDir = process.platform === 'darwin'
5363
+ ? resolve(home, 'Library/Application Support/Zed')
5364
+ : resolve(home, '.config/zed');
5365
+ if (existsSync(zedDir))
5366
+ detected.push('zed');
5367
+ const editorLabel = (e) => ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
5006
5368
  console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
5007
5369
  const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
5008
5370
  const editorsToSetup = !editorsRaw || editorsRaw.toLowerCase() === 'all'
@@ -5043,6 +5405,86 @@ if (process.argv[2] === 'setup') {
5043
5405
  rl.close();
5044
5406
  console.log(`\n๐Ÿง  Done! Restart your editor โ€” the \`cachly\` MCP tools will appear.`);
5045
5407
  console.log(` Your AI now has persistent memory across every session.\n`);
5408
+ // โ”€โ”€ Step 7: Show first session_start preview (Aha moment) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5409
+ // Fetch brain health from the API to show the user what their agent will see.
5410
+ process.stdout.write('\nโณ Fetching your Brain health preview...');
5411
+ try {
5412
+ const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
5413
+ headers: { Authorization: `Bearer ${token}` },
5414
+ signal: AbortSignal.timeout(6000),
5415
+ });
5416
+ if (brainRes.ok) {
5417
+ const brainData = await brainRes.json();
5418
+ const lessons = brainData.lesson_count ?? 0;
5419
+ const contexts = brainData.context_count ?? 0;
5420
+ const score = brainData.quality_score ?? 0;
5421
+ const level = lessons === 0 ? 'Intern ๐ŸŒฑ' :
5422
+ lessons < 10 ? 'Junior Dev ๐Ÿ”ง' :
5423
+ lessons < 30 ? 'Mid Dev โšก' :
5424
+ lessons < 60 ? 'Senior Dev ๐Ÿง ' :
5425
+ lessons < 100 ? 'Staff Eng ๐Ÿš€' : 'Principal Eng ๐Ÿ†';
5426
+ console.log(' โœ“\n');
5427
+ console.log('โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”');
5428
+ console.log(`โ”‚ ๐Ÿง  Brain Health Report โ”‚`);
5429
+ console.log('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค');
5430
+ console.log(`โ”‚ Lessons stored : ${String(lessons).padEnd(6)} โ”‚ Level: ${level.padEnd(20)}โ”‚`);
5431
+ console.log(`โ”‚ Context entries : ${String(contexts).padEnd(6)} โ”‚ Quality score: ${String(Math.round(score * 100)).padEnd(3)}% โ”‚`);
5432
+ if (lessons === 0) {
5433
+ console.log('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค');
5434
+ console.log('โ”‚ Your Brain is empty and ready to learn. โ”‚');
5435
+ console.log('โ”‚ After your first coding session it will contain: โ”‚');
5436
+ console.log('โ”‚ โ€ข Lessons from every bug you fixed โ”‚');
5437
+ console.log('โ”‚ โ€ข Your project\'s indexed files โ”‚');
5438
+ console.log('โ”‚ โ€ข A session summary for next time โ”‚');
5439
+ }
5440
+ console.log('โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜');
5441
+ }
5442
+ else {
5443
+ console.log(' (skipped)');
5444
+ }
5445
+ }
5446
+ catch {
5447
+ console.log(' (skipped)');
5448
+ }
5449
+ console.log(`\n Your AI will call session_start automatically when it uses the first tool.`);
5450
+ console.log(` To see your Brain at any time: https://cachly.dev/instances/${instance.id}\n`);
5451
+ process.exit(0);
5452
+ }
5453
+ // โ”€โ”€ CLI: cachly index <dir> โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5454
+ // Usage: npx @cachly-dev/mcp-server@latest index [./path/to/project]
5455
+ // Indexes the project directory into the Brain โ€” perfect for CI/CD cron jobs.
5456
+ if (process.argv[2] === 'index') {
5457
+ const { resolve } = await import('node:path');
5458
+ const argv = process.argv.slice(3);
5459
+ const flag = (name) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
5460
+ const dir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
5461
+ const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
5462
+ const maxFiles = parseInt(flag('max-files') ?? '500', 10);
5463
+ const namespace = flag('namespace') ?? 'cachly:sem:code';
5464
+ if (!instanceId || !JWT) {
5465
+ console.error('\nโŒ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
5466
+ console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
5467
+ console.error(' export CACHLY_JWT=<cky_live_...>');
5468
+ console.error(' npx @cachly-dev/mcp-server@latest index ./my-project\n');
5469
+ process.exit(1);
5470
+ }
5471
+ console.log(`\n๐Ÿ“‚ Indexing: ${dir}`);
5472
+ console.log(` Instance: ${instanceId.slice(0, 8)}โ€ฆ Max files: ${maxFiles}\n`);
5473
+ try {
5474
+ const result = await handleTool('index_project', {
5475
+ instance_id: instanceId,
5476
+ dir,
5477
+ max_files: maxFiles,
5478
+ ttl: 86400 * 7,
5479
+ namespace,
5480
+ });
5481
+ console.log(result);
5482
+ console.log('\nโœ… Indexing complete.\n');
5483
+ }
5484
+ catch (err) {
5485
+ console.error(`\nโŒ Indexing failed: ${err.message}\n`);
5486
+ process.exit(1);
5487
+ }
5046
5488
  process.exit(0);
5047
5489
  }
5048
5490
  // โ”€โ”€ Start โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€