@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/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 &
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
.
|
|
377
|
-
|
|
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
|
-
|
|
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 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|