@cachly-dev/mcp-server 0.5.35 → 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.35",
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/src/index.ts CHANGED
@@ -1681,6 +1681,104 @@ const TOOLS = [
1681
1681
  required: ['instance_id'],
1682
1682
  },
1683
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
+ },
1684
1782
  {
1685
1783
  name: 'brain_doctor',
1686
1784
  description:
@@ -3603,6 +3701,35 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3603
3701
  }
3604
3702
  } catch { /* non-critical */ }
3605
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
+
3606
3733
  return lines.join('\n');
3607
3734
  }
3608
3735
 
@@ -5137,6 +5264,178 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
5137
5264
  );
5138
5265
  }
5139
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
+
5140
5439
  default:
5141
5440
  return null;
5142
5441
  }