@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.5.34",
3
+ "version": "0.5.36",
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
@@ -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
- /** 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
+ */
387
414
  function tokenize(text: string): string[] {
388
- return text
389
- .toLowerCase()
390
- .replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
391
- .split(/\s+/)
392
- .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;
393
443
  }
394
444
 
395
445
  /**
@@ -1631,6 +1681,104 @@ const TOOLS = [
1631
1681
  required: ['instance_id'],
1632
1682
  },
1633
1683
  },
1684
+ // ── Roadmap — Persistent project plan tracker ───────────────────────────
1685
+ {
1686
+ name: 'roadmap_add',
1687
+ description:
1688
+ 'Add a new item to the persistent project roadmap stored in the Brain. ' +
1689
+ 'Items survive across sessions and editors — the roadmap is always up to date. ' +
1690
+ 'Use for features, bugs, refactors, or any planned work. ' +
1691
+ 'Call roadmap_list to see all open items, roadmap_next to get the next actionable item.',
1692
+ inputSchema: {
1693
+ type: 'object',
1694
+ properties: {
1695
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1696
+ title: { type: 'string', description: 'Short title of the task/feature (3–10 words)' },
1697
+ description: { type: 'string', description: 'What needs to be done, acceptance criteria, context' },
1698
+ priority: {
1699
+ type: 'string',
1700
+ enum: ['critical', 'high', 'medium', 'low'],
1701
+ description: 'Priority level (default: medium)',
1702
+ },
1703
+ tags: {
1704
+ type: 'array',
1705
+ items: { type: 'string' },
1706
+ description: 'Tags for filtering (e.g. ["api", "web", "sdk", "infra"])',
1707
+ },
1708
+ milestone: { type: 'string', description: 'Milestone/epic this belongs to (optional)' },
1709
+ },
1710
+ required: ['instance_id', 'title'],
1711
+ },
1712
+ },
1713
+ {
1714
+ name: 'roadmap_update',
1715
+ description:
1716
+ 'Update the status, priority, or details of a roadmap item. ' +
1717
+ 'Use to move items through the lifecycle: planned → in-progress → done (or blocked/cancelled). ' +
1718
+ 'Also use to add notes/findings while working on an item.',
1719
+ inputSchema: {
1720
+ type: 'object',
1721
+ properties: {
1722
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1723
+ id: { type: 'string', description: 'Item ID returned by roadmap_add or roadmap_list' },
1724
+ status: {
1725
+ type: 'string',
1726
+ enum: ['planned', 'in-progress', 'done', 'blocked', 'cancelled'],
1727
+ description: 'New status for the item',
1728
+ },
1729
+ priority: {
1730
+ type: 'string',
1731
+ enum: ['critical', 'high', 'medium', 'low'],
1732
+ description: 'Updated priority (optional)',
1733
+ },
1734
+ notes: { type: 'string', description: 'Progress notes, findings, or blockers (appended to existing notes)' },
1735
+ title: { type: 'string', description: 'Updated title (optional)' },
1736
+ description: { type: 'string', description: 'Updated description (optional)' },
1737
+ },
1738
+ required: ['instance_id', 'id'],
1739
+ },
1740
+ },
1741
+ {
1742
+ name: 'roadmap_list',
1743
+ description:
1744
+ 'List all roadmap items, optionally filtered by status, priority, tag, or milestone. ' +
1745
+ 'Returns items sorted by priority then creation date. ' +
1746
+ 'Called automatically by session_start to show open work.',
1747
+ inputSchema: {
1748
+ type: 'object',
1749
+ properties: {
1750
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1751
+ status: {
1752
+ type: 'string',
1753
+ enum: ['planned', 'in-progress', 'done', 'blocked', 'cancelled', 'open'],
1754
+ description: 'Filter by status. Use \'open\' to see planned+in-progress+blocked (default: open)',
1755
+ },
1756
+ tag: { type: 'string', description: 'Filter by tag (optional)' },
1757
+ milestone: { type: 'string', description: 'Filter by milestone (optional)' },
1758
+ priority: {
1759
+ type: 'string',
1760
+ enum: ['critical', 'high', 'medium', 'low'],
1761
+ description: 'Filter by minimum priority (optional)',
1762
+ },
1763
+ },
1764
+ required: ['instance_id'],
1765
+ },
1766
+ },
1767
+ {
1768
+ name: 'roadmap_next',
1769
+ description:
1770
+ 'Get the single most important next actionable roadmap item. ' +
1771
+ 'Returns the highest-priority in-progress item first, then planned items, sorted by priority. ' +
1772
+ 'Call at session start to immediately know what to work on next.',
1773
+ inputSchema: {
1774
+ type: 'object',
1775
+ properties: {
1776
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1777
+ tag: { type: 'string', description: 'Filter by tag (optional)' },
1778
+ },
1779
+ required: ['instance_id'],
1780
+ },
1781
+ },
1634
1782
  {
1635
1783
  name: 'brain_doctor',
1636
1784
  description:
@@ -3553,6 +3701,35 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3553
3701
  }
3554
3702
  } catch { /* non-critical */ }
3555
3703
 
3704
+ // ── 🗺️ Roadmap — open items at session start ────────────────────────────
3705
+ try {
3706
+ const roadmapAll = await redis.hgetall(`cachly:roadmap:${instance_id}`);
3707
+ if (roadmapAll && Object.keys(roadmapAll).length > 0) {
3708
+ const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
3709
+ const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
3710
+ const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
3711
+ const allItems = Object.values(roadmapAll).map(v => JSON.parse(v as string) as Record<string, unknown>);
3712
+ const openItems = allItems
3713
+ .filter(i => openStatuses.has(i.status as string))
3714
+ .sort((a, b) => {
3715
+ if (a.status === 'in-progress' && b.status !== 'in-progress') return -1;
3716
+ if (b.status === 'in-progress' && a.status !== 'in-progress') return 1;
3717
+ return (PRIORITY_ORDER[a.priority as string] ?? 99) - (PRIORITY_ORDER[b.priority as string] ?? 99);
3718
+ });
3719
+ const doneCount = allItems.filter(i => i.status === 'done').length;
3720
+ if (openItems.length > 0) {
3721
+ lines.push(`🗺️ **Roadmap** (${openItems.length} open · ${doneCount} done):`);
3722
+ for (const it of openItems.slice(0, 5)) {
3723
+ const statusIcon = it.status === 'in-progress' ? '⚡' : it.status === 'blocked' ? '🚫' : '📋';
3724
+ lines.push(` ${statusIcon} ${PRIORITY_ICON[it.priority as string] ?? '⚪'} \`${it.id}\` **${it.title}**`);
3725
+ }
3726
+ if (openItems.length > 5) lines.push(` … and ${openItems.length - 5} more`);
3727
+ lines.push(` _Use \`roadmap_next\` for the top priority item · \`roadmap_list\` for full view_`);
3728
+ lines.push('');
3729
+ }
3730
+ }
3731
+ } catch { /* non-critical */ }
3732
+
3556
3733
  return lines.join('\n');
3557
3734
  }
3558
3735
 
@@ -5087,6 +5264,178 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
5087
5264
  );
5088
5265
  }
5089
5266
 
5267
+ // ── Roadmap ──────────────────────────────────────────────────────────────
5268
+
5269
+ case 'roadmap_add': {
5270
+ const {
5271
+ instance_id: rid,
5272
+ title,
5273
+ description: desc = '',
5274
+ priority = 'medium',
5275
+ tags: rtags = [],
5276
+ milestone = '',
5277
+ } = args as {
5278
+ instance_id: string; title: string; description?: string;
5279
+ priority?: string; tags?: string[]; milestone?: string;
5280
+ };
5281
+ const redis = await getConnection(rid);
5282
+ const id = `rm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
5283
+ const item = {
5284
+ id, title, description: desc, priority, tags: rtags, milestone,
5285
+ status: 'planned',
5286
+ created: new Date().toISOString(),
5287
+ updated: new Date().toISOString(),
5288
+ notes: '',
5289
+ };
5290
+ await redis.hset(`cachly:roadmap:${rid}`, id, JSON.stringify(item));
5291
+ const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
5292
+ return [
5293
+ `📋 **Roadmap item added**`,
5294
+ ``,
5295
+ ` ID: \`${id}\``,
5296
+ ` Title: ${title}`,
5297
+ ` Priority: ${PRIORITY_ICON[priority] ?? '⚪'} ${priority}`,
5298
+ ` Status: planned`,
5299
+ milestone ? ` Milestone: ${milestone}` : '',
5300
+ rtags.length ? ` Tags: ${rtags.join(', ')}` : '',
5301
+ ``,
5302
+ `💡 Use \`roadmap_update(id: "${id}", status: "in-progress")\` when you start working on it.`,
5303
+ ].filter(Boolean).join('\n');
5304
+ }
5305
+
5306
+ case 'roadmap_update': {
5307
+ const {
5308
+ instance_id: rid,
5309
+ id: itemId,
5310
+ status: newStatus,
5311
+ priority: newPriority,
5312
+ notes: newNotes,
5313
+ title: newTitle,
5314
+ description: newDesc,
5315
+ } = args as {
5316
+ instance_id: string; id: string; status?: string; priority?: string;
5317
+ notes?: string; title?: string; description?: string;
5318
+ };
5319
+ const redis = await getConnection(rid);
5320
+ const raw = await redis.hget(`cachly:roadmap:${rid}`, itemId);
5321
+ if (!raw) return `⚠️ **roadmap_update** – Item \`${itemId}\` not found. Use \`roadmap_list\` to see all items.`;
5322
+ const item = JSON.parse(raw) as Record<string, unknown>;
5323
+ const oldStatus = item.status as string;
5324
+ if (newStatus) item.status = newStatus;
5325
+ if (newPriority) item.priority = newPriority;
5326
+ if (newTitle) item.title = newTitle;
5327
+ if (newDesc) item.description = newDesc;
5328
+ if (newNotes) item.notes = item.notes ? `${item.notes}\n[${new Date().toISOString().slice(0, 10)}] ${newNotes}` : `[${new Date().toISOString().slice(0, 10)}] ${newNotes}`;
5329
+ item.updated = new Date().toISOString();
5330
+ await redis.hset(`cachly:roadmap:${rid}`, itemId, JSON.stringify(item));
5331
+ const statusEmoji: Record<string, string> = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
5332
+ return [
5333
+ `${statusEmoji[newStatus ?? oldStatus] ?? '📋'} **Roadmap updated** \`${itemId}\``,
5334
+ ``,
5335
+ ` Title: ${item.title}`,
5336
+ oldStatus !== newStatus ? ` Status: ${oldStatus} → ${newStatus}` : ` Status: ${item.status}`,
5337
+ newNotes ? ` Notes: ${newNotes}` : '',
5338
+ ].filter(Boolean).join('\n');
5339
+ }
5340
+
5341
+ case 'roadmap_list': {
5342
+ const {
5343
+ instance_id: rid,
5344
+ status: filterStatus = 'open',
5345
+ tag: filterTag,
5346
+ milestone: filterMilestone,
5347
+ priority: filterPriority,
5348
+ } = args as {
5349
+ instance_id: string; status?: string; tag?: string;
5350
+ milestone?: string; priority?: string;
5351
+ };
5352
+ const redis = await getConnection(rid);
5353
+ const all = await redis.hgetall(`cachly:roadmap:${rid}`);
5354
+ if (!all || Object.keys(all).length === 0) {
5355
+ return '📋 **Roadmap is empty.**\n\nUse `roadmap_add` to create your first item.';
5356
+ }
5357
+ const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
5358
+ const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
5359
+ const STATUS_ICON: Record<string, string> = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
5360
+ const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
5361
+ let items = Object.values(all).map(v => JSON.parse(v as string) as Record<string, string | string[]>);
5362
+ // Filter
5363
+ if (filterStatus === 'open') items = items.filter(i => openStatuses.has(i.status as string));
5364
+ else if (filterStatus) items = items.filter(i => i.status === filterStatus);
5365
+ if (filterTag) items = items.filter(i => (i.tags as string[]).includes(filterTag));
5366
+ if (filterMilestone) items = items.filter(i => i.milestone === filterMilestone);
5367
+ if (filterPriority) items = items.filter(i => (PRIORITY_ORDER[i.priority as string] ?? 99) <= (PRIORITY_ORDER[filterPriority] ?? 99));
5368
+ // Sort: priority asc, then created asc
5369
+ items.sort((a, b) => {
5370
+ const pa = PRIORITY_ORDER[a.priority as string] ?? 99;
5371
+ const pb = PRIORITY_ORDER[b.priority as string] ?? 99;
5372
+ return pa !== pb ? pa - pb : (a.created as string).localeCompare(b.created as string);
5373
+ });
5374
+ if (items.length === 0) return `📋 **No roadmap items** match the current filter (status: ${filterStatus}).`;
5375
+ const grouped: Record<string, typeof items> = {};
5376
+ for (const it of items) {
5377
+ const st = it.status as string;
5378
+ if (!grouped[st]) grouped[st] = [];
5379
+ grouped[st].push(it);
5380
+ }
5381
+ const lines: string[] = [`📋 **Roadmap** (${items.length} item${items.length !== 1 ? 's' : ''})`, ''];
5382
+ for (const [st, grp] of Object.entries(grouped)) {
5383
+ lines.push(`**${STATUS_ICON[st] ?? '•'} ${st.toUpperCase()}** (${grp.length})`);
5384
+ for (const it of grp) {
5385
+ const tags = (it.tags as string[]).length ? ` [${(it.tags as string[]).join(', ')}]` : '';
5386
+ const milestone = it.milestone ? ` · ${it.milestone}` : '';
5387
+ lines.push(` ${PRIORITY_ICON[it.priority as string] ?? '⚪'} \`${it.id}\` **${it.title}**${tags}${milestone}`);
5388
+ if (it.description) lines.push(` ${(it.description as string).slice(0, 120)}`);
5389
+ if (it.notes) lines.push(` 📝 ${(it.notes as string).split('\n').pop()?.slice(0, 100)}`);
5390
+ }
5391
+ lines.push('');
5392
+ }
5393
+ lines.push(`💡 \`roadmap_update(id, status: "in-progress")\` to start · \`roadmap_next\` for top priority item`);
5394
+ return lines.join('\n');
5395
+ }
5396
+
5397
+ case 'roadmap_next': {
5398
+ const { instance_id: rid, tag: filterTag } = args as { instance_id: string; tag?: string };
5399
+ const redis = await getConnection(rid);
5400
+ const all = await redis.hgetall(`cachly:roadmap:${rid}`);
5401
+ if (!all || Object.keys(all).length === 0) {
5402
+ return '📋 **Roadmap is empty.** Use `roadmap_add` to plan your first task.';
5403
+ }
5404
+ const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
5405
+ const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
5406
+ let items = Object.values(all)
5407
+ .map(v => JSON.parse(v as string) as Record<string, unknown>)
5408
+ .filter(i => i.status === 'in-progress' || i.status === 'planned')
5409
+ .filter(i => !filterTag || (i.tags as string[]).includes(filterTag));
5410
+ if (items.length === 0) return '🎉 **No open roadmap items!** All tasks are done (or use `roadmap_list` to check).';
5411
+ // in-progress first, then by priority
5412
+ items.sort((a, b) => {
5413
+ if (a.status === 'in-progress' && b.status !== 'in-progress') return -1;
5414
+ if (b.status === 'in-progress' && a.status !== 'in-progress') return 1;
5415
+ return (PRIORITY_ORDER[a.priority as string] ?? 99) - (PRIORITY_ORDER[b.priority as string] ?? 99);
5416
+ });
5417
+ const next = items[0];
5418
+ const remaining = items.length - 1;
5419
+ const tags = (next.tags as string[]).length ? `\nTags: ${(next.tags as string[]).join(', ')}` : '';
5420
+ const milestone = next.milestone ? `\nMilestone: ${next.milestone}` : '';
5421
+ const notes = next.notes ? `\nNotes: ${(next.notes as string).split('\n').pop()?.slice(0, 120)}` : '';
5422
+ return [
5423
+ `${next.status === 'in-progress' ? '⚡' : '📋'} **Next up: ${next.title}**`,
5424
+ ``,
5425
+ `ID: \`${next.id}\``,
5426
+ `Priority: ${PRIORITY_ICON[next.priority as string] ?? '⚪'} ${next.priority}`,
5427
+ `Status: ${next.status}`,
5428
+ next.description ? `\nWhat to do:\n${next.description}` : '',
5429
+ tags, milestone, notes,
5430
+ ``,
5431
+ remaining > 0 ? `(+${remaining} more open item${remaining !== 1 ? 's' : ''} in roadmap)` : '(last open item)',
5432
+ ``,
5433
+ next.status === 'planned'
5434
+ ? `💡 Start with: \`roadmap_update(id: "${next.id}", status: "in-progress")\``
5435
+ : `💡 Finish with: \`roadmap_update(id: "${next.id}", status: "done", notes: "...")\``,
5436
+ ].filter(s => s !== undefined).join('\n');
5437
+ }
5438
+
5090
5439
  default:
5091
5440
  return null;
5092
5441
  }
@@ -5208,6 +5557,8 @@ const EDITOR_FILES: Record<string, string> = {
5208
5557
  windsurf: '.windsurf/mcp.json',
5209
5558
  copilot: '.vscode/mcp.json',
5210
5559
  continue: '.continue/config.json',
5560
+ cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
5561
+ zed: '.zed/settings.json', // Zed project-level context_servers
5211
5562
  };
5212
5563
 
5213
5564
  const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
@@ -5224,6 +5575,20 @@ function buildMcpConfig(apiKey: string, instanceId: string, editor: string): str
5224
5575
  },
5225
5576
  }, null, 2);
5226
5577
  }
5578
+ if (editor === 'zed') {
5579
+ return JSON.stringify({
5580
+ context_servers: {
5581
+ cachly: {
5582
+ command: {
5583
+ path: 'npx',
5584
+ args: ['-y', '@cachly-dev/mcp-server@latest'],
5585
+ env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
5586
+ },
5587
+ settings: {},
5588
+ },
5589
+ },
5590
+ }, null, 2);
5591
+ }
5227
5592
  return JSON.stringify({
5228
5593
  mcpServers: {
5229
5594
  cachly: {
@@ -5556,15 +5921,28 @@ if (process.argv[2] === 'setup') {
5556
5921
  // ── Step 3: Detect editors ────────────────────────────────────────────────
5557
5922
  const cwd = process.cwd();
5558
5923
  const detected: string[] = [];
5924
+ const { homedir } = await import('node:os');
5925
+ const home = homedir();
5559
5926
  // Claude Code: always include (CLAUDE.md is universal)
5560
5927
  detected.push('claude');
5561
5928
  if (existsSync(resolve(cwd, '.cursor'))) detected.push('cursor');
5562
5929
  if (existsSync(resolve(cwd, '.windsurf'))) detected.push('windsurf');
5563
5930
  if (existsSync(resolve(cwd, '.vscode'))) detected.push('copilot');
5564
5931
  if (existsSync(resolve(cwd, '.continue'))) detected.push('continue');
5932
+ // Cline: VS Code extension — detect via globalStorage on the machine
5933
+ const clineGlobalDir = process.platform === 'darwin'
5934
+ ? resolve(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev')
5935
+ : resolve(home, '.vscode/extensions');
5936
+ if (existsSync(clineGlobalDir) && !detected.includes('copilot')) detected.push('cline');
5937
+ else if (existsSync(clineGlobalDir)) detected.push('cline'); // both copilot + cline share .vscode/mcp.json — fine
5938
+ // Zed editor — detect via app data dir
5939
+ const zedDir = process.platform === 'darwin'
5940
+ ? resolve(home, 'Library/Application Support/Zed')
5941
+ : resolve(home, '.config/zed');
5942
+ if (existsSync(zedDir)) detected.push('zed');
5565
5943
 
5566
5944
  const editorLabel = (e: string) =>
5567
- ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev' })[e] ?? e;
5945
+ ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
5568
5946
 
5569
5947
  console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
5570
5948
  const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
@@ -5604,6 +5982,92 @@ if (process.argv[2] === 'setup') {
5604
5982
  rl.close();
5605
5983
  console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
5606
5984
  console.log(` Your AI now has persistent memory across every session.\n`);
5985
+
5986
+ // ── Step 7: Show first session_start preview (Aha moment) ────────────────
5987
+ // Fetch brain health from the API to show the user what their agent will see.
5988
+ process.stdout.write('\n⏳ Fetching your Brain health preview...');
5989
+ try {
5990
+ const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
5991
+ headers: { Authorization: `Bearer ${token}` },
5992
+ signal: AbortSignal.timeout(6000),
5993
+ });
5994
+ if (brainRes.ok) {
5995
+ const brainData = await brainRes.json() as {
5996
+ lesson_count?: number; context_count?: number;
5997
+ open_failures?: number; quality_score?: number;
5998
+ };
5999
+ const lessons = brainData.lesson_count ?? 0;
6000
+ const contexts = brainData.context_count ?? 0;
6001
+ const score = brainData.quality_score ?? 0;
6002
+ const level = lessons === 0 ? 'Intern 🌱' :
6003
+ lessons < 10 ? 'Junior Dev 🔧' :
6004
+ lessons < 30 ? 'Mid Dev ⚡' :
6005
+ lessons < 60 ? 'Senior Dev 🧠' :
6006
+ lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
6007
+ console.log(' ✓\n');
6008
+ console.log('┌──────────────────────────────────────────────────────┐');
6009
+ console.log(`│ 🧠 Brain Health Report │`);
6010
+ console.log('├──────────────────────────────────────────────────────┤');
6011
+ console.log(`│ Lessons stored : ${String(lessons).padEnd(6)} │ Level: ${level.padEnd(20)}│`);
6012
+ console.log(`│ Context entries : ${String(contexts).padEnd(6)} │ Quality score: ${String(Math.round(score * 100)).padEnd(3)}% │`);
6013
+ if (lessons === 0) {
6014
+ console.log('├──────────────────────────────────────────────────────┤');
6015
+ console.log('│ Your Brain is empty and ready to learn. │');
6016
+ console.log('│ After your first coding session it will contain: │');
6017
+ console.log('│ • Lessons from every bug you fixed │');
6018
+ console.log('│ • Your project\'s indexed files │');
6019
+ console.log('│ • A session summary for next time │');
6020
+ }
6021
+ console.log('└──────────────────────────────────────────────────────┘');
6022
+ } else {
6023
+ console.log(' (skipped)');
6024
+ }
6025
+ } catch { console.log(' (skipped)'); }
6026
+
6027
+ console.log(`\n Your AI will call session_start automatically when it uses the first tool.`);
6028
+ console.log(` To see your Brain at any time: https://cachly.dev/instances/${instance.id}\n`);
6029
+ process.exit(0);
6030
+ }
6031
+
6032
+ // ── CLI: cachly index <dir> ───────────────────────────────────────────────────
6033
+ // Usage: npx @cachly-dev/mcp-server@latest index [./path/to/project]
6034
+ // Indexes the project directory into the Brain — perfect for CI/CD cron jobs.
6035
+
6036
+ if (process.argv[2] === 'index') {
6037
+ const { resolve } = await import('node:path');
6038
+ const argv = process.argv.slice(3);
6039
+ const flag = (name: string) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
6040
+
6041
+ const dir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
6042
+ const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
6043
+ const maxFiles = parseInt(flag('max-files') ?? '500', 10);
6044
+ const namespace = flag('namespace') ?? 'cachly:sem:code';
6045
+
6046
+ if (!instanceId || !JWT) {
6047
+ console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
6048
+ console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
6049
+ console.error(' export CACHLY_JWT=<cky_live_...>');
6050
+ console.error(' npx @cachly-dev/mcp-server@latest index ./my-project\n');
6051
+ process.exit(1);
6052
+ }
6053
+
6054
+ console.log(`\n📂 Indexing: ${dir}`);
6055
+ console.log(` Instance: ${instanceId.slice(0, 8)}… Max files: ${maxFiles}\n`);
6056
+
6057
+ try {
6058
+ const result = await handleTool('index_project', {
6059
+ instance_id: instanceId,
6060
+ dir,
6061
+ max_files: maxFiles,
6062
+ ttl: 86400 * 7,
6063
+ namespace,
6064
+ });
6065
+ console.log(result);
6066
+ console.log('\n✅ Indexing complete.\n');
6067
+ } catch (err) {
6068
+ console.error(`\n❌ Indexing failed: ${(err as Error).message}\n`);
6069
+ process.exit(1);
6070
+ }
5607
6071
  process.exit(0);
5608
6072
  }
5609
6073