@cachly-dev/mcp-server 0.10.42 → 0.10.44
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 +39 -0
- package/dist/__tests__/brain-flow.test.js +141 -0
- package/dist/__tests__/brain-flow.test.js.map +1 -1
- package/dist/handlers/brain.js +38 -0
- package/dist/handlers/brain.js.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/brain-flow.test.ts +174 -0
- package/src/handlers/brain.ts +41 -0
- package/src/index.ts +5 -1
|
@@ -17,6 +17,7 @@ import { EventEmitter } from 'node:events';
|
|
|
17
17
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
18
18
|
import { EMBED_PROVIDER, hasEmbedProvider, embedProviderHint, embedConfig, setEmbedJwt } from '../embeddings.js';
|
|
19
19
|
import { keywordSearch } from '../search.js';
|
|
20
|
+
import { handleBrainTool } from '../handlers/brain.js';
|
|
20
21
|
import type { Redis } from 'ioredis';
|
|
21
22
|
|
|
22
23
|
// ── In-memory Redis mock ──────────────────────────────────────────────────────
|
|
@@ -67,6 +68,21 @@ class MockRedis {
|
|
|
67
68
|
return [...(this.sets.get(key) ?? [])];
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
async mget(...keys: string[]): Promise<(string | null)[]> {
|
|
72
|
+
return keys.map(k => this.store.get(k) ?? null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async exists(key: string): Promise<number> {
|
|
76
|
+
return this.store.has(key) ? 1 : 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async incrbyfloat(key: string, increment: number): Promise<string> {
|
|
80
|
+
const cur = parseFloat(this.store.get(key) ?? '0');
|
|
81
|
+
const next = cur + increment;
|
|
82
|
+
this.store.set(key, String(next));
|
|
83
|
+
return String(next);
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
/** Simplified scanStream: returns all matching keys in a single 'data' event. */
|
|
71
87
|
scanStream(opts: { match: string; count?: number }): EventEmitter {
|
|
72
88
|
const emitter = new EventEmitter();
|
|
@@ -872,3 +888,161 @@ describe('Brain Flow: advanced scenarios', () => {
|
|
|
872
888
|
});
|
|
873
889
|
});
|
|
874
890
|
});
|
|
891
|
+
|
|
892
|
+
// ── New feature coverage (0.10.41 / 0.10.42) ─────────────────────────────────
|
|
893
|
+
// Trust badges, Proven Laws crystallization, and focus-less predictive warning.
|
|
894
|
+
// These exercise the REAL handleBrainTool code path with the MockRedis.
|
|
895
|
+
|
|
896
|
+
const noopApiFetch = (async () => null) as unknown as Parameters<typeof handleBrainTool>[3];
|
|
897
|
+
|
|
898
|
+
/** Seed a best-solution lesson with an explicit recall_count / authors / pinned. */
|
|
899
|
+
async function seedLesson(
|
|
900
|
+
redis: MockRedis,
|
|
901
|
+
topic: string,
|
|
902
|
+
opts: {
|
|
903
|
+
outcome?: 'success' | 'failure' | 'partial';
|
|
904
|
+
what_worked?: string;
|
|
905
|
+
what_failed?: string;
|
|
906
|
+
recall_count?: number;
|
|
907
|
+
severity?: 'critical' | 'major' | 'minor';
|
|
908
|
+
pinned?: boolean;
|
|
909
|
+
author?: string;
|
|
910
|
+
tags?: string[];
|
|
911
|
+
} = {},
|
|
912
|
+
): Promise<void> {
|
|
913
|
+
const ts = new Date().toISOString();
|
|
914
|
+
const obj = {
|
|
915
|
+
topic,
|
|
916
|
+
outcome: opts.outcome ?? 'success',
|
|
917
|
+
what_worked: opts.what_worked ?? `solution for ${topic}`,
|
|
918
|
+
what_failed: opts.what_failed,
|
|
919
|
+
severity: opts.severity ?? 'minor',
|
|
920
|
+
recall_count: opts.recall_count ?? 0,
|
|
921
|
+
ts,
|
|
922
|
+
verified_at: ts,
|
|
923
|
+
confidence: 1.0,
|
|
924
|
+
tags: opts.tags ?? [],
|
|
925
|
+
...(opts.pinned ? { pinned: true } : {}),
|
|
926
|
+
...(opts.author ? { author: opts.author } : {}),
|
|
927
|
+
};
|
|
928
|
+
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(obj));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
describe('Trust badges (recall_best_solution)', () => {
|
|
932
|
+
let redis: MockRedis;
|
|
933
|
+
const getConn = async () => redis as unknown as Redis;
|
|
934
|
+
beforeEach(() => { redis = new MockRedis(); });
|
|
935
|
+
|
|
936
|
+
it('shows "Battle-tested" when recall_count crosses 10', async () => {
|
|
937
|
+
await seedLesson(redis, 'redis:pool', { recall_count: 9, what_worked: 'set max_retries=3' });
|
|
938
|
+
const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'redis:pool' }, getConn, noopApiFetch))!;
|
|
939
|
+
expect(out).toContain('Battle-tested');
|
|
940
|
+
expect(out).toContain('recalled 10×');
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('shows "Proven" when recall_count is between 5 and 9', async () => {
|
|
944
|
+
await seedLesson(redis, 'api:auth', { recall_count: 4 });
|
|
945
|
+
const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'api:auth' }, getConn, noopApiFetch))!;
|
|
946
|
+
expect(out).toContain('Proven');
|
|
947
|
+
expect(out).toContain('recalled 5×');
|
|
948
|
+
expect(out).not.toContain('Battle-tested');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('shows no trust badge for a fresh low-recall lesson', async () => {
|
|
952
|
+
await seedLesson(redis, 'misc:thing', { recall_count: 0 });
|
|
953
|
+
const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'misc:thing' }, getConn, noopApiFetch))!;
|
|
954
|
+
expect(out).not.toContain('Battle-tested');
|
|
955
|
+
expect(out).not.toContain('Proven —');
|
|
956
|
+
expect(out).not.toContain('Team-verified');
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('increments recall_count and persists it', async () => {
|
|
960
|
+
await seedLesson(redis, 'deploy:x', { recall_count: 2 });
|
|
961
|
+
await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'deploy:x' }, getConn, noopApiFetch);
|
|
962
|
+
const stored = JSON.parse((await redis.get('cachly:lesson:best:deploy:x'))!);
|
|
963
|
+
expect(stored.recall_count).toBe(3);
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
describe('Proven Laws crystallization (session_start)', () => {
|
|
968
|
+
let redis: MockRedis;
|
|
969
|
+
const getConn = async () => redis as unknown as Redis;
|
|
970
|
+
beforeEach(async () => {
|
|
971
|
+
redis = new MockRedis();
|
|
972
|
+
// Provide a last session so session_start does not attempt git reconstruction.
|
|
973
|
+
await redis.set('cachly:session:last', JSON.stringify({ summary: 'prior work', ts: new Date().toISOString(), files_changed: [] }));
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it('surfaces lessons recalled >= 5 in the Proven Laws section', async () => {
|
|
977
|
+
await seedLesson(redis, 'infra:k3s', { recall_count: 7, what_worked: 'add WireGuard IP to TLS-SAN' });
|
|
978
|
+
await seedLesson(redis, 'random:note', { recall_count: 1 });
|
|
979
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
980
|
+
expect(out).toContain('Proven Laws');
|
|
981
|
+
expect(out).toContain('infra:k3s');
|
|
982
|
+
expect(out).toContain('recalled 7×');
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it('surfaces an explicitly pinned lesson even with low recall', async () => {
|
|
986
|
+
await seedLesson(redis, 'team:convention', { recall_count: 0, pinned: true, what_worked: 'always snake_case handlers' });
|
|
987
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
988
|
+
expect(out).toContain('Proven Laws');
|
|
989
|
+
expect(out).toContain('team:convention');
|
|
990
|
+
expect(out).toContain('pinned');
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('omits the Proven Laws section when nothing qualifies', async () => {
|
|
994
|
+
await seedLesson(redis, 'small:thing', { recall_count: 1 });
|
|
995
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
996
|
+
expect(out).not.toContain('Proven Laws');
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('does not crystallize failure lessons', async () => {
|
|
1000
|
+
await seedLesson(redis, 'broken:flow', { recall_count: 9, outcome: 'failure', what_failed: 'never works' });
|
|
1001
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1002
|
+
expect(out).not.toContain('Proven Laws');
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
describe('Predictive pre-warning without explicit focus (session_start)', () => {
|
|
1007
|
+
let redis: MockRedis;
|
|
1008
|
+
const getConn = async () => redis as unknown as Redis;
|
|
1009
|
+
beforeEach(() => { redis = new MockRedis(); });
|
|
1010
|
+
|
|
1011
|
+
it('derives danger area from last session files and warns', async () => {
|
|
1012
|
+
// Last session touched payment files → likely to continue there.
|
|
1013
|
+
await redis.set('cachly:session:last', JSON.stringify({
|
|
1014
|
+
summary: 'worked on payment integration',
|
|
1015
|
+
ts: new Date().toISOString(),
|
|
1016
|
+
files_changed: ['api/payment/stripe_webhook.go'],
|
|
1017
|
+
}));
|
|
1018
|
+
// A known failure in the payment area.
|
|
1019
|
+
await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'missed idempotency key → double charge', tags: ['payment'] });
|
|
1020
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1021
|
+
expect(out).toContain('PRE-WARNING');
|
|
1022
|
+
expect(out).toContain('payment:webhook');
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
it('does not warn when last-session area has no known failures', async () => {
|
|
1026
|
+
await redis.set('cachly:session:last', JSON.stringify({
|
|
1027
|
+
summary: 'wrote docs',
|
|
1028
|
+
ts: new Date().toISOString(),
|
|
1029
|
+
files_changed: ['docs/readme.md'],
|
|
1030
|
+
}));
|
|
1031
|
+
await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'x', tags: ['payment'] });
|
|
1032
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1033
|
+
expect(out).not.toContain('PRE-WARNING');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('filters path noise so common segments do not trigger false matches', async () => {
|
|
1037
|
+
// Last session only touched generic noise paths (src/lib/index).
|
|
1038
|
+
await redis.set('cachly:session:last', JSON.stringify({
|
|
1039
|
+
summary: 'refactor',
|
|
1040
|
+
ts: new Date().toISOString(),
|
|
1041
|
+
files_changed: ['src/lib/index.ts'],
|
|
1042
|
+
}));
|
|
1043
|
+
// A failure tagged 'src' — should NOT match because 'src' is noise.
|
|
1044
|
+
await seedLesson(redis, 'src:thing', { outcome: 'failure', what_failed: 'noise', tags: ['src'] });
|
|
1045
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1046
|
+
expect(out).not.toContain('PRE-WARNING');
|
|
1047
|
+
});
|
|
1048
|
+
});
|
package/src/handlers/brain.ts
CHANGED
|
@@ -502,6 +502,22 @@ export async function handleBrainTool(
|
|
|
502
502
|
10,
|
|
503
503
|
);
|
|
504
504
|
|
|
505
|
+
// Increment recall_count on matched lessons (fire-and-forget — same as recall_best_solution).
|
|
506
|
+
// This ensures the dashboard metric, Proven Laws, and trust badges reflect real smart_recall usage.
|
|
507
|
+
const lessonMatches = kwMatches.filter(m => m.key.startsWith('cachly:lesson:best:'));
|
|
508
|
+
for (const m of lessonMatches.slice(0, 5)) {
|
|
509
|
+
const existing = await redis.get(m.key).catch(() => null);
|
|
510
|
+
if (existing) {
|
|
511
|
+
const lesson = safeJsonParse(existing, null as null | { recall_count?: number; [k: string]: unknown });
|
|
512
|
+
if (lesson) {
|
|
513
|
+
const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: new Date().toISOString() };
|
|
514
|
+
redis.set(m.key, JSON.stringify(updated)).catch(() => {});
|
|
515
|
+
const savedMins = (lesson.severity as string) === 'critical' ? 240 : (lesson.severity as string) === 'major' ? 60 : 30;
|
|
516
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => {});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
505
521
|
const lines: string[] = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
506
522
|
|
|
507
523
|
// Show sub-query info if multi-topic was detected
|
|
@@ -916,6 +932,31 @@ export async function handleBrainTool(
|
|
|
916
932
|
// Brain health
|
|
917
933
|
lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
|
|
918
934
|
|
|
935
|
+
// ── 📌 Proven Laws — auto-crystallized lessons (quick win 3) ─────────────
|
|
936
|
+
// A lesson that has proven itself (recalled ≥ CRYSTALLIZE_RECALLS times) or
|
|
937
|
+
// was explicitly pinned becomes a "law" — surfaced at the top of every
|
|
938
|
+
// briefing so the most battle-tested knowledge is never buried. This is
|
|
939
|
+
// the today-safe version of swarm crystallization: enough confirmations
|
|
940
|
+
// auto-promote a lesson without any manual step.
|
|
941
|
+
{
|
|
942
|
+
type LessonPin = typeof lessons[0] & { pinned?: boolean };
|
|
943
|
+
const CRYSTALLIZE_RECALLS = 5;
|
|
944
|
+
const laws = (lessons as LessonPin[])
|
|
945
|
+
.filter(l => l.outcome === 'success' && (l.pinned === true || (l.recall_count ?? 0) >= CRYSTALLIZE_RECALLS))
|
|
946
|
+
.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0))
|
|
947
|
+
.slice(0, 5);
|
|
948
|
+
if (laws.length > 0) {
|
|
949
|
+
lines.push(`📌 **Proven Laws** (auto-crystallized — your most-trusted knowledge):`);
|
|
950
|
+
for (const l of laws) {
|
|
951
|
+
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
952
|
+
const rc = l.recall_count ?? 0;
|
|
953
|
+
const why = l.pinned === true ? 'pinned' : `recalled ${rc}×`;
|
|
954
|
+
lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${l.what_worked.slice(0, 100)}`);
|
|
955
|
+
}
|
|
956
|
+
lines.push('');
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
919
960
|
// ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
|
|
920
961
|
if (lessons.length >= 3) {
|
|
921
962
|
const domainMap = new Map<string, { total: number; success: number; critical: number }>();
|
package/src/index.ts
CHANGED
|
@@ -60,7 +60,7 @@ import { Redis } from 'ioredis';
|
|
|
60
60
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
61
61
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
62
62
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
63
|
-
const CURRENT_VERSION = '0.10.
|
|
63
|
+
const CURRENT_VERSION = '0.10.44';
|
|
64
64
|
|
|
65
65
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
66
66
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
@@ -2156,6 +2156,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2156
2156
|
const CLIENT_ID = 'cachly-cli';
|
|
2157
2157
|
|
|
2158
2158
|
console.log('Step 1: Sign in to cachly (free, no credit card)\n');
|
|
2159
|
+
sendFunnelEvent('setup_auth_started');
|
|
2159
2160
|
|
|
2160
2161
|
// Start device flow
|
|
2161
2162
|
let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
|
|
@@ -2179,6 +2180,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2179
2180
|
console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
|
|
2180
2181
|
token = await ask(' Paste API token (cky_live_...): ');
|
|
2181
2182
|
if (!token) { console.error('\nToken is required. Aborting.\n'); rl.close(); process.exit(1); }
|
|
2183
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2182
2184
|
console.log('');
|
|
2183
2185
|
deviceCode = ''; // mark as fallback so we skip polling
|
|
2184
2186
|
}
|
|
@@ -2212,6 +2214,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2212
2214
|
if (tokenData.access_token) {
|
|
2213
2215
|
token = tokenData.access_token;
|
|
2214
2216
|
console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
|
|
2217
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2215
2218
|
break;
|
|
2216
2219
|
}
|
|
2217
2220
|
// authorization_pending = keep polling; slow_down = increase interval
|
|
@@ -2328,6 +2331,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2328
2331
|
console.log(` (Run with --instance-id <id> to use a different one)\n`);
|
|
2329
2332
|
}
|
|
2330
2333
|
console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
|
|
2334
|
+
sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
|
|
2331
2335
|
|
|
2332
2336
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
2333
2337
|
const cwd = process.cwd();
|