@cachly-dev/mcp-server 0.5.7 → 0.5.8

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/dist/index.js CHANGED
@@ -1208,6 +1208,11 @@ const TOOLS = [
1208
1208
  items: { type: 'string' },
1209
1209
  description: 'Topic tags for filtering (e.g. ["bash", "deploy", "env"])',
1210
1210
  },
1211
+ depends_on: {
1212
+ type: 'array',
1213
+ items: { type: 'string' },
1214
+ description: 'Prerequisites this lesson depends on (e.g. ["node:>=20", "docker:running", "wireguard:active"]). When a dependency is marked stale, all dependent lessons get needs_review.',
1215
+ },
1211
1216
  },
1212
1217
  required: ['instance_id', 'topic', 'outcome', 'what_worked'],
1213
1218
  },
@@ -1504,6 +1509,94 @@ const TOOLS = [
1504
1509
  required: ['instance_id', 'framework'],
1505
1510
  },
1506
1511
  },
1512
+ // ── Brain Archaeology + Causal Chain ────────────────────────────────────
1513
+ {
1514
+ name: 'recall_at',
1515
+ description: 'Brain Archaeology — see what a lesson looked like at a specific point in time. ' +
1516
+ '"What did we know about deployments 3 months ago?" ' +
1517
+ 'Returns the history of a topic filtered to entries before the given date. ' +
1518
+ 'Shows how the lesson evolved: failure → partial → success. ' +
1519
+ 'Also useful to understand WHY old code decisions were made.',
1520
+ inputSchema: {
1521
+ type: 'object',
1522
+ properties: {
1523
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1524
+ topic: { type: 'string', description: 'Topic slug to look up, e.g. "deploy:api"' },
1525
+ date: { type: 'string', description: 'ISO date string (e.g. "2026-01-15") — returns entries stored BEFORE this date' },
1526
+ },
1527
+ required: ['instance_id', 'topic', 'date'],
1528
+ },
1529
+ },
1530
+ {
1531
+ name: 'trace_dependency',
1532
+ description: 'Causal Chain — find all lessons that depend on a given prerequisite. ' +
1533
+ '"What lessons are affected if node version changes?" ' +
1534
+ 'When a dependency changes (new version, different provider, new OS), call this to see which lessons need review. ' +
1535
+ 'Lessons store dependencies via the depends_on field in learn_from_attempts.',
1536
+ inputSchema: {
1537
+ type: 'object',
1538
+ properties: {
1539
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1540
+ dependency: { type: 'string', description: 'Dependency to trace (e.g. "node:>=20", "docker:running", "wireguard:active")' },
1541
+ mark_review: { type: 'boolean', description: 'If true, marks all dependent lessons as needs_review (default: false)' },
1542
+ },
1543
+ required: ['instance_id', 'dependency'],
1544
+ },
1545
+ },
1546
+ // ── Team / Org Management ────────────────────────────────────────────────
1547
+ {
1548
+ name: 'list_orgs',
1549
+ description: 'List your Cachly organizations (team/org plans). ' +
1550
+ 'Returns each org with plan, seat count, and member info. ' +
1551
+ 'Org plans (Team €99, Business €299, Enterprise custom) are billed separately from cache tiers.',
1552
+ inputSchema: {
1553
+ type: 'object',
1554
+ properties: {},
1555
+ required: [],
1556
+ },
1557
+ },
1558
+ {
1559
+ name: 'create_org',
1560
+ description: 'Create a new Cachly organization for team collaboration. ' +
1561
+ 'After creation, invite team members with invite_member and upgrade the plan via the billing portal. ' +
1562
+ 'Org plans: Team (€99/mo, 10 seats), Business (€299/mo, 50 seats), Enterprise (custom).',
1563
+ inputSchema: {
1564
+ type: 'object',
1565
+ properties: {
1566
+ name: { type: 'string', description: 'Organization display name (e.g. "Acme Engineering")' },
1567
+ slug: { type: 'string', description: 'URL-safe slug (e.g. "acme-eng"). Auto-generated from name if omitted.' },
1568
+ },
1569
+ required: ['name'],
1570
+ },
1571
+ },
1572
+ {
1573
+ name: 'invite_member',
1574
+ description: 'Invite a team member to a Cachly organization by email. ' +
1575
+ 'They will receive an invite email and can join via the dashboard. ' +
1576
+ 'Roles: owner (full access), admin (manage members + instances), member (read + cache ops).',
1577
+ inputSchema: {
1578
+ type: 'object',
1579
+ properties: {
1580
+ org_id: { type: 'string', description: 'UUID of the organization' },
1581
+ email: { type: 'string', description: 'Email address to invite' },
1582
+ role: { type: 'string', enum: ['admin', 'member'], description: 'Role for the invited member (default: member)' },
1583
+ },
1584
+ required: ['org_id', 'email'],
1585
+ },
1586
+ },
1587
+ {
1588
+ name: 'get_org_plan',
1589
+ description: 'Get the current org plan, seat usage, and billing info for an organization. ' +
1590
+ 'Shows: plan name, price, seats used/max, next billing date. ' +
1591
+ 'To upgrade: use the billing portal URL returned by this tool.',
1592
+ inputSchema: {
1593
+ type: 'object',
1594
+ properties: {
1595
+ org_id: { type: 'string', description: 'UUID of the organization' },
1596
+ },
1597
+ required: ['org_id'],
1598
+ },
1599
+ },
1507
1600
  // ── Legacy / Setup ────────────────────────────────────────────────────────
1508
1601
  {
1509
1602
  name: 'setup_ai_memory',
@@ -1669,6 +1762,63 @@ async function handleTool(name, args) {
1669
1762
  await apiFetch(`/api/v1/instances/${instance_id}`, { method: 'DELETE' });
1670
1763
  return `✅ Instance \`${instance_id}\` has been deleted and all data removed.`;
1671
1764
  }
1765
+ // ── Org / Team management ────────────────────────────────────────────
1766
+ case 'list_orgs': {
1767
+ const res = await apiFetch('/api/v1/orgs');
1768
+ const orgs = res.orgs ?? [];
1769
+ if (orgs.length === 0)
1770
+ return `📭 No organizations yet.\n\nCreate one with \`create_org(name="My Team")\`.\nOrg plans: Team €99/mo (10 seats), Business €299/mo (50 seats), Enterprise custom.`;
1771
+ return [
1772
+ `🏢 **Your organizations (${orgs.length})**\n`,
1773
+ ...orgs.map(o => `• **${o.name}** (\`${o.slug}\`) — plan: ${o.plan} · seats: ${o.member_count ?? '?'}/${o.max_members}\n ID: \`${o.id}\``),
1774
+ `\n_Manage: \`get_org_plan\`, \`invite_member\`, dashboard → /team_`,
1775
+ ].join('\n');
1776
+ }
1777
+ case 'create_org': {
1778
+ const { name: orgName, slug } = args;
1779
+ const body = { name: orgName };
1780
+ if (slug)
1781
+ body.slug = slug;
1782
+ const res = await apiFetch('/api/v1/orgs', {
1783
+ method: 'POST',
1784
+ body: JSON.stringify(body),
1785
+ });
1786
+ return [
1787
+ `✅ **Organization created:** ${res.name}`,
1788
+ ` ID: \`${res.id}\` · Slug: \`${res.slug}\` · Plan: ${res.plan}`,
1789
+ ``,
1790
+ `**Next steps:**`,
1791
+ `1. Invite team members: \`invite_member(org_id="${res.id}", email="dev@example.com")\``,
1792
+ `2. Upgrade plan: open billing portal via dashboard → /team`,
1793
+ ` Team: €99/mo (10 seats) · Business: €299/mo (50 seats)`,
1794
+ ].join('\n');
1795
+ }
1796
+ case 'invite_member': {
1797
+ const { org_id, email, role = 'member' } = args;
1798
+ await apiFetch(`/api/v1/orgs/${org_id}/members`, {
1799
+ method: 'POST',
1800
+ body: JSON.stringify({ email, role }),
1801
+ });
1802
+ return `✅ Invite sent to **${email}** as \`${role}\` in org \`${org_id}\`.\n\nThey will receive an email to join the organization.`;
1803
+ }
1804
+ case 'get_org_plan': {
1805
+ const { org_id } = args;
1806
+ const org = await apiFetch(`/api/v1/orgs/${org_id}`);
1807
+ const accepted = (org.members ?? []).filter(m => m.accepted_at).length;
1808
+ const pending = (org.members ?? []).filter(m => !m.accepted_at).length;
1809
+ const planPrice = { free: '€0', team: '€99/mo', business: '€299/mo', enterprise: 'custom' };
1810
+ return [
1811
+ `🏢 **${org.name}** — Plan: **${org.plan}** (${planPrice[org.plan] ?? org.plan})`,
1812
+ ` Seats: ${accepted} active + ${pending} pending / ${org.max_members} max`,
1813
+ ``,
1814
+ `**Members:**`,
1815
+ ...(org.members ?? []).map(m => ` • ${m.invite_email} (${m.role})${m.accepted_at ? '' : ' — pending'}`),
1816
+ ``,
1817
+ org.stripe_customer_id
1818
+ ? `💳 Billing: managed via Stripe. Upgrade/cancel: dashboard → /billing`
1819
+ : `💳 No payment method yet. Upgrade: dashboard → /billing → Team Plans`,
1820
+ ].join('\n');
1821
+ }
1672
1822
  // ── Live cache operations ────────────────────────────────────────────
1673
1823
  case 'cache_get': {
1674
1824
  const { instance_id, key } = args;
@@ -2253,7 +2403,7 @@ async function handleTool(name, args) {
2253
2403
  return `🗑️ **Forgot ${deleted} context entries.**\n\nKeys: ${keys.map(k => `\`${k}\``).join(', ')}`;
2254
2404
  }
2255
2405
  case 'learn_from_attempts': {
2256
- const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], } = args;
2406
+ const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], } = args;
2257
2407
  const redis = await getConnection(instance_id);
2258
2408
  const ts = new Date().toISOString();
2259
2409
  // ── Structured template hints ──────────────────────────────────────────
@@ -2299,6 +2449,45 @@ async function handleTool(name, args) {
2299
2449
  else {
2300
2450
  auditTrail = [{ ts, action: 'created' }];
2301
2451
  }
2452
+ // ── "I Was Wrong" Protocol — failure attribution ───────────────────────
2453
+ const iWasWrongWarning = [];
2454
+ if (outcome === 'failure') {
2455
+ // Search for related success lessons that might have prevented this failure
2456
+ const scanKeys = [];
2457
+ const scanStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 100 });
2458
+ await new Promise((res, rej) => {
2459
+ scanStream.on('data', (b) => scanKeys.push(...b));
2460
+ scanStream.on('end', res);
2461
+ scanStream.on('error', rej);
2462
+ });
2463
+ const topicWords = topic.split(/[:\-_]/).filter(w => w.length > 2);
2464
+ for (const k of scanKeys.slice(0, 50)) {
2465
+ const raw = await redis.get(k);
2466
+ if (!raw)
2467
+ continue;
2468
+ try {
2469
+ const l = JSON.parse(raw);
2470
+ if (l.outcome !== 'success')
2471
+ continue;
2472
+ const lWords = (l.topic ?? '').split(/[:\-_]/).filter(w => w.length > 2);
2473
+ const overlap = topicWords.filter(w => lWords.includes(w)).length;
2474
+ if (overlap >= 1 && l.topic !== topic) {
2475
+ iWasWrongWarning.push(`⚠️ **"I Was Wrong"**: lesson \`${l.topic}\` (success, ${l.severity ?? 'major'}) might have prevented this failure.`, ` → Use \`recall_best_solution(topic="${l.topic}")\` before next attempt.`, ` → To mark it critical: \`learn_from_attempts(topic="${l.topic}", ..., severity="critical")\``);
2476
+ break; // only show the most relevant match
2477
+ }
2478
+ }
2479
+ catch { /* skip */ }
2480
+ }
2481
+ }
2482
+ // ── Register dependency index for causal chain ─────────────────────────
2483
+ for (const dep of depends_on) {
2484
+ const depKey = `cachly:dep:${dep}`;
2485
+ const existing = await redis.get(depKey);
2486
+ const depTopics = existing ? JSON.parse(existing) : [];
2487
+ if (!depTopics.includes(topic))
2488
+ depTopics.push(topic);
2489
+ await redis.set(depKey, JSON.stringify(depTopics));
2490
+ }
2302
2491
  const lessonObj = {
2303
2492
  topic,
2304
2493
  outcome,
@@ -2309,6 +2498,7 @@ async function handleTool(name, args) {
2309
2498
  file_paths,
2310
2499
  commands,
2311
2500
  tags,
2501
+ depends_on,
2312
2502
  recall_count: recallCount,
2313
2503
  ts,
2314
2504
  verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
@@ -2351,7 +2541,11 @@ async function handleTool(name, args) {
2351
2541
  isUpdate
2352
2542
  ? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
2353
2543
  : `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
2544
+ depends_on.length > 0
2545
+ ? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
2546
+ : '',
2354
2547
  ...templateWarnings,
2548
+ ...iWasWrongWarning,
2355
2549
  ].filter(l => l !== '').join('\n');
2356
2550
  }
2357
2551
  case 'recall_best_solution': {
@@ -3260,6 +3454,86 @@ async function handleTool(name, args) {
3260
3454
  }
3261
3455
  return lines.join('\n');
3262
3456
  }
3457
+ // ── recall_at — Brain Archaeology ────────────────────────────────────────
3458
+ case 'recall_at': {
3459
+ const { instance_id, topic, date } = args;
3460
+ const redis = await getConnection(instance_id);
3461
+ const cutoff = new Date(date).getTime();
3462
+ if (isNaN(cutoff))
3463
+ return `❌ Invalid date "${date}". Use ISO format: "2026-01-15"`;
3464
+ const listKey = `cachly:lessons:${topic}`;
3465
+ const all = await redis.lrange(listKey, 0, -1);
3466
+ if (all.length === 0)
3467
+ return `📭 No history found for \`${topic}\`. Lessons are stored via \`learn_from_attempts\`.`;
3468
+ const before = all
3469
+ .map(raw => { try {
3470
+ return JSON.parse(raw);
3471
+ }
3472
+ catch {
3473
+ return null;
3474
+ } })
3475
+ .filter((l) => l !== null && new Date(l.ts).getTime() <= cutoff)
3476
+ .sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
3477
+ if (before.length === 0)
3478
+ return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${new Date(JSON.parse(all[0]).ts).toLocaleDateString('de-DE')}.`;
3479
+ const lines = [
3480
+ `🏺 **Brain Archaeology: \`${topic}\` before ${date}**`,
3481
+ `_${before.length} of ${all.length} total entries shown_`,
3482
+ '',
3483
+ ];
3484
+ for (const l of before.slice(-10)) {
3485
+ const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
3486
+ const d = new Date(l.ts).toLocaleDateString('de-DE');
3487
+ lines.push(`**${d}** ${emoji} ${l.outcome}`);
3488
+ if (l.what_worked)
3489
+ lines.push(` → ${l.what_worked.slice(0, 100)}`);
3490
+ lines.push('');
3491
+ }
3492
+ lines.push(`_Full evolution: ${all.map(r => { try {
3493
+ const l = JSON.parse(r);
3494
+ return l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
3495
+ }
3496
+ catch {
3497
+ return '?';
3498
+ } }).join(' → ')}_`);
3499
+ return lines.join('\n');
3500
+ }
3501
+ // ── trace_dependency — Causal Chain ──────────────────────────────────────
3502
+ case 'trace_dependency': {
3503
+ const { instance_id, dependency, mark_review = false } = args;
3504
+ const redis = await getConnection(instance_id);
3505
+ const depKey = `cachly:dep:${dependency}`;
3506
+ const raw = await redis.get(depKey);
3507
+ if (!raw)
3508
+ return `📭 No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
3509
+ const topics = JSON.parse(raw);
3510
+ const lines = [
3511
+ `🔗 **Causal Chain: \`${dependency}\`** — ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
3512
+ '',
3513
+ ];
3514
+ for (const t of topics) {
3515
+ const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
3516
+ if (!lessonRaw) {
3517
+ lines.push(` • \`${t}\` _(lesson deleted)_`);
3518
+ continue;
3519
+ }
3520
+ const lesson = JSON.parse(lessonRaw);
3521
+ const emoji = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'partial' ? '⚠️' : '❌';
3522
+ const reviewBadge = lesson.needs_review ? ' 🔍 **needs_review**' : '';
3523
+ lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
3524
+ if (mark_review) {
3525
+ const updated = { ...lesson, needs_review: true };
3526
+ await redis.set(`cachly:lesson:best:${t}`, JSON.stringify(updated));
3527
+ }
3528
+ }
3529
+ if (mark_review) {
3530
+ lines.push('', `🔍 All ${topics.length} lessons marked as **needs_review** — verify they still work with the changed dependency.`);
3531
+ }
3532
+ else {
3533
+ lines.push('', `_Run with \`mark_review: true\` to flag all dependent lessons for re-verification._`);
3534
+ }
3535
+ return lines.join('\n');
3536
+ }
3263
3537
  // ── global_learn ──────────────────────────────────────────────────────────
3264
3538
  case 'global_learn': {
3265
3539
  const { instance_id, topic, lesson, severity = 'minor', tags = [] } = args;