@cachly-dev/mcp-server 0.10.23 → 0.10.25
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/CHANGELOG.md +0 -122
- package/README.md +112 -136
- package/dist/__tests__/brain-predict-failures.test.d.ts +17 -0
- package/dist/__tests__/brain-predict-failures.test.js +339 -0
- package/dist/__tests__/brain-predict-failures.test.js.map +1 -0
- package/dist/handlers/brain.js +67 -7
- package/dist/handlers/brain.js.map +1 -1
- package/dist/handlers/fedbrain.js +44 -93
- package/dist/handlers/fedbrain.js.map +1 -1
- package/dist/handlers/roadmap.js +2 -8
- package/dist/handlers/roadmap.js.map +1 -1
- package/dist/handlers/syndicate.js +7 -15
- package/dist/handlers/syndicate.js.map +1 -1
- package/dist/handlers/team.js +50 -62
- package/dist/handlers/team.js.map +1 -1
- package/dist/index.js +122 -979
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +54 -3
- package/dist/tools.js +62 -8
- package/dist/tools.js.map +1 -1
- package/package.json +13 -29
- package/scripts/postinstall.js +12 -12
- package/server.json +9 -3
- package/smithery.yaml +8 -15
- package/src/__tests__/brain-predict-failures.test.ts +430 -0
- package/src/handlers/brain.ts +64 -7
- package/src/handlers/fedbrain.ts +38 -88
- package/src/handlers/roadmap.ts +2 -7
- package/src/handlers/syndicate.ts +7 -13
- package/src/handlers/team.ts +33 -57
- package/src/index.ts +135 -1011
- package/src/tools.ts +66 -8
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/workflows/publish.yml +0 -72
- package/LAUNCH_TEXTS.md +0 -182
- package/LICENSE +0 -202
- package/dist/__tests__/auth.test.d.ts +0 -1
- package/dist/__tests__/auth.test.js +0 -107
- package/dist/__tests__/auth.test.js.map +0 -1
- package/glama.json +0 -36
- package/llms.txt +0 -155
- package/src/__tests__/auth.test.ts +0 -147
package/src/handlers/fedbrain.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { calculateConfidence, confidenceBadge, CONFIDENCE_WARN_VALUE, CONFIDENCE
|
|
|
6
6
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
7
7
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
8
8
|
import { keywordSearch, tokenize } from '../search.js';
|
|
9
|
-
import { safeJsonParse } from '../utils.js';
|
|
10
9
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
11
10
|
|
|
12
11
|
// Git concurrency semaphore (for brain_from_git parallel workers)
|
|
@@ -398,8 +397,7 @@ export async function handleFedbrainTool(
|
|
|
398
397
|
const raw = await redis.get(`cachly:lesson:best:${lesson_key}`);
|
|
399
398
|
if (!raw) return `❌ Lesson \`${lesson_key}\` not found. Store it first with \`learn_from_attempts\`.`;
|
|
400
399
|
|
|
401
|
-
const lesson =
|
|
402
|
-
if (!lesson) return `❌ Lesson \`${lesson_key}\` data is corrupted. Re-store with \`learn_from_attempts\`.`;
|
|
400
|
+
const lesson = JSON.parse(raw) as { topic: string; outcome: string; what_worked: string; what_failed?: string; tags?: string[]; commands?: string[]; severity?: string; ts?: string };
|
|
403
401
|
|
|
404
402
|
const domainTokens = [lesson.topic.split(':')[0], ...(lesson.tags ?? [])].filter(Boolean);
|
|
405
403
|
const domainFingerprint = [...new Set(domainTokens)].sort().join(',');
|
|
@@ -749,8 +747,7 @@ export async function handleFedbrainTool(
|
|
|
749
747
|
}
|
|
750
748
|
|
|
751
749
|
type Crystal = { label: string; ts: string; session_count: number; lesson_count: number; top_patterns: Array<{ category: string; insight: string; count: number }>; categories: string[]; created_from: string };
|
|
752
|
-
const crystal
|
|
753
|
-
if (!crystal) return `❌ Memory Crystal data is corrupted. Re-create with \`memory_crystalize()\`.`;
|
|
750
|
+
const crystal: Crystal = JSON.parse(raw);
|
|
754
751
|
const age = Math.floor((Date.now() - new Date(crystal.ts).getTime()) / 86400000);
|
|
755
752
|
const freshEmoji = age <= 7 ? '🟢' : age <= 30 ? '🟡' : '🔴';
|
|
756
753
|
|
|
@@ -787,55 +784,49 @@ export async function handleFedbrainTool(
|
|
|
787
784
|
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
788
785
|
if (crystalRaw) {
|
|
789
786
|
type Crystal = { label: string; ts: string; session_count: number; lesson_count: number; top_patterns: Array<{ category: string; insight: string; count: number }> };
|
|
790
|
-
const crystal
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
lines.push('');
|
|
799
|
-
}
|
|
787
|
+
const crystal: Crystal = JSON.parse(crystalRaw);
|
|
788
|
+
lines.push(`### 💎 Memory Crystal: ${crystal.label}`);
|
|
789
|
+
lines.push(`Compressed from ${crystal.session_count} sessions, ${crystal.lesson_count} lessons.`);
|
|
790
|
+
const topN = focus
|
|
791
|
+
? crystal.top_patterns.filter(p => p.category.toLowerCase().includes(focus.toLowerCase()) || p.insight.toLowerCase().includes(focus.toLowerCase())).slice(0, 4)
|
|
792
|
+
: crystal.top_patterns.slice(0, 4);
|
|
793
|
+
for (const p of topN) lines.push(` • **${p.category}**: ${p.insight.slice(0, 100)}`);
|
|
794
|
+
lines.push('');
|
|
800
795
|
}
|
|
801
796
|
|
|
802
797
|
// 2. Last session summary
|
|
803
798
|
const lastSession = await redis.get('cachly:session:last');
|
|
804
799
|
if (lastSession) {
|
|
805
800
|
type Session = { summary?: string; ts?: string; focus?: string };
|
|
806
|
-
const sess
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
lines.push('');
|
|
812
|
-
}
|
|
801
|
+
const sess: Session = JSON.parse(lastSession);
|
|
802
|
+
lines.push(`### 🕐 Last Session`);
|
|
803
|
+
if (sess.focus) lines.push(`Focus: _${sess.focus}_`);
|
|
804
|
+
if (sess.summary) lines.push(`Summary: ${sess.summary.slice(0, 300)}`);
|
|
805
|
+
lines.push('');
|
|
813
806
|
}
|
|
814
807
|
|
|
815
808
|
// 3. Session handoff
|
|
816
809
|
const handoff = await redis.get('cachly:session:handoff');
|
|
817
810
|
if (handoff) {
|
|
818
811
|
type Handoff = { remaining_tasks?: string[]; instructions?: string; context_summary?: string; blocked_on?: string };
|
|
819
|
-
const h
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
for (const t of h.remaining_tasks.slice(0, 5)) lines.push(` • ${t}`);
|
|
826
|
-
}
|
|
827
|
-
if (h.instructions) lines.push(`⚠️ Instructions: ${h.instructions.slice(0, 200)}`);
|
|
828
|
-
if (h.blocked_on) lines.push(`🚧 Blocked on: ${h.blocked_on}`);
|
|
829
|
-
lines.push('');
|
|
812
|
+
const h: Handoff = JSON.parse(handoff);
|
|
813
|
+
lines.push(`### 📋 Handoff (from last window)`);
|
|
814
|
+
if (h.context_summary) lines.push(`Context: ${h.context_summary.slice(0, 200)}`);
|
|
815
|
+
if (h.remaining_tasks?.length) {
|
|
816
|
+
lines.push(`Remaining tasks:`);
|
|
817
|
+
for (const t of h.remaining_tasks.slice(0, 5)) lines.push(` • ${t}`);
|
|
830
818
|
}
|
|
819
|
+
if (h.instructions) lines.push(`⚠️ Instructions: ${h.instructions.slice(0, 200)}`);
|
|
820
|
+
if (h.blocked_on) lines.push(`🚧 Blocked on: ${h.blocked_on}`);
|
|
821
|
+
lines.push('');
|
|
831
822
|
}
|
|
832
823
|
|
|
833
824
|
// 4. WIP registry
|
|
834
825
|
const wipRaw = await redis.get('cachly:ctx:wip-registry');
|
|
835
826
|
if (wipRaw) {
|
|
836
827
|
type Ctx = { content?: string };
|
|
837
|
-
const wip
|
|
838
|
-
if (wip
|
|
828
|
+
const wip: Ctx = JSON.parse(wipRaw);
|
|
829
|
+
if (wip.content) {
|
|
839
830
|
lines.push(`### 🔧 WIP Registry`);
|
|
840
831
|
lines.push(wip.content.slice(0, 400));
|
|
841
832
|
lines.push('');
|
|
@@ -893,8 +884,8 @@ export async function handleFedbrainTool(
|
|
|
893
884
|
|
|
894
885
|
// ── brain_from_git ────────────────────────────────────────────────────────
|
|
895
886
|
case 'brain_from_git': {
|
|
896
|
-
const { instance_id, repo_path = '.', limit = 100, branch = 'HEAD', since = ''
|
|
897
|
-
instance_id: string; repo_path?: string; limit?: number; branch?: string; since?: string;
|
|
887
|
+
const { instance_id, repo_path = '.', limit = 100, branch = 'HEAD', since = '' } = args as {
|
|
888
|
+
instance_id: string; repo_path?: string; limit?: number; branch?: string; since?: string;
|
|
898
889
|
};
|
|
899
890
|
const redis = await getConnection(instance_id);
|
|
900
891
|
const { execSync } = await import('node:child_process');
|
|
@@ -913,21 +904,9 @@ export async function handleFedbrainTool(
|
|
|
913
904
|
return `❌ Not a git repository: \`${repoDir}\`. Pass \`repo_path\` pointing to a git checkout.`;
|
|
914
905
|
}
|
|
915
906
|
|
|
916
|
-
// Incremental mode: resume from last processed commit SHA
|
|
917
|
-
const lastShaKey = `cachly:brain_from_git:last_sha:${Buffer.from(repoDir).toString('base64').slice(0, 32)}`;
|
|
918
|
-
let lastSha = '';
|
|
919
|
-
let isIncremental = false;
|
|
920
|
-
if (incremental !== false && !since) {
|
|
921
|
-
lastSha = (await redis.get(lastShaKey)) ?? '';
|
|
922
|
-
isIncremental = Boolean(lastSha);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
907
|
// Build git log command
|
|
926
|
-
// In incremental mode, only fetch commits after the last processed SHA
|
|
927
908
|
const sinceFlag = since ? `--since="${since}"` : '';
|
|
928
|
-
const
|
|
929
|
-
const revRange = afterFlag || branch;
|
|
930
|
-
const logCmd = `git log ${revRange} ${sinceFlag} --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n ${maxCommits}`;
|
|
909
|
+
const logCmd = `git log ${branch} ${sinceFlag} --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n ${maxCommits}`;
|
|
931
910
|
|
|
932
911
|
let logOutput = '';
|
|
933
912
|
try {
|
|
@@ -942,9 +921,6 @@ export async function handleFedbrainTool(
|
|
|
942
921
|
});
|
|
943
922
|
|
|
944
923
|
if (commits.length === 0) {
|
|
945
|
-
if (isIncremental) {
|
|
946
|
-
return `✅ brain_from_git: Brain is up to date — no new commits since last run (last SHA: \`${lastSha.slice(0, 8)}\`).`;
|
|
947
|
-
}
|
|
948
924
|
return `⚠️ No commits found in \`${repoDir}\` on branch \`${branch}\`${since ? ` since ${since}` : ''}.`;
|
|
949
925
|
}
|
|
950
926
|
|
|
@@ -975,21 +951,8 @@ export async function handleFedbrainTool(
|
|
|
975
951
|
let ingested = 0;
|
|
976
952
|
let skipped = 0;
|
|
977
953
|
const categoryCount = new Map<string, number>();
|
|
978
|
-
const total = commits.length;
|
|
979
|
-
const progressInterval = Math.max(1, Math.floor(total / 10)); // emit ~10 progress updates
|
|
980
|
-
let lastProgressAt = 0;
|
|
981
|
-
|
|
982
|
-
process.stderr.write(`\n🧠 brain_from_git: processing ${total} commits from ${repoDir}...\n`);
|
|
983
|
-
|
|
984
|
-
for (let i = 0; i < commits.length; i++) {
|
|
985
|
-
const commit = commits[i]!;
|
|
986
|
-
|
|
987
|
-
// Emit progress to stderr so editors can display it in MCP logs
|
|
988
|
-
if (i - lastProgressAt >= progressInterval) {
|
|
989
|
-
process.stderr.write(` ⏳ Processing ${i}/${total} commits (${ingested} lessons so far)...\n`);
|
|
990
|
-
lastProgressAt = i;
|
|
991
|
-
}
|
|
992
954
|
|
|
955
|
+
for (const commit of commits) {
|
|
993
956
|
if (!commit.subject) { skipped++; continue; }
|
|
994
957
|
const { category, outcome, severity } = classifyCommit(commit.subject);
|
|
995
958
|
const domain = extractDomain(commit.subject);
|
|
@@ -1024,20 +987,11 @@ export async function handleFedbrainTool(
|
|
|
1024
987
|
ingested++;
|
|
1025
988
|
}
|
|
1026
989
|
|
|
1027
|
-
process.stderr.write(` ✅ brain_from_git complete: ${ingested}/${total} commits ingested\n\n`);
|
|
1028
|
-
|
|
1029
|
-
// Save the latest commit SHA for incremental runs
|
|
1030
|
-
if (commits.length > 0 && commits[0]?.sha) {
|
|
1031
|
-
await redis.set(lastShaKey, commits[0].sha, 'EX', 90 * 24 * 3600); // expire after 90 days
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
990
|
const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
|
|
1035
991
|
|
|
1036
992
|
return [
|
|
1037
|
-
`🔁 **brain_from_git: ${repoDir}**`,
|
|
1038
|
-
|
|
1039
|
-
``,
|
|
1040
|
-
`📂 Branch: \`${revRange}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
|
|
993
|
+
`🔁 **brain_from_git: ${repoDir}**`, '',
|
|
994
|
+
`📂 Branch: \`${branch}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
|
|
1041
995
|
``,
|
|
1042
996
|
`**Breakdown by category:**`,
|
|
1043
997
|
categoryBreakdown,
|
|
@@ -1045,8 +999,7 @@ export async function handleFedbrainTool(
|
|
|
1045
999
|
`💡 New lessons are stored with confidence 0.55 (auto-inferred).`,
|
|
1046
1000
|
`💡 As you confirm them via \`learn_from_attempts\`, confidence rises automatically.`,
|
|
1047
1001
|
`🔍 Explore: \`brain_search(query="fix")\` | \`ckg_inspect(concept="deploy")\``,
|
|
1048
|
-
|
|
1049
|
-
].filter(Boolean).join('\n');
|
|
1002
|
+
].join('\n');
|
|
1050
1003
|
} finally {
|
|
1051
1004
|
_gitSemRelease();
|
|
1052
1005
|
}
|
|
@@ -1073,14 +1026,12 @@ export async function handleFedbrainTool(
|
|
|
1073
1026
|
for (const nk of nodeKeys.slice(0, 5)) {
|
|
1074
1027
|
const nodeRaw = await redis.get(nk);
|
|
1075
1028
|
if (!nodeRaw) continue;
|
|
1076
|
-
const node
|
|
1077
|
-
if (!node) continue;
|
|
1029
|
+
const node: CKGNode = JSON.parse(nodeRaw);
|
|
1078
1030
|
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
1079
1031
|
for (const ek of edgeKeys.slice(0, 20)) {
|
|
1080
1032
|
const edgeRaw = await redis.get(ek);
|
|
1081
1033
|
if (!edgeRaw) continue;
|
|
1082
|
-
const edge
|
|
1083
|
-
if (!edge) continue;
|
|
1034
|
+
const edge: CKGEdge = JSON.parse(edgeRaw);
|
|
1084
1035
|
if (edge.edgeType !== 'causes' && edge.edgeType !== 'degrades_under') continue;
|
|
1085
1036
|
|
|
1086
1037
|
// Look up fix for this failure from CKG 'fixes' edges
|
|
@@ -1089,13 +1040,12 @@ export async function handleFedbrainTool(
|
|
|
1089
1040
|
for (const fek of fixEdgeKeys.slice(0, 10)) {
|
|
1090
1041
|
const feRaw = await redis.get(fek);
|
|
1091
1042
|
if (!feRaw) continue;
|
|
1092
|
-
const fe
|
|
1093
|
-
if (!fe) continue;
|
|
1043
|
+
const fe: CKGEdge = JSON.parse(feRaw);
|
|
1094
1044
|
if (fe.edgeType === 'fixes') {
|
|
1095
1045
|
const lessonRaw = await redis.get(`cachly:lesson:best:${fe.from}`);
|
|
1096
1046
|
if (lessonRaw) {
|
|
1097
|
-
const lesson =
|
|
1098
|
-
fix = lesson
|
|
1047
|
+
const lesson = JSON.parse(lessonRaw) as { what_worked?: string };
|
|
1048
|
+
fix = lesson.what_worked?.slice(0, 120);
|
|
1099
1049
|
break;
|
|
1100
1050
|
}
|
|
1101
1051
|
}
|
package/src/handlers/roadmap.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Redis } from 'ioredis';
|
|
2
|
-
import { safeJsonParse } from '../utils.js';
|
|
3
2
|
|
|
4
3
|
type GetConnection = (instanceId: string) => Promise<Redis>;
|
|
5
4
|
type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
@@ -68,8 +67,7 @@ export async function handleRoadmapTool(
|
|
|
68
67
|
const redis = await getConnection(rid);
|
|
69
68
|
const raw = await redis.hget(`cachly:roadmap:${rid}`, itemId);
|
|
70
69
|
if (!raw) return `⚠️ **roadmap_update** – Item \`${itemId}\` not found. Use \`roadmap_list\` to see all items.`;
|
|
71
|
-
const item =
|
|
72
|
-
if (!item) return `⚠️ **roadmap_update** – Item \`${itemId}\` data is corrupted. Re-add with \`roadmap_add\`.`;
|
|
70
|
+
const item = JSON.parse(raw) as Record<string, unknown>;
|
|
73
71
|
const oldStatus = item.status as string;
|
|
74
72
|
if (newStatus) item.status = newStatus;
|
|
75
73
|
if (newPriority) item.priority = newPriority;
|
|
@@ -108,10 +106,7 @@ export async function handleRoadmapTool(
|
|
|
108
106
|
const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
109
107
|
const STATUS_ICON: Record<string, string> = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
|
|
110
108
|
const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
|
|
111
|
-
let items = Object.values(all).
|
|
112
|
-
const item = safeJsonParse<Record<string, string | string[]> | null>(v as string, null);
|
|
113
|
-
return item ? [item] : [];
|
|
114
|
-
});
|
|
109
|
+
let items = Object.values(all).map(v => JSON.parse(v as string) as Record<string, string | string[]>);
|
|
115
110
|
// Filter
|
|
116
111
|
if (filterStatus === 'open') items = items.filter(i => openStatuses.has(i.status as string));
|
|
117
112
|
else if (filterStatus) items = items.filter(i => i.status === filterStatus);
|
|
@@ -4,7 +4,6 @@ import { calculateConfidence } from '../confidence.js';
|
|
|
4
4
|
import { ckgSlug, ckgUpdateEdge } from '../ckg.js';
|
|
5
5
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
6
6
|
import { keywordSearch, tokenize } from '../search.js';
|
|
7
|
-
import { safeJsonParse } from '../utils.js';
|
|
8
7
|
|
|
9
8
|
type GetConnection = (instanceId: string) => Promise<Redis>;
|
|
10
9
|
type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
@@ -293,8 +292,7 @@ export async function handleSyndicateTool(
|
|
|
293
292
|
for (const ek of [...fromKeys, ...toKeys].slice(0, 50)) {
|
|
294
293
|
const raw = await redis.get(ek);
|
|
295
294
|
if (!raw) continue;
|
|
296
|
-
const edge
|
|
297
|
-
if (!edge) continue;
|
|
295
|
+
const edge: CKGEdge = JSON.parse(raw);
|
|
298
296
|
allEdges.push(edge);
|
|
299
297
|
if (hop < max_hops) {
|
|
300
298
|
if (!visited.has(edge.from)) queue.push({ id: edge.from, hop: hop + 1 });
|
|
@@ -368,18 +366,16 @@ export async function handleSyndicateTool(
|
|
|
368
366
|
for (const nk of nodeKeys.slice(0, 5)) {
|
|
369
367
|
const nodeRaw = await redis.get(nk);
|
|
370
368
|
if (!nodeRaw) continue;
|
|
371
|
-
const node
|
|
372
|
-
if (!node) continue;
|
|
369
|
+
const node: CKGNode = JSON.parse(nodeRaw);
|
|
373
370
|
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
374
371
|
for (const ek of edgeKeys.slice(0, 20)) {
|
|
375
372
|
const edgeRaw = await redis.get(ek);
|
|
376
373
|
if (!edgeRaw) continue;
|
|
377
|
-
const edge
|
|
378
|
-
if (!edge) continue;
|
|
374
|
+
const edge: CKGEdge = JSON.parse(edgeRaw);
|
|
379
375
|
// Only interested in fixes and co-occurs for prediction
|
|
380
376
|
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes') continue;
|
|
381
377
|
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
382
|
-
const lesson = lessonRaw ?
|
|
378
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
383
379
|
predictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
|
|
384
380
|
}
|
|
385
381
|
}
|
|
@@ -393,17 +389,15 @@ export async function handleSyndicateTool(
|
|
|
393
389
|
for (const k of lessonKeys) {
|
|
394
390
|
const raw = await redis.get(k);
|
|
395
391
|
if (!raw) continue;
|
|
396
|
-
{
|
|
397
|
-
|
|
398
|
-
const l = safeJsonParse<PredLesson | null>(raw, null);
|
|
399
|
-
if (!l) continue;
|
|
392
|
+
try {
|
|
393
|
+
const l = JSON.parse(raw) as { topic: string; what_worked?: string; what_failed?: string; outcome: string; severity?: string; ts: string; verified_at?: string; recall_count?: number };
|
|
400
394
|
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? ''].join(' ').toLowerCase();
|
|
401
395
|
const score = ctxTokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
|
|
402
396
|
if (score >= 1 && l.outcome !== 'failure') {
|
|
403
397
|
const conf = calculateConfidence(l);
|
|
404
398
|
textPredictions.push({ ...l, confidence: conf });
|
|
405
399
|
}
|
|
406
|
-
}
|
|
400
|
+
} catch { /* skip */ }
|
|
407
401
|
}
|
|
408
402
|
textPredictions.sort((a, b) => b.confidence - a.confidence);
|
|
409
403
|
|
package/src/handlers/team.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { Redis } from 'ioredis';
|
|
|
5
5
|
import { calculateConfidence, confidenceBadge, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_VALUE,
|
|
6
6
|
CONFIDENCE_WARN_DAYS, CONFIDENCE_STALE_DAYS, simpleHash } from '../confidence.js';
|
|
7
7
|
import type { Instance } from './brain.js';
|
|
8
|
-
import { safeJsonParse } from '../utils.js';
|
|
9
8
|
|
|
10
9
|
type GetConnection = (instanceId: string) => Promise<Redis>;
|
|
11
10
|
type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
@@ -54,11 +53,10 @@ export async function handleTeamTool(
|
|
|
54
53
|
|
|
55
54
|
type Lesson = { topic: string; what_worked: string; outcome: string; file_paths?: string[] };
|
|
56
55
|
const relevant: string[] = [];
|
|
57
|
-
const
|
|
58
|
-
|
|
56
|
+
for (const k of lessonKeys) {
|
|
57
|
+
const raw = await redis.get(k);
|
|
59
58
|
if (!raw) continue;
|
|
60
|
-
const lesson =
|
|
61
|
-
if (!lesson) continue;
|
|
59
|
+
const lesson = JSON.parse(raw) as Lesson;
|
|
62
60
|
// Match by file_paths stored in lesson OR by topic keywords matching file name
|
|
63
61
|
const topicWords = lesson.topic.toLowerCase().split(/[:\-_]/);
|
|
64
62
|
const fileMatches = changed_files.some(f => {
|
|
@@ -148,12 +146,10 @@ export async function handleTeamTool(
|
|
|
148
146
|
author?: string; tags?: string[];
|
|
149
147
|
};
|
|
150
148
|
let lessons: TeamLesson[] = [];
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (l) lessons.push(l);
|
|
156
|
-
}
|
|
149
|
+
for (const k of lessonKeys) {
|
|
150
|
+
const raw = await redis.get(k);
|
|
151
|
+
if (!raw) continue;
|
|
152
|
+
try { lessons.push(JSON.parse(raw) as TeamLesson); } catch { /* skip */ }
|
|
157
153
|
}
|
|
158
154
|
|
|
159
155
|
// Filter
|
|
@@ -282,12 +278,10 @@ export async function handleTeamTool(
|
|
|
282
278
|
|
|
283
279
|
type RawLesson = { topic: string; outcome: string; what_worked: string; severity?: string; ts: string; auto_learned?: boolean };
|
|
284
280
|
const allLessons: RawLesson[] = [];
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (l) allLessons.push(l);
|
|
290
|
-
}
|
|
281
|
+
for (const k of allLessonKeys) {
|
|
282
|
+
const raw = await redis.get(k);
|
|
283
|
+
if (!raw) continue;
|
|
284
|
+
try { allLessons.push(JSON.parse(raw) as RawLesson); } catch { /* skip */ }
|
|
291
285
|
}
|
|
292
286
|
|
|
293
287
|
// Group lessons by top-level category
|
|
@@ -373,17 +367,18 @@ export async function handleTeamTool(
|
|
|
373
367
|
verified_at?: string; severity?: string; audit_trail?: unknown[];
|
|
374
368
|
};
|
|
375
369
|
const lessons: DrLesson[] = [];
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (l) lessons.push(l);
|
|
381
|
-
}
|
|
370
|
+
for (const k of lessonKeys) {
|
|
371
|
+
const raw = await redis.get(k);
|
|
372
|
+
if (!raw) continue;
|
|
373
|
+
try { lessons.push(JSON.parse(raw) as DrLesson); } catch { /* skip */ }
|
|
382
374
|
}
|
|
383
375
|
|
|
384
376
|
// Last session
|
|
385
377
|
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
386
|
-
|
|
378
|
+
let lastSession: { ts: string; summary: string } | null = null;
|
|
379
|
+
if (lastSessionRaw) {
|
|
380
|
+
try { lastSession = JSON.parse(lastSessionRaw); } catch { /* ignore */ }
|
|
381
|
+
}
|
|
387
382
|
|
|
388
383
|
// Open failures
|
|
389
384
|
const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
|
|
@@ -514,24 +509,13 @@ export async function handleTeamTool(
|
|
|
514
509
|
issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
|
|
515
510
|
}
|
|
516
511
|
|
|
517
|
-
const
|
|
518
|
-
const lines = [
|
|
519
|
-
`🩺 **Brain Doctor**`,
|
|
520
|
-
`\`${divider}\``,
|
|
521
|
-
'',
|
|
522
|
-
'**📊 Health Checks**',
|
|
523
|
-
...checks.map(c => ' ' + c),
|
|
524
|
-
'',
|
|
525
|
-
];
|
|
512
|
+
const lines = ['🩺 **Brain Doctor Report**', '', ...checks.map(c => ' ' + c), ''];
|
|
526
513
|
if (issues.length > 0) {
|
|
527
|
-
lines.push(
|
|
528
|
-
|
|
514
|
+
lines.push('**Issues to fix:**');
|
|
515
|
+
for (const i of issues) lines.push(' ' + i);
|
|
529
516
|
lines.push('');
|
|
530
|
-
lines.push(`_Fix the action items above, then re-run \`brain_doctor\` to verify._`);
|
|
531
517
|
} else {
|
|
532
|
-
lines.push('
|
|
533
|
-
lines.push('');
|
|
534
|
-
lines.push('_Keep the brain sharp: `session_start` → work → `learn_from_attempts` → `session_end`_');
|
|
518
|
+
lines.push(' 🎉 Brain looks healthy! Keep calling session_start/session_end.');
|
|
535
519
|
}
|
|
536
520
|
return lines.join('\n');
|
|
537
521
|
}
|
|
@@ -552,11 +536,7 @@ export async function handleTeamTool(
|
|
|
552
536
|
.filter((l): l is NonNullable<typeof l> => l !== null && new Date(l.ts).getTime() <= cutoff)
|
|
553
537
|
.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
554
538
|
|
|
555
|
-
if (before.length === 0) {
|
|
556
|
-
const first = safeJsonParse(all[0] ?? null, null as null | { ts?: string });
|
|
557
|
-
const earliest = first?.ts ? new Date(first.ts).toLocaleDateString('de-DE') : 'unknown';
|
|
558
|
-
return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${earliest}.`;
|
|
559
|
-
}
|
|
539
|
+
if (before.length === 0) return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${new Date(JSON.parse(all[0]).ts).toLocaleDateString('de-DE')}.`;
|
|
560
540
|
|
|
561
541
|
const lines = [
|
|
562
542
|
`🏺 **Brain Archaeology: \`${topic}\` before ${date}**`,
|
|
@@ -583,7 +563,7 @@ export async function handleTeamTool(
|
|
|
583
563
|
const raw = await redis.get(depKey);
|
|
584
564
|
if (!raw) return `📭 No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
|
|
585
565
|
|
|
586
|
-
const topics: string[] =
|
|
566
|
+
const topics: string[] = JSON.parse(raw);
|
|
587
567
|
const lines = [
|
|
588
568
|
`🔗 **Causal Chain: \`${dependency}\`** — ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
|
|
589
569
|
'',
|
|
@@ -592,8 +572,7 @@ export async function handleTeamTool(
|
|
|
592
572
|
for (const t of topics) {
|
|
593
573
|
const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
|
|
594
574
|
if (!lessonRaw) { lines.push(` • \`${t}\` _(lesson deleted)_`); continue; }
|
|
595
|
-
const lesson =
|
|
596
|
-
if (!lesson) { lines.push(` • \`${t}\` _(lesson corrupted)_`); continue; }
|
|
575
|
+
const lesson = JSON.parse(lessonRaw) as { outcome?: string; severity?: string; needs_review?: boolean };
|
|
597
576
|
const emoji = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'partial' ? '⚠️' : '❌';
|
|
598
577
|
const reviewBadge = lesson.needs_review ? ' 🔍 **needs_review**' : '';
|
|
599
578
|
lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
|
|
@@ -635,8 +614,8 @@ export async function handleTeamTool(
|
|
|
635
614
|
// Preserve recall_count on update
|
|
636
615
|
const existing = await redis.get(key);
|
|
637
616
|
if (existing) {
|
|
638
|
-
const prev =
|
|
639
|
-
record.recall_count = prev
|
|
617
|
+
const prev = JSON.parse(existing) as { recall_count?: number };
|
|
618
|
+
record.recall_count = prev.recall_count ?? 0;
|
|
640
619
|
}
|
|
641
620
|
await redis.set(key, JSON.stringify(record));
|
|
642
621
|
return `🌐 **Global lesson stored**: \`${topic}\`\n\n${lesson}\n\nRecallable from any project via \`global_recall(topic="${topic}")\`.`;
|
|
@@ -656,12 +635,10 @@ export async function handleTeamTool(
|
|
|
656
635
|
|
|
657
636
|
type GlobalLesson = { topic: string; lesson: string; severity?: string; ts: string; recall_count?: number };
|
|
658
637
|
let lessons: GlobalLesson[] = [];
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (l) lessons.push(l);
|
|
664
|
-
}
|
|
638
|
+
for (const k of keys) {
|
|
639
|
+
const raw = await redis.get(k);
|
|
640
|
+
if (!raw) continue;
|
|
641
|
+
try { lessons.push(JSON.parse(raw) as GlobalLesson); } catch { /* skip */ }
|
|
665
642
|
}
|
|
666
643
|
|
|
667
644
|
if (topic) {
|
|
@@ -678,8 +655,7 @@ export async function handleTeamTool(
|
|
|
678
655
|
const k = `cachly:global:lesson:${l.topic}`;
|
|
679
656
|
const raw = await redis.get(k);
|
|
680
657
|
if (raw) {
|
|
681
|
-
const rec =
|
|
682
|
-
if (!rec) continue;
|
|
658
|
+
const rec = JSON.parse(raw) as { recall_count?: number };
|
|
683
659
|
rec.recall_count = (rec.recall_count ?? 0) + 1;
|
|
684
660
|
await redis.set(k, JSON.stringify(rec));
|
|
685
661
|
}
|