@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 +8 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +451 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/smithery.yaml +10 -5
- package/src/index.ts +473 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cachly-dev/mcp-server",
|
|
3
|
-
"version": "0.5.
|
|
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
|
|
9
|
-
description: "Your Cachly API key
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|