@cachly-dev/mcp-server 0.5.6 → 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 +331 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +334 -2
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1343,6 +1343,11 @@ const TOOLS = [
|
|
|
1343
1343
|
items: { type: 'string' },
|
|
1344
1344
|
description: 'Topic tags for filtering (e.g. ["bash", "deploy", "env"])',
|
|
1345
1345
|
},
|
|
1346
|
+
depends_on: {
|
|
1347
|
+
type: 'array',
|
|
1348
|
+
items: { type: 'string' },
|
|
1349
|
+
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.',
|
|
1350
|
+
},
|
|
1346
1351
|
},
|
|
1347
1352
|
required: ['instance_id', 'topic', 'outcome', 'what_worked'],
|
|
1348
1353
|
},
|
|
@@ -1653,6 +1658,100 @@ const TOOLS = [
|
|
|
1653
1658
|
required: ['instance_id', 'framework'],
|
|
1654
1659
|
},
|
|
1655
1660
|
},
|
|
1661
|
+
// ── Brain Archaeology + Causal Chain ────────────────────────────────────
|
|
1662
|
+
{
|
|
1663
|
+
name: 'recall_at',
|
|
1664
|
+
description:
|
|
1665
|
+
'Brain Archaeology — see what a lesson looked like at a specific point in time. ' +
|
|
1666
|
+
'"What did we know about deployments 3 months ago?" ' +
|
|
1667
|
+
'Returns the history of a topic filtered to entries before the given date. ' +
|
|
1668
|
+
'Shows how the lesson evolved: failure → partial → success. ' +
|
|
1669
|
+
'Also useful to understand WHY old code decisions were made.',
|
|
1670
|
+
inputSchema: {
|
|
1671
|
+
type: 'object',
|
|
1672
|
+
properties: {
|
|
1673
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1674
|
+
topic: { type: 'string', description: 'Topic slug to look up, e.g. "deploy:api"' },
|
|
1675
|
+
date: { type: 'string', description: 'ISO date string (e.g. "2026-01-15") — returns entries stored BEFORE this date' },
|
|
1676
|
+
},
|
|
1677
|
+
required: ['instance_id', 'topic', 'date'],
|
|
1678
|
+
},
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
name: 'trace_dependency',
|
|
1682
|
+
description:
|
|
1683
|
+
'Causal Chain — find all lessons that depend on a given prerequisite. ' +
|
|
1684
|
+
'"What lessons are affected if node version changes?" ' +
|
|
1685
|
+
'When a dependency changes (new version, different provider, new OS), call this to see which lessons need review. ' +
|
|
1686
|
+
'Lessons store dependencies via the depends_on field in learn_from_attempts.',
|
|
1687
|
+
inputSchema: {
|
|
1688
|
+
type: 'object',
|
|
1689
|
+
properties: {
|
|
1690
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1691
|
+
dependency: { type: 'string', description: 'Dependency to trace (e.g. "node:>=20", "docker:running", "wireguard:active")' },
|
|
1692
|
+
mark_review: { type: 'boolean', description: 'If true, marks all dependent lessons as needs_review (default: false)' },
|
|
1693
|
+
},
|
|
1694
|
+
required: ['instance_id', 'dependency'],
|
|
1695
|
+
},
|
|
1696
|
+
},
|
|
1697
|
+
// ── Team / Org Management ────────────────────────────────────────────────
|
|
1698
|
+
{
|
|
1699
|
+
name: 'list_orgs',
|
|
1700
|
+
description:
|
|
1701
|
+
'List your Cachly organizations (team/org plans). ' +
|
|
1702
|
+
'Returns each org with plan, seat count, and member info. ' +
|
|
1703
|
+
'Org plans (Team €99, Business €299, Enterprise custom) are billed separately from cache tiers.',
|
|
1704
|
+
inputSchema: {
|
|
1705
|
+
type: 'object',
|
|
1706
|
+
properties: {},
|
|
1707
|
+
required: [],
|
|
1708
|
+
},
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
name: 'create_org',
|
|
1712
|
+
description:
|
|
1713
|
+
'Create a new Cachly organization for team collaboration. ' +
|
|
1714
|
+
'After creation, invite team members with invite_member and upgrade the plan via the billing portal. ' +
|
|
1715
|
+
'Org plans: Team (€99/mo, 10 seats), Business (€299/mo, 50 seats), Enterprise (custom).',
|
|
1716
|
+
inputSchema: {
|
|
1717
|
+
type: 'object',
|
|
1718
|
+
properties: {
|
|
1719
|
+
name: { type: 'string', description: 'Organization display name (e.g. "Acme Engineering")' },
|
|
1720
|
+
slug: { type: 'string', description: 'URL-safe slug (e.g. "acme-eng"). Auto-generated from name if omitted.' },
|
|
1721
|
+
},
|
|
1722
|
+
required: ['name'],
|
|
1723
|
+
},
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
name: 'invite_member',
|
|
1727
|
+
description:
|
|
1728
|
+
'Invite a team member to a Cachly organization by email. ' +
|
|
1729
|
+
'They will receive an invite email and can join via the dashboard. ' +
|
|
1730
|
+
'Roles: owner (full access), admin (manage members + instances), member (read + cache ops).',
|
|
1731
|
+
inputSchema: {
|
|
1732
|
+
type: 'object',
|
|
1733
|
+
properties: {
|
|
1734
|
+
org_id: { type: 'string', description: 'UUID of the organization' },
|
|
1735
|
+
email: { type: 'string', description: 'Email address to invite' },
|
|
1736
|
+
role: { type: 'string', enum: ['admin', 'member'], description: 'Role for the invited member (default: member)' },
|
|
1737
|
+
},
|
|
1738
|
+
required: ['org_id', 'email'],
|
|
1739
|
+
},
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
name: 'get_org_plan',
|
|
1743
|
+
description:
|
|
1744
|
+
'Get the current org plan, seat usage, and billing info for an organization. ' +
|
|
1745
|
+
'Shows: plan name, price, seats used/max, next billing date. ' +
|
|
1746
|
+
'To upgrade: use the billing portal URL returned by this tool.',
|
|
1747
|
+
inputSchema: {
|
|
1748
|
+
type: 'object',
|
|
1749
|
+
properties: {
|
|
1750
|
+
org_id: { type: 'string', description: 'UUID of the organization' },
|
|
1751
|
+
},
|
|
1752
|
+
required: ['org_id'],
|
|
1753
|
+
},
|
|
1754
|
+
},
|
|
1656
1755
|
// ── Legacy / Setup ────────────────────────────────────────────────────────
|
|
1657
1756
|
{
|
|
1658
1757
|
name: 'setup_ai_memory',
|
|
@@ -1829,6 +1928,69 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
1829
1928
|
return `✅ Instance \`${instance_id}\` has been deleted and all data removed.`;
|
|
1830
1929
|
}
|
|
1831
1930
|
|
|
1931
|
+
// ── Org / Team management ────────────────────────────────────────────
|
|
1932
|
+
case 'list_orgs': {
|
|
1933
|
+
const res = await apiFetch<{ orgs: Array<{ id: string; name: string; slug: string; plan: string; max_members: number; member_count?: number }> }>('/api/v1/orgs');
|
|
1934
|
+
const orgs = res.orgs ?? [];
|
|
1935
|
+
if (orgs.length === 0) 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.`;
|
|
1936
|
+
return [
|
|
1937
|
+
`🏢 **Your organizations (${orgs.length})**\n`,
|
|
1938
|
+
...orgs.map(o => `• **${o.name}** (\`${o.slug}\`) — plan: ${o.plan} · seats: ${o.member_count ?? '?'}/${o.max_members}\n ID: \`${o.id}\``),
|
|
1939
|
+
`\n_Manage: \`get_org_plan\`, \`invite_member\`, dashboard → /team_`,
|
|
1940
|
+
].join('\n');
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
case 'create_org': {
|
|
1944
|
+
const { name: orgName, slug } = args as { name: string; slug?: string };
|
|
1945
|
+
const body: Record<string, string> = { name: orgName };
|
|
1946
|
+
if (slug) body.slug = slug;
|
|
1947
|
+
const res = await apiFetch<{ id: string; name: string; slug: string; plan: string }>('/api/v1/orgs', {
|
|
1948
|
+
method: 'POST',
|
|
1949
|
+
body: JSON.stringify(body),
|
|
1950
|
+
});
|
|
1951
|
+
return [
|
|
1952
|
+
`✅ **Organization created:** ${res.name}`,
|
|
1953
|
+
` ID: \`${res.id}\` · Slug: \`${res.slug}\` · Plan: ${res.plan}`,
|
|
1954
|
+
``,
|
|
1955
|
+
`**Next steps:**`,
|
|
1956
|
+
`1. Invite team members: \`invite_member(org_id="${res.id}", email="dev@example.com")\``,
|
|
1957
|
+
`2. Upgrade plan: open billing portal via dashboard → /team`,
|
|
1958
|
+
` Team: €99/mo (10 seats) · Business: €299/mo (50 seats)`,
|
|
1959
|
+
].join('\n');
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
case 'invite_member': {
|
|
1963
|
+
const { org_id, email, role = 'member' } = args as { org_id: string; email: string; role?: string };
|
|
1964
|
+
await apiFetch(`/api/v1/orgs/${org_id}/members`, {
|
|
1965
|
+
method: 'POST',
|
|
1966
|
+
body: JSON.stringify({ email, role }),
|
|
1967
|
+
});
|
|
1968
|
+
return `✅ Invite sent to **${email}** as \`${role}\` in org \`${org_id}\`.\n\nThey will receive an email to join the organization.`;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
case 'get_org_plan': {
|
|
1972
|
+
const { org_id } = args as { org_id: string };
|
|
1973
|
+
const org = await apiFetch<{
|
|
1974
|
+
id: string; name: string; plan: string; max_members: number;
|
|
1975
|
+
members: Array<{ role: string; invite_email: string; accepted_at?: string }>;
|
|
1976
|
+
stripe_customer_id?: string;
|
|
1977
|
+
}>(`/api/v1/orgs/${org_id}`);
|
|
1978
|
+
const accepted = (org.members ?? []).filter(m => m.accepted_at).length;
|
|
1979
|
+
const pending = (org.members ?? []).filter(m => !m.accepted_at).length;
|
|
1980
|
+
const planPrice: Record<string, string> = { free: '€0', team: '€99/mo', business: '€299/mo', enterprise: 'custom' };
|
|
1981
|
+
return [
|
|
1982
|
+
`🏢 **${org.name}** — Plan: **${org.plan}** (${planPrice[org.plan] ?? org.plan})`,
|
|
1983
|
+
` Seats: ${accepted} active + ${pending} pending / ${org.max_members} max`,
|
|
1984
|
+
``,
|
|
1985
|
+
`**Members:**`,
|
|
1986
|
+
...(org.members ?? []).map(m => ` • ${m.invite_email} (${m.role})${m.accepted_at ? '' : ' — pending'}`),
|
|
1987
|
+
``,
|
|
1988
|
+
org.stripe_customer_id
|
|
1989
|
+
? `💳 Billing: managed via Stripe. Upgrade/cancel: dashboard → /billing`
|
|
1990
|
+
: `💳 No payment method yet. Upgrade: dashboard → /billing → Team Plans`,
|
|
1991
|
+
].join('\n');
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1832
1994
|
// ── Live cache operations ────────────────────────────────────────────
|
|
1833
1995
|
case 'cache_get': {
|
|
1834
1996
|
const { instance_id, key } = args as { instance_id: string; key: string };
|
|
@@ -2556,6 +2718,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2556
2718
|
file_paths = [],
|
|
2557
2719
|
commands = [],
|
|
2558
2720
|
tags = [],
|
|
2721
|
+
depends_on = [],
|
|
2559
2722
|
} = args as {
|
|
2560
2723
|
instance_id: string;
|
|
2561
2724
|
topic: string;
|
|
@@ -2567,6 +2730,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2567
2730
|
file_paths?: string[];
|
|
2568
2731
|
commands?: string[];
|
|
2569
2732
|
tags?: string[];
|
|
2733
|
+
depends_on?: string[];
|
|
2570
2734
|
};
|
|
2571
2735
|
|
|
2572
2736
|
const redis = await getConnection(instance_id);
|
|
@@ -2624,6 +2788,47 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2624
2788
|
auditTrail = [{ ts, action: 'created' }];
|
|
2625
2789
|
}
|
|
2626
2790
|
|
|
2791
|
+
// ── "I Was Wrong" Protocol — failure attribution ───────────────────────
|
|
2792
|
+
const iWasWrongWarning: string[] = [];
|
|
2793
|
+
if (outcome === 'failure') {
|
|
2794
|
+
// Search for related success lessons that might have prevented this failure
|
|
2795
|
+
const scanKeys: string[] = [];
|
|
2796
|
+
const scanStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 100 });
|
|
2797
|
+
await new Promise<void>((res, rej) => {
|
|
2798
|
+
scanStream.on('data', (b: string[]) => scanKeys.push(...b));
|
|
2799
|
+
scanStream.on('end', res);
|
|
2800
|
+
scanStream.on('error', rej);
|
|
2801
|
+
});
|
|
2802
|
+
const topicWords = topic.split(/[:\-_]/).filter(w => w.length > 2);
|
|
2803
|
+
for (const k of scanKeys.slice(0, 50)) {
|
|
2804
|
+
const raw = await redis.get(k);
|
|
2805
|
+
if (!raw) continue;
|
|
2806
|
+
try {
|
|
2807
|
+
const l = JSON.parse(raw) as { outcome?: string; topic?: string; severity?: string };
|
|
2808
|
+
if (l.outcome !== 'success') continue;
|
|
2809
|
+
const lWords = (l.topic ?? '').split(/[:\-_]/).filter(w => w.length > 2);
|
|
2810
|
+
const overlap = topicWords.filter(w => lWords.includes(w)).length;
|
|
2811
|
+
if (overlap >= 1 && l.topic !== topic) {
|
|
2812
|
+
iWasWrongWarning.push(
|
|
2813
|
+
`⚠️ **"I Was Wrong"**: lesson \`${l.topic}\` (success, ${l.severity ?? 'major'}) might have prevented this failure.`,
|
|
2814
|
+
` → Use \`recall_best_solution(topic="${l.topic}")\` before next attempt.`,
|
|
2815
|
+
` → To mark it critical: \`learn_from_attempts(topic="${l.topic}", ..., severity="critical")\``,
|
|
2816
|
+
);
|
|
2817
|
+
break; // only show the most relevant match
|
|
2818
|
+
}
|
|
2819
|
+
} catch { /* skip */ }
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// ── Register dependency index for causal chain ─────────────────────────
|
|
2824
|
+
for (const dep of depends_on) {
|
|
2825
|
+
const depKey = `cachly:dep:${dep}`;
|
|
2826
|
+
const existing = await redis.get(depKey);
|
|
2827
|
+
const depTopics: string[] = existing ? JSON.parse(existing) : [];
|
|
2828
|
+
if (!depTopics.includes(topic)) depTopics.push(topic);
|
|
2829
|
+
await redis.set(depKey, JSON.stringify(depTopics));
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2627
2832
|
const lessonObj = {
|
|
2628
2833
|
topic,
|
|
2629
2834
|
outcome,
|
|
@@ -2634,6 +2839,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2634
2839
|
file_paths,
|
|
2635
2840
|
commands,
|
|
2636
2841
|
tags,
|
|
2842
|
+
depends_on,
|
|
2637
2843
|
recall_count: recallCount,
|
|
2638
2844
|
ts,
|
|
2639
2845
|
verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
|
|
@@ -2678,7 +2884,11 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2678
2884
|
isUpdate
|
|
2679
2885
|
? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
|
|
2680
2886
|
: `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
|
|
2887
|
+
depends_on.length > 0
|
|
2888
|
+
? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
|
|
2889
|
+
: '',
|
|
2681
2890
|
...templateWarnings,
|
|
2891
|
+
...iWasWrongWarning,
|
|
2682
2892
|
].filter(l => l !== '').join('\n');
|
|
2683
2893
|
}
|
|
2684
2894
|
|
|
@@ -2713,9 +2923,22 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2713
2923
|
|
|
2714
2924
|
const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
|
|
2715
2925
|
const auditSummary = (lesson.audit_trail ?? []).length > 1
|
|
2716
|
-
?
|
|
2926
|
+
? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
|
|
2927
|
+
: '';
|
|
2928
|
+
|
|
2929
|
+
// "Remember when..." — emotional header for lessons > 60 days old
|
|
2930
|
+
const ageFromStoreDays = (Date.now() - new Date(lesson.ts).getTime()) / 86400000;
|
|
2931
|
+
const rememberWhen = ageFromStoreDays > 60
|
|
2932
|
+
? `💭 _Remember when you solved this ${Math.round(ageFromStoreDays / 30)} months ago? Still works._`
|
|
2933
|
+
: '';
|
|
2934
|
+
|
|
2935
|
+
// "Never Google This Again" — suggest pinning after 3rd recall
|
|
2936
|
+
const suggestPin = updatedLesson.recall_count === 3 && !(lesson as { pinned?: boolean }).pinned
|
|
2937
|
+
? `📌 **You've looked this up 3 times.** Consider pinning it for instant access: add \`pinned: true\` via \`learn_from_attempts\` to always surface it first.`
|
|
2717
2938
|
: '';
|
|
2939
|
+
|
|
2718
2940
|
return [
|
|
2941
|
+
rememberWhen,
|
|
2719
2942
|
`${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
|
|
2720
2943
|
``,
|
|
2721
2944
|
`**What worked:** ${lesson.what_worked}`,
|
|
@@ -2725,6 +2948,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2725
2948
|
(lesson.commands ?? []).length > 0 ? `**Commands:** ${(lesson.commands ?? []).map((c: string) => `\`${c}\``).join(', ')}` : '',
|
|
2726
2949
|
(lesson.tags ?? []).length > 0 ? `**Tags:** ${(lesson.tags ?? []).map((t: string) => `#${t}`).join(' ')}` : '',
|
|
2727
2950
|
auditSummary,
|
|
2951
|
+
suggestPin,
|
|
2728
2952
|
].filter(l => l !== '').join('\n');
|
|
2729
2953
|
}
|
|
2730
2954
|
|
|
@@ -2995,7 +3219,43 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2995
3219
|
)
|
|
2996
3220
|
: [];
|
|
2997
3221
|
|
|
2998
|
-
// 6.
|
|
3222
|
+
// 6. Streak tracking
|
|
3223
|
+
let streakDays = 0;
|
|
3224
|
+
let streakRecord = 0;
|
|
3225
|
+
let streakMessage = '';
|
|
3226
|
+
try {
|
|
3227
|
+
const streakRaw = await redis.get('cachly:streak:current');
|
|
3228
|
+
const streak = streakRaw ? JSON.parse(streakRaw) as { days: number; last_date: string; record: number } : null;
|
|
3229
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
3230
|
+
if (streak) {
|
|
3231
|
+
const lastDate = streak.last_date;
|
|
3232
|
+
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
3233
|
+
if (lastDate === today) {
|
|
3234
|
+
// Already counted today
|
|
3235
|
+
streakDays = streak.days;
|
|
3236
|
+
streakRecord = streak.record;
|
|
3237
|
+
} else if (lastDate === yesterday) {
|
|
3238
|
+
// Continuing streak
|
|
3239
|
+
streakDays = streak.days + 1;
|
|
3240
|
+
streakRecord = Math.max(streakDays, streak.record);
|
|
3241
|
+
await redis.set('cachly:streak:current', JSON.stringify({ days: streakDays, last_date: today, record: streakRecord }));
|
|
3242
|
+
} else {
|
|
3243
|
+
// Streak broken
|
|
3244
|
+
streakDays = 1;
|
|
3245
|
+
streakRecord = streak.record;
|
|
3246
|
+
await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: streakRecord }));
|
|
3247
|
+
}
|
|
3248
|
+
} else {
|
|
3249
|
+
// First session ever
|
|
3250
|
+
streakDays = 1;
|
|
3251
|
+
streakRecord = 1;
|
|
3252
|
+
await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: 1 }));
|
|
3253
|
+
}
|
|
3254
|
+
if (streakDays >= 7) streakMessage = `🔥 **${streakDays}-day streak!** ${streakDays === streakRecord ? ' New record!' : `Best: ${streakRecord}d`}`;
|
|
3255
|
+
else if (streakDays > 1) streakMessage = `🔥 ${streakDays}-day streak`;
|
|
3256
|
+
} catch { /* non-critical */ }
|
|
3257
|
+
|
|
3258
|
+
// 7. Save session start marker
|
|
2999
3259
|
await redis.set('cachly:session:current', JSON.stringify({
|
|
3000
3260
|
started: new Date().toISOString(),
|
|
3001
3261
|
focus,
|
|
@@ -3003,6 +3263,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
3003
3263
|
|
|
3004
3264
|
// ── Build briefing ──────────────────────────────────────────────────────
|
|
3005
3265
|
const lines: string[] = ['🧠 **Session Briefing**', ''];
|
|
3266
|
+
if (streakMessage) lines.push(streakMessage, '');
|
|
3006
3267
|
|
|
3007
3268
|
// Handoff from previous window (if any)
|
|
3008
3269
|
const handoffRaw = await redis.get('cachly:session:handoff');
|
|
@@ -3666,6 +3927,77 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
3666
3927
|
return lines.join('\n');
|
|
3667
3928
|
}
|
|
3668
3929
|
|
|
3930
|
+
// ── recall_at — Brain Archaeology ────────────────────────────────────────
|
|
3931
|
+
case 'recall_at': {
|
|
3932
|
+
const { instance_id, topic, date } = args as { instance_id: string; topic: string; date: string };
|
|
3933
|
+
const redis = await getConnection(instance_id);
|
|
3934
|
+
const cutoff = new Date(date).getTime();
|
|
3935
|
+
if (isNaN(cutoff)) return `❌ Invalid date "${date}". Use ISO format: "2026-01-15"`;
|
|
3936
|
+
|
|
3937
|
+
const listKey = `cachly:lessons:${topic}`;
|
|
3938
|
+
const all = await redis.lrange(listKey, 0, -1);
|
|
3939
|
+
if (all.length === 0) return `📭 No history found for \`${topic}\`. Lessons are stored via \`learn_from_attempts\`.`;
|
|
3940
|
+
|
|
3941
|
+
const before = all
|
|
3942
|
+
.map(raw => { try { return JSON.parse(raw) as { ts: string; outcome?: string; what_worked?: string; what_failed?: string }; } catch { return null; } })
|
|
3943
|
+
.filter((l): l is NonNullable<typeof l> => l !== null && new Date(l.ts).getTime() <= cutoff)
|
|
3944
|
+
.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
3945
|
+
|
|
3946
|
+
if (before.length === 0) return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${new Date(JSON.parse(all[0]).ts).toLocaleDateString('de-DE')}.`;
|
|
3947
|
+
|
|
3948
|
+
const lines = [
|
|
3949
|
+
`🏺 **Brain Archaeology: \`${topic}\` before ${date}**`,
|
|
3950
|
+
`_${before.length} of ${all.length} total entries shown_`,
|
|
3951
|
+
'',
|
|
3952
|
+
];
|
|
3953
|
+
for (const l of before.slice(-10)) {
|
|
3954
|
+
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
3955
|
+
const d = new Date(l.ts).toLocaleDateString('de-DE');
|
|
3956
|
+
lines.push(`**${d}** ${emoji} ${l.outcome}`);
|
|
3957
|
+
if (l.what_worked) lines.push(` → ${l.what_worked.slice(0, 100)}`);
|
|
3958
|
+
lines.push('');
|
|
3959
|
+
}
|
|
3960
|
+
lines.push(`_Full evolution: ${all.map(r => { try { const l = JSON.parse(r) as { outcome?: string }; return l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌'; } catch { return '?'; } }).join(' → ')}_`);
|
|
3961
|
+
return lines.join('\n');
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
// ── trace_dependency — Causal Chain ──────────────────────────────────────
|
|
3965
|
+
case 'trace_dependency': {
|
|
3966
|
+
const { instance_id, dependency, mark_review = false } = args as { instance_id: string; dependency: string; mark_review?: boolean };
|
|
3967
|
+
const redis = await getConnection(instance_id);
|
|
3968
|
+
|
|
3969
|
+
const depKey = `cachly:dep:${dependency}`;
|
|
3970
|
+
const raw = await redis.get(depKey);
|
|
3971
|
+
if (!raw) return `📭 No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
|
|
3972
|
+
|
|
3973
|
+
const topics: string[] = JSON.parse(raw);
|
|
3974
|
+
const lines = [
|
|
3975
|
+
`🔗 **Causal Chain: \`${dependency}\`** — ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
|
|
3976
|
+
'',
|
|
3977
|
+
];
|
|
3978
|
+
|
|
3979
|
+
for (const t of topics) {
|
|
3980
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
|
|
3981
|
+
if (!lessonRaw) { lines.push(` • \`${t}\` _(lesson deleted)_`); continue; }
|
|
3982
|
+
const lesson = JSON.parse(lessonRaw) as { outcome?: string; severity?: string; needs_review?: boolean };
|
|
3983
|
+
const emoji = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'partial' ? '⚠️' : '❌';
|
|
3984
|
+
const reviewBadge = lesson.needs_review ? ' 🔍 **needs_review**' : '';
|
|
3985
|
+
lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
|
|
3986
|
+
|
|
3987
|
+
if (mark_review) {
|
|
3988
|
+
const updated = { ...lesson, needs_review: true };
|
|
3989
|
+
await redis.set(`cachly:lesson:best:${t}`, JSON.stringify(updated));
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
if (mark_review) {
|
|
3994
|
+
lines.push('', `🔍 All ${topics.length} lessons marked as **needs_review** — verify they still work with the changed dependency.`);
|
|
3995
|
+
} else {
|
|
3996
|
+
lines.push('', `_Run with \`mark_review: true\` to flag all dependent lessons for re-verification._`);
|
|
3997
|
+
}
|
|
3998
|
+
return lines.join('\n');
|
|
3999
|
+
}
|
|
4000
|
+
|
|
3669
4001
|
// ── global_learn ──────────────────────────────────────────────────────────
|
|
3670
4002
|
case 'global_learn': {
|
|
3671
4003
|
const { instance_id, topic, lesson, severity = 'minor', tags = [] } = args as {
|