@cachly-dev/mcp-server 0.10.4 โ 0.10.6
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/.github/workflows/publish.yml +45 -0
- package/LAUNCH_TEXTS.md +182 -0
- package/LICENSE +202 -0
- package/dist/__tests__/auth.test.d.ts +1 -0
- package/dist/__tests__/auth.test.js +107 -0
- package/dist/__tests__/auth.test.js.map +1 -0
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +32 -0
- package/dist/auth.js.map +1 -0
- package/dist/handlers/advanced.js +38 -31
- package/dist/handlers/advanced.js.map +1 -1
- package/dist/handlers/brain.js +144 -50
- package/dist/handlers/brain.js.map +1 -1
- package/dist/handlers/fedbrain.js +55 -38
- package/dist/handlers/fedbrain.js.map +1 -1
- package/dist/handlers/roadmap.js +8 -2
- package/dist/handlers/roadmap.js.map +1 -1
- package/dist/handlers/syndicate.js +15 -7
- package/dist/handlers/syndicate.js.map +1 -1
- package/dist/handlers/team.js +62 -50
- package/dist/handlers/team.js.map +1 -1
- package/dist/index.js +222 -217
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +0 -8
- package/dist/tools.js +5 -15
- package/dist/tools.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/glama.json +29 -0
- package/llms.txt +155 -0
- package/package.json +29 -13
- package/scripts/postinstall.js +12 -19
- package/server.json +3 -9
- package/smithery.yaml +6 -8
- package/src/__tests__/auth.test.ts +147 -0
- package/src/auth.ts +46 -0
- package/src/handlers/advanced.ts +37 -23
- package/src/handlers/brain.ts +141 -47
- package/src/handlers/fedbrain.ts +43 -31
- package/src/handlers/roadmap.ts +7 -2
- package/src/handlers/syndicate.ts +13 -7
- package/src/handlers/team.ts +57 -33
- package/src/index.ts +224 -234
- package/src/tools.ts +5 -16
- package/src/utils.ts +10 -0
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
package/src/handlers/advanced.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { calculateConfidence, confidenceBadge, CONFIDENCE_STALE_VALUE, CONFIDENC
|
|
|
4
4
|
import { ckgSlug, ckgUpdateEdge } from '../ckg.js';
|
|
5
5
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
6
6
|
import { keywordSearch } from '../search.js';
|
|
7
|
+
import { safeJsonParse } from '../utils.js';
|
|
7
8
|
|
|
8
9
|
type GetConnection = (instanceId: string) => Promise<Redis>;
|
|
9
10
|
type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
@@ -45,7 +46,8 @@ export async function handleAdvancedTool(
|
|
|
45
46
|
for (const k of lessonKeys) {
|
|
46
47
|
const raw = await redis.get(k);
|
|
47
48
|
if (!raw) continue;
|
|
48
|
-
|
|
49
|
+
const lesson = safeJsonParse<Lesson | null>(raw, null);
|
|
50
|
+
if (lesson) lessons.set(k, lesson);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
// Detect duplicates: same topic prefix (e.g. deploy:api vs deploy:api-v2)
|
|
@@ -166,8 +168,9 @@ export async function handleAdvancedTool(
|
|
|
166
168
|
for (const k of lessonKeys) {
|
|
167
169
|
const raw = await redis.get(k);
|
|
168
170
|
if (!raw) continue;
|
|
169
|
-
|
|
170
|
-
const l =
|
|
171
|
+
{
|
|
172
|
+
const l = safeJsonParse<Lesson | null>(raw, null);
|
|
173
|
+
if (!l) continue;
|
|
171
174
|
const ts = new Date(l.ts).getTime();
|
|
172
175
|
if (ts >= sinceMs) {
|
|
173
176
|
// Check history to determine add vs update
|
|
@@ -181,7 +184,7 @@ export async function handleAdvancedTool(
|
|
|
181
184
|
// lessons with recalls as "active"
|
|
182
185
|
recalled.push(l);
|
|
183
186
|
}
|
|
184
|
-
}
|
|
187
|
+
}
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
const sinceLabel = match ? `last ${since}` : new Date(sinceMs).toLocaleDateString('de-DE');
|
|
@@ -215,10 +218,10 @@ export async function handleAdvancedTool(
|
|
|
215
218
|
for (const rk of resolutionKeys) {
|
|
216
219
|
const rr = await redis.get(rk);
|
|
217
220
|
if (!rr) continue;
|
|
218
|
-
|
|
219
|
-
const rn =
|
|
220
|
-
if (rn
|
|
221
|
-
}
|
|
221
|
+
{
|
|
222
|
+
const rn = safeJsonParse<{ topic?: string; resolution?: string; ts?: string } | null>(rr, null);
|
|
223
|
+
if (rn?.ts && new Date(rn.ts).getTime() >= sinceMs) recentResolutions.push({ topic: rn.topic ?? rk, resolution: rn.resolution ?? 'unknown', ts: rn.ts });
|
|
224
|
+
}
|
|
222
225
|
}
|
|
223
226
|
if (recentResolutions.length > 0) {
|
|
224
227
|
lines.push(`๐ณ๏ธ **MADC Resolutions** (${recentResolutions.length} contested beliefs resolved):`);
|
|
@@ -238,8 +241,10 @@ export async function handleAdvancedTool(
|
|
|
238
241
|
|
|
239
242
|
// 3. FedBrain transfers received in window
|
|
240
243
|
const fedHistRaw = await redis.lrange('cachly:fedbrain:federations', -20, -1);
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
type FedEntry = { source: string; domain: string; transferred_at: string; nodes: number; edges: number };
|
|
245
|
+
const recentFeds = fedHistRaw
|
|
246
|
+
.map(r => safeJsonParse<FedEntry | null>(r, null))
|
|
247
|
+
.filter((f): f is FedEntry => f !== null && new Date(f.transferred_at).getTime() >= sinceMs);
|
|
243
248
|
if (recentFeds.length > 0) {
|
|
244
249
|
lines.push(`๐ง **FedBrain transfers received (${recentFeds.length}):**`);
|
|
245
250
|
for (const f of recentFeds.slice(0, 3)) {
|
|
@@ -282,27 +287,30 @@ export async function handleAdvancedTool(
|
|
|
282
287
|
for (const nodeKey of nodeKeys.slice(0, 10)) {
|
|
283
288
|
const nodeRaw = await redis.get(nodeKey);
|
|
284
289
|
if (!nodeRaw) continue;
|
|
285
|
-
const node
|
|
290
|
+
const node = safeJsonParse<CKGNode | null>(nodeRaw, null);
|
|
291
|
+
if (!node) continue;
|
|
286
292
|
// Get edges from this node
|
|
287
293
|
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
288
294
|
for (const ek of edgeKeys.slice(0, 20)) {
|
|
289
295
|
const edgeRaw = await redis.get(ek);
|
|
290
296
|
if (!edgeRaw) continue;
|
|
291
|
-
const edge
|
|
297
|
+
const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
|
|
298
|
+
if (!edge) continue;
|
|
292
299
|
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'requires') continue;
|
|
293
300
|
// Find lesson for this concept
|
|
294
301
|
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from.replace(/-/g, ':').replace(/^fix:/, 'fix:')}`);
|
|
295
|
-
const lesson = lessonRaw ?
|
|
296
|
-
ckgResults.push({ conceptId: node.id, edge, lesson });
|
|
302
|
+
const lesson = lessonRaw ? safeJsonParse<{ topic: string; what_worked?: string; ts: string; outcome: string; recall_count?: number; severity?: string } | null>(lessonRaw, null) ?? undefined : undefined;
|
|
303
|
+
ckgResults.push({ conceptId: node.id, edge, lesson: lesson ?? undefined });
|
|
297
304
|
}
|
|
298
305
|
}
|
|
299
306
|
for (const ek of [...fromKeys, ...toKeys].slice(0, 20)) {
|
|
300
307
|
const edgeRaw = await redis.get(ek);
|
|
301
308
|
if (!edgeRaw) continue;
|
|
302
|
-
const edge
|
|
309
|
+
const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
|
|
310
|
+
if (!edge) continue;
|
|
303
311
|
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
304
|
-
const lesson = lessonRaw ?
|
|
305
|
-
ckgResults.push({ conceptId: edge.from, edge, lesson });
|
|
312
|
+
const lesson = lessonRaw ? safeJsonParse<{ topic: string; what_worked?: string; ts: string; outcome: string; recall_count?: number; severity?: string } | null>(lessonRaw, null) ?? undefined : undefined;
|
|
313
|
+
ckgResults.push({ conceptId: edge.from, edge, lesson: lesson ?? undefined });
|
|
306
314
|
}
|
|
307
315
|
}
|
|
308
316
|
} catch { /* CKG traversal non-critical */ }
|
|
@@ -336,14 +344,15 @@ export async function handleAdvancedTool(
|
|
|
336
344
|
for (const k of lessonKeys) {
|
|
337
345
|
const raw = await redis.get(k);
|
|
338
346
|
if (!raw) continue;
|
|
339
|
-
|
|
340
|
-
const l =
|
|
347
|
+
{
|
|
348
|
+
const l = safeJsonParse<Lesson | null>(raw, null);
|
|
349
|
+
if (!l) continue;
|
|
341
350
|
if (filterTags.length > 0 && !(l.tags ?? []).some((t: string) => filterTags.includes(t))) continue;
|
|
342
351
|
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? '', l.context ?? '']
|
|
343
352
|
.join(' ').toLowerCase();
|
|
344
353
|
const score = tokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
|
|
345
354
|
if (score > 0) scored.push({ score, lesson: l, key: k });
|
|
346
|
-
}
|
|
355
|
+
}
|
|
347
356
|
}
|
|
348
357
|
scored.sort((a, b) => b.score - a.score);
|
|
349
358
|
const chain = scored.slice(0, max_depth);
|
|
@@ -420,6 +429,10 @@ export async function handleAdvancedTool(
|
|
|
420
429
|
lines.push(topSolution.what_worked.slice(0, 500));
|
|
421
430
|
lines.push(`\`\`\``);
|
|
422
431
|
lines.push('');
|
|
432
|
+
|
|
433
|
+
// Track estimated time saved โ causal_trace finding a known fix is worth 1-4h
|
|
434
|
+
const savedMins = topSolution.severity === 'critical' ? 240 : topSolution.severity === 'major' ? 120 : 60;
|
|
435
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => {});
|
|
423
436
|
}
|
|
424
437
|
}
|
|
425
438
|
|
|
@@ -452,8 +465,9 @@ export async function handleAdvancedTool(
|
|
|
452
465
|
for (const k of lessonKeys) {
|
|
453
466
|
const raw = await redis.get(k);
|
|
454
467
|
if (!raw) continue;
|
|
455
|
-
|
|
456
|
-
const l =
|
|
468
|
+
{
|
|
469
|
+
const l = safeJsonParse<Lesson | null>(raw, null);
|
|
470
|
+
if (!l) continue;
|
|
457
471
|
const ageMs = now - new Date(l.ts).getTime();
|
|
458
472
|
if (ageMs < minAgeMs) continue;
|
|
459
473
|
const age_days = Math.floor(ageMs / 86400000);
|
|
@@ -470,7 +484,7 @@ export async function handleAdvancedTool(
|
|
|
470
484
|
const confidence = Math.min(100, Math.max(0, base + recallBoost + outcomePenalty));
|
|
471
485
|
|
|
472
486
|
scores.push({ topic: l.topic, confidence, age_days, recalls, outcome: l.outcome });
|
|
473
|
-
}
|
|
487
|
+
}
|
|
474
488
|
}
|
|
475
489
|
|
|
476
490
|
// Sort by lowest confidence first
|
package/src/handlers/brain.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Redis } from 'ioredis';
|
|
|
3
3
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES,
|
|
4
4
|
CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
5
5
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
6
|
+
import { safeJsonParse } from '../utils.js';
|
|
6
7
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
7
8
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein,
|
|
8
9
|
ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from '../search.js';
|
|
@@ -10,6 +11,19 @@ import type { KeywordMatch } from '../search.js';
|
|
|
10
11
|
import { computeEmbedding, hasEmbedProvider, embedProviderHint, EMBED_PROVIDER } from '../embeddings.js';
|
|
11
12
|
import { detectNamespace } from '../namespace.js';
|
|
12
13
|
|
|
14
|
+
// โโ Changelog (shown once per version in session_start) โโโโโโโโโโโโโโโโโโโโโโ
|
|
15
|
+
const MCP_VERSION = '0.10.0';
|
|
16
|
+
const WHATS_NEW: Record<string, string[]> = {
|
|
17
|
+
'0.10.0': [
|
|
18
|
+
`๐ **What's new in v${MCP_VERSION}:**`,
|
|
19
|
+
` โ
Zero-friction setup โ auto-provisions your Brain, no manual config needed`,
|
|
20
|
+
` โ
10ร faster briefings โ batch Redis fetches replace per-key loops`,
|
|
21
|
+
` โ
Crash-proof data reads โ corrupted cache entries no longer break tools`,
|
|
22
|
+
` โ
\`session_end\` warns when called without an active session`,
|
|
23
|
+
` โ
\`get_api_status\` now shows live Redis ping + all instance statuses`,
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
// Shared types used in brain handlers
|
|
14
28
|
export interface Instance {
|
|
15
29
|
id: string; name: string; tier: string; status: string; region: string;
|
|
@@ -167,7 +181,7 @@ export async function handleBrainTool(
|
|
|
167
181
|
for (const dep of depends_on) {
|
|
168
182
|
const depKey = `cachly:dep:${dep}`;
|
|
169
183
|
const existing = await redis.get(depKey);
|
|
170
|
-
const depTopics: string[] =
|
|
184
|
+
const depTopics: string[] = safeJsonParse<string[]>(existing, []);
|
|
171
185
|
if (!depTopics.includes(topic)) depTopics.push(topic);
|
|
172
186
|
await redis.set(depKey, JSON.stringify(depTopics));
|
|
173
187
|
}
|
|
@@ -266,7 +280,8 @@ export async function handleBrainTool(
|
|
|
266
280
|
for (const ek of existingEdgeKeys) {
|
|
267
281
|
const er = await redis.get(ek);
|
|
268
282
|
if (!er) continue;
|
|
269
|
-
const existEdge
|
|
283
|
+
const existEdge = safeJsonParse<CKGEdge | null>(er, null);
|
|
284
|
+
if (!existEdge) continue;
|
|
270
285
|
if (existEdge.edgeType === 'fixes' && existEdge.confidence > 0.7 && existEdge.trials >= 3) {
|
|
271
286
|
beliefConflict = `โ ๏ธ **belief_conflict** on \`${topic}\`: previously confirmed fix (confidence ${existEdge.confidence.toFixed(2)}, n=${existEdge.trials}) now reports failure. Both beliefs retained as \`contested\`. Use \`ckg_inspect(concept="${conceptId}")\` to review.`;
|
|
272
287
|
// Store conflict marker
|
|
@@ -274,7 +289,7 @@ export async function handleBrainTool(
|
|
|
274
289
|
await redis.set(conflictKey, JSON.stringify({ topic, detected_at: new Date().toISOString(), fix_confidence: existEdge.confidence, fix_trials: existEdge.trials, failure_outcome: outcome }), 'EX', 60 * 60 * 24 * 90);
|
|
275
290
|
// Auto-trigger MADC deliberation in background โ never blocks learn_from_attempts
|
|
276
291
|
// Note: fire-and-forget; madc_deliberate is handled elsewhere in the switch
|
|
277
|
-
|
|
292
|
+
// (conflict marker stored above โ no warm-up call needed)
|
|
278
293
|
}
|
|
279
294
|
}
|
|
280
295
|
}
|
|
@@ -286,14 +301,16 @@ export async function handleBrainTool(
|
|
|
286
301
|
for (const ek of fromEdgeKeys.slice(0, 10)) {
|
|
287
302
|
const er = await redis.get(ek);
|
|
288
303
|
if (!er) continue;
|
|
289
|
-
const e2
|
|
304
|
+
const e2 = safeJsonParse<CKGEdge | null>(er, null);
|
|
305
|
+
if (!e2) continue;
|
|
290
306
|
if (e2.edgeType !== 'fixes') continue;
|
|
291
307
|
// Boost second-degree: edges from e2.to get a small fractional success
|
|
292
308
|
const secondKeys = await redis.smembers(`cachly:ckg:idx:from:${e2.to}`);
|
|
293
309
|
for (const sk of secondKeys.slice(0, 5)) {
|
|
294
310
|
const sr = await redis.get(sk);
|
|
295
311
|
if (!sr) continue;
|
|
296
|
-
const se
|
|
312
|
+
const se = safeJsonParse<CKGEdge | null>(sr, null);
|
|
313
|
+
if (!se) continue;
|
|
297
314
|
if (se.edgeType !== 'co-occurs') continue;
|
|
298
315
|
// Add 0.1 fractional success (second-degree signal)
|
|
299
316
|
se.successes = (se.successes || 0) + 0.1;
|
|
@@ -314,7 +331,8 @@ export async function handleBrainTool(
|
|
|
314
331
|
for (const ek of fromEdgeKeys) {
|
|
315
332
|
const er = await redis.get(ek);
|
|
316
333
|
if (!er) continue;
|
|
317
|
-
const de
|
|
334
|
+
const de = safeJsonParse<CKGEdge | null>(er, null);
|
|
335
|
+
if (!de) continue;
|
|
318
336
|
if (de.trials < 3 && de.last_updated && new Date(de.last_updated).getTime() < decayCutoff) {
|
|
319
337
|
de.confidence = de.confidence * 0.9;
|
|
320
338
|
de.last_updated = new Date().toISOString();
|
|
@@ -356,12 +374,13 @@ export async function handleBrainTool(
|
|
|
356
374
|
// Try exact best-solution key first
|
|
357
375
|
const best = await redis.get(`cachly:lesson:best:${topic}`);
|
|
358
376
|
if (best) {
|
|
359
|
-
const lesson =
|
|
377
|
+
const lesson = safeJsonParse(best, null as null | {
|
|
360
378
|
topic: string; outcome: string; what_worked: string; what_failed?: string;
|
|
361
379
|
context?: string; ts: string; verified_at?: string; severity?: string;
|
|
362
380
|
file_paths?: string[]; commands?: string[]; tags?: string[];
|
|
363
381
|
recall_count?: number; audit_trail?: unknown[];
|
|
364
|
-
};
|
|
382
|
+
});
|
|
383
|
+
if (!lesson) return `โ ๏ธ Lesson data for \`${topic}\` is corrupted. Re-store it with \`learn_from_attempts\`.`;
|
|
365
384
|
|
|
366
385
|
// โโ Confidence decay check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
367
386
|
const confidence = calculateConfidence(lesson);
|
|
@@ -378,6 +397,10 @@ export async function handleBrainTool(
|
|
|
378
397
|
};
|
|
379
398
|
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
|
|
380
399
|
|
|
400
|
+
// Track estimated time saved (30m minor ยท 60m major ยท 240m critical)
|
|
401
|
+
const savedMins = lesson.severity === 'critical' ? 240 : lesson.severity === 'major' ? 60 : 30;
|
|
402
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => {});
|
|
403
|
+
|
|
381
404
|
const sevEmoji = lesson.severity === 'critical' ? '๐ด' : lesson.severity === 'major' ? '๐ก' : lesson.severity ? '๐ข' : '';
|
|
382
405
|
const auditSummary = (lesson.audit_trail ?? []).length > 1
|
|
383
406
|
? `_Audit: ${(lesson.audit_trail ?? []).length} changes ยท stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
|
|
@@ -424,7 +447,7 @@ export async function handleBrainTool(
|
|
|
424
447
|
const histKey = `cachly:lessons:${topic}`;
|
|
425
448
|
const all = await redis.lrange(histKey, -3, -1);
|
|
426
449
|
if (all.length > 0) {
|
|
427
|
-
const parsed = all.map(e =>
|
|
450
|
+
const parsed = all.map(e => safeJsonParse(e, null as null | { outcome: string; what_worked: string; ts: string })).filter(Boolean) as Array<{ outcome: string; what_worked: string; ts: string }>;
|
|
428
451
|
const lines = parsed.map(p => `- ${p.outcome === 'success' ? 'โ
' : 'โ'} ${p.what_worked.slice(0, 120)} (${new Date(p.ts).toLocaleDateString('de-DE')})`);
|
|
429
452
|
return `โ ๏ธ No successful solution for \`${topic}\` yet. Last attempts:\n\n${lines.join('\n')}`;
|
|
430
453
|
}
|
|
@@ -436,7 +459,8 @@ export async function handleBrainTool(
|
|
|
436
459
|
for (const k of matching.slice(0, 5)) {
|
|
437
460
|
const raw = await redis.get(k);
|
|
438
461
|
if (!raw) continue;
|
|
439
|
-
const lesson =
|
|
462
|
+
const lesson = safeJsonParse(raw, null as null | { topic: string; what_worked: string; context?: string; ts: string });
|
|
463
|
+
if (!lesson) continue;
|
|
440
464
|
results.push(`**\`${lesson.topic}\`** โ ${lesson.what_worked.slice(0, 200)}`);
|
|
441
465
|
}
|
|
442
466
|
return `๐ **Partial matches for \`${topic}\`:**\n\n${results.join('\n\n')}`;
|
|
@@ -597,10 +621,12 @@ export async function handleBrainTool(
|
|
|
597
621
|
tags?: string[]; confidence?: number; audit_trail?: unknown[];
|
|
598
622
|
};
|
|
599
623
|
const lessons: Lesson[] = [];
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
624
|
+
if (lessonKeys.length > 0) {
|
|
625
|
+
const raws = await redis.mget(...lessonKeys);
|
|
626
|
+
for (const raw of raws) {
|
|
627
|
+
const l = safeJsonParse<Lesson | null>(raw ?? null, null);
|
|
628
|
+
if (l) lessons.push(l);
|
|
629
|
+
}
|
|
604
630
|
}
|
|
605
631
|
lessons.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
|
|
606
632
|
|
|
@@ -617,10 +643,8 @@ export async function handleBrainTool(
|
|
|
617
643
|
|
|
618
644
|
// 4. Last session
|
|
619
645
|
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
try { lastSession = JSON.parse(lastSessionRaw); } catch { /* ignore */ }
|
|
623
|
-
}
|
|
646
|
+
type LastSession = { summary: string; ts: string; files_changed?: string[]; duration_min?: number };
|
|
647
|
+
const lastSession = safeJsonParse<LastSession | null>(lastSessionRaw, null);
|
|
624
648
|
|
|
625
649
|
// 5. Focus filtering
|
|
626
650
|
const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
|
|
@@ -639,7 +663,7 @@ export async function handleBrainTool(
|
|
|
639
663
|
let streakMessage = '';
|
|
640
664
|
try {
|
|
641
665
|
const streakRaw = await redis.get('cachly:streak:current');
|
|
642
|
-
const streak =
|
|
666
|
+
const streak = safeJsonParse<{ days: number; last_date: string; record: number } | null>(streakRaw, null);
|
|
643
667
|
const today = new Date().toISOString().slice(0, 10);
|
|
644
668
|
if (streak) {
|
|
645
669
|
const lastDate = streak.last_date;
|
|
@@ -676,11 +700,54 @@ export async function handleBrainTool(
|
|
|
676
700
|
provider,
|
|
677
701
|
}), 'EX', 86400); // auto-expire after 24h if session_end never called
|
|
678
702
|
|
|
703
|
+
// 8. Time saved counter
|
|
704
|
+
let timeSavedMins = 0;
|
|
705
|
+
try {
|
|
706
|
+
const raw = await redis.get(`cachly:stats:time_saved_mins:${instance_id}`);
|
|
707
|
+
timeSavedMins = parseFloat(raw ?? '0');
|
|
708
|
+
} catch { /* non-critical */ }
|
|
709
|
+
|
|
679
710
|
// โโ Build briefing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
680
711
|
const providerLabel = provider ? ` ยท ${provider}` : '';
|
|
681
712
|
const lines: string[] = [`๐ง **Session Briefing**${providerLabel}`, ''];
|
|
682
713
|
if (streakMessage) lines.push(streakMessage, '');
|
|
683
714
|
|
|
715
|
+
// Time saved (only show when meaningful โ 30+ minutes)
|
|
716
|
+
if (timeSavedMins >= 30) {
|
|
717
|
+
const h = Math.floor(timeSavedMins / 60);
|
|
718
|
+
const m = Math.round(timeSavedMins % 60);
|
|
719
|
+
const timeStr = h > 0 ? `${h}h${m > 0 ? ` ${m}m` : ''}` : `${m}m`;
|
|
720
|
+
lines.push(`โฑ๏ธ **Brain saved you ~${timeStr} total** (time not re-researching known fixes)`, '');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// โโ What's New (shown once per version update) โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
724
|
+
try {
|
|
725
|
+
const seenVersion = await redis.get('cachly:mcp:version:last_seen');
|
|
726
|
+
if (seenVersion !== MCP_VERSION) {
|
|
727
|
+
await redis.set('cachly:mcp:version:last_seen', MCP_VERSION, 'EX', 365 * 86400);
|
|
728
|
+
const changelog = WHATS_NEW[MCP_VERSION];
|
|
729
|
+
if (changelog) { lines.push(...changelog, ''); }
|
|
730
|
+
}
|
|
731
|
+
} catch { /* non-critical */ }
|
|
732
|
+
|
|
733
|
+
// โโ First-time welcome (empty brain) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
734
|
+
const isFirstSession = !lastSession && lessons.length === 0 && ctxCount === 0;
|
|
735
|
+
if (isFirstSession) {
|
|
736
|
+
lines.push('๐ **Welcome! Your AI Brain is live.**', '');
|
|
737
|
+
lines.push('It learns from your work automatically. After your first session it will look like this:', '');
|
|
738
|
+
lines.push(' โ
`api:auth` โ Bearer token in header, not cookie; 401 on missing scope');
|
|
739
|
+
lines.push(' โ
`database:migrations` โ always run migrations before deploy');
|
|
740
|
+
lines.push(' โ ๏ธ `docker:build` โ ARG changes bust all subsequent cache layers');
|
|
741
|
+
lines.push('');
|
|
742
|
+
lines.push('**3 steps to grow your brain:**');
|
|
743
|
+
lines.push(' 1. Fix something โ `learn_from_attempts(topic="bug:name", outcome="success", what_worked="...")`');
|
|
744
|
+
lines.push(' 2. Save context โ `remember_context(key="arch", value="...")`');
|
|
745
|
+
lines.push(' 3. End session โ `session_end(summary="What I did")`');
|
|
746
|
+
lines.push('');
|
|
747
|
+
lines.push('๐ก Run `brain_doctor` for a setup health-check and personalised tips.');
|
|
748
|
+
lines.push('');
|
|
749
|
+
}
|
|
750
|
+
|
|
684
751
|
// Handoff from previous window (if any)
|
|
685
752
|
const handoffRaw = await redis.get('cachly:session:handoff');
|
|
686
753
|
if (handoffRaw) {
|
|
@@ -820,7 +887,8 @@ export async function handleBrainTool(
|
|
|
820
887
|
for (const ck of conflictKeys.slice(0, 3)) {
|
|
821
888
|
const cr = await redis.get(ck);
|
|
822
889
|
if (!cr) continue;
|
|
823
|
-
const cf =
|
|
890
|
+
const cf = safeJsonParse<{ topic: string; fix_confidence: number; fix_trials: number } | null>(cr, null);
|
|
891
|
+
if (!cf) continue;
|
|
824
892
|
lines.push(` โ ๏ธ \`${cf.topic}\` โ previously confirmed fix (${(cf.fix_confidence * 100).toFixed(0)}%, n=${cf.fix_trials}) now contradicted. Use \`ckg_inspect(concept="${ckgSlug(cf.topic)}")\``);
|
|
825
893
|
}
|
|
826
894
|
lines.push('');
|
|
@@ -879,6 +947,15 @@ export async function handleBrainTool(
|
|
|
879
947
|
lines.push('๐ญ No lessons yet. Use `learn_from_attempts` after solving tasks.', '');
|
|
880
948
|
}
|
|
881
949
|
|
|
950
|
+
// Team invite prompt โ fires once after 10th lesson if no team use yet
|
|
951
|
+
if (lessons.length >= 10) {
|
|
952
|
+
const hasTeamUse = lessons.some(l => (l as typeof l & { author?: string }).author);
|
|
953
|
+
if (!hasTeamUse) {
|
|
954
|
+
lines.push(`๐ค **Your Brain has ${lessons.length} lessons โ your team could benefit instantly.**`);
|
|
955
|
+
lines.push(` Share: **cachly.dev/team** ยท or run \`team_learn\` after the next fix.`, '');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
882
959
|
// Open failures (lessons whose best-key has outcome != success)
|
|
883
960
|
const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
|
|
884
961
|
if (openFailures.length > 0) {
|
|
@@ -975,17 +1052,17 @@ export async function handleBrainTool(
|
|
|
975
1052
|
try {
|
|
976
1053
|
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
977
1054
|
if (crystalRaw) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1055
|
+
type CrystalData = { label: string; ts: string; session_count: number; top_patterns: Array<{ category: string; insight: string; count: number }> };
|
|
1056
|
+
const crystal = safeJsonParse<CrystalData | null>(crystalRaw, null);
|
|
1057
|
+
if (crystal) {
|
|
1058
|
+
const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
1059
|
+
if (crystalAge <= 90) {
|
|
1060
|
+
lines.push(`๐ **Memory Crystal** (${crystal.label} ยท ${crystal.session_count} sessions compressed):`);
|
|
1061
|
+
for (const p of crystal.top_patterns.slice(0, 3)) {
|
|
1062
|
+
lines.push(` โข **${p.category}** (${p.count}ร): ${p.insight.slice(0, 90)}`);
|
|
1063
|
+
}
|
|
1064
|
+
lines.push('');
|
|
987
1065
|
}
|
|
988
|
-
lines.push('');
|
|
989
1066
|
}
|
|
990
1067
|
}
|
|
991
1068
|
} catch { /* non-critical */ }
|
|
@@ -997,7 +1074,10 @@ export async function handleBrainTool(
|
|
|
997
1074
|
const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
998
1075
|
const PRIORITY_ICON: Record<string, string> = { critical: '๐ด', high: '๐ ', medium: '๐ก', low: '๐ต' };
|
|
999
1076
|
const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
|
|
1000
|
-
const allItems = Object.values(roadmapAll).
|
|
1077
|
+
const allItems = Object.values(roadmapAll).flatMap(v => {
|
|
1078
|
+
const item = safeJsonParse<Record<string, unknown> | null>(v as string, null);
|
|
1079
|
+
return item ? [item] : [];
|
|
1080
|
+
});
|
|
1001
1081
|
const openItems = allItems
|
|
1002
1082
|
.filter(i => openStatuses.has(i.status as string))
|
|
1003
1083
|
.sort((a, b) => {
|
|
@@ -1037,20 +1117,20 @@ export async function handleBrainTool(
|
|
|
1037
1117
|
for (const nk of nodeKeys.slice(0, 3)) {
|
|
1038
1118
|
const nodeRaw = await redis.get(nk);
|
|
1039
1119
|
if (!nodeRaw) continue;
|
|
1040
|
-
const node
|
|
1120
|
+
const node = safeJsonParse<CKGNode | null>(nodeRaw, null);
|
|
1121
|
+
if (!node) continue;
|
|
1041
1122
|
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
1042
1123
|
for (const ek of edgeKeys.slice(0, 15)) {
|
|
1043
1124
|
const edgeRaw = await redis.get(ek);
|
|
1044
1125
|
if (!edgeRaw) continue;
|
|
1045
|
-
const edge
|
|
1126
|
+
const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
|
|
1127
|
+
if (!edge) continue;
|
|
1046
1128
|
if (edge.edgeType !== 'causes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'fixes') continue;
|
|
1047
1129
|
if (edge.confidence < 0.35) continue;
|
|
1048
|
-
// For "fixes" edges, the lesson is on the "from" node (fix for "to")
|
|
1049
|
-
// For "causes" edges, target is the failure mode
|
|
1050
1130
|
const lessonKey = edge.edgeType === 'fixes' ? `cachly:lesson:best:${edge.from}` : `cachly:lesson:best:${edge.to}`;
|
|
1051
1131
|
const lessonRaw = await redis.get(lessonKey);
|
|
1052
|
-
const lesson =
|
|
1053
|
-
ppePredictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
|
|
1132
|
+
const lesson = safeJsonParse<{ what_worked?: string; topic: string } | null>(lessonRaw, null);
|
|
1133
|
+
ppePredictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson: lesson ?? undefined });
|
|
1054
1134
|
}
|
|
1055
1135
|
}
|
|
1056
1136
|
}
|
|
@@ -1081,7 +1161,7 @@ export async function handleBrainTool(
|
|
|
1081
1161
|
// Measures: of recalled lessons with confidence > 0.85, what % had outcome=success?
|
|
1082
1162
|
try {
|
|
1083
1163
|
const calRaw = await redis.get('cachly:mcm:calibration:last');
|
|
1084
|
-
const lastCalMs =
|
|
1164
|
+
const lastCalMs = safeJsonParse<{ ts?: number } | null>(calRaw, null)?.ts ?? 0;
|
|
1085
1165
|
const daysSinceCal = (Date.now() - lastCalMs) / 86_400_000;
|
|
1086
1166
|
if (daysSinceCal >= 30 && lessons.length >= 5) {
|
|
1087
1167
|
// Quick calibration pass on recalled lessons (recall_count > 0, outcome=success)
|
|
@@ -1125,8 +1205,6 @@ export async function handleBrainTool(
|
|
|
1125
1205
|
} catch { /* non-critical */ }
|
|
1126
1206
|
|
|
1127
1207
|
// โโ ๐ Knowledge Commons โ community stats banner โโโโโโโโโโโโโโโโโโโโโโโ
|
|
1128
|
-
// Fetch syndication stats and show a 1-liner: total lessons + confirms + weekly growth.
|
|
1129
|
-
// Non-fatal: never blocks session start if the API call fails.
|
|
1130
1208
|
try {
|
|
1131
1209
|
const commonsStats = await apiFetch<{
|
|
1132
1210
|
total_lessons: number;
|
|
@@ -1143,6 +1221,17 @@ export async function handleBrainTool(
|
|
|
1143
1221
|
}
|
|
1144
1222
|
} catch { /* non-critical โ never block session start */ }
|
|
1145
1223
|
|
|
1224
|
+
// โโ Brain Doctor hint (Punkt 6) โ surface when brain needs attention โโโ
|
|
1225
|
+
const hasOpenFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial').length > 0;
|
|
1226
|
+
const hasStaleLessons = lessons.some(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
1227
|
+
if (lessons.length === 0 || hasOpenFailures || hasStaleLessons) {
|
|
1228
|
+
const reasons: string[] = [];
|
|
1229
|
+
if (lessons.length === 0) reasons.push('brain is empty');
|
|
1230
|
+
if (hasOpenFailures) reasons.push('open failures');
|
|
1231
|
+
if (hasStaleLessons) reasons.push('stale lessons');
|
|
1232
|
+
lines.push(`๐ฉบ _Run \`brain_doctor\` to fix: ${reasons.join(', ')}._`);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1146
1235
|
return lines.join('\n');
|
|
1147
1236
|
}
|
|
1148
1237
|
|
|
@@ -1168,18 +1257,23 @@ export async function handleBrainTool(
|
|
|
1168
1257
|
// Calculate duration from session_start marker
|
|
1169
1258
|
let durationMin: number | undefined;
|
|
1170
1259
|
const currentRaw = await redis.get('cachly:session:current');
|
|
1171
|
-
if (currentRaw) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1260
|
+
if (!currentRaw) {
|
|
1261
|
+
return `โ ๏ธ No active session found.\n\nRun \`session_start\` first to begin tracking a session, then call \`session_end\` when you're done.`;
|
|
1262
|
+
}
|
|
1263
|
+
const current = safeJsonParse<{ started: string } | null>(currentRaw, null);
|
|
1264
|
+
if (current?.started) {
|
|
1265
|
+
durationMin = Math.round((now.getTime() - new Date(current.started).getTime()) / 60000);
|
|
1176
1266
|
}
|
|
1177
1267
|
|
|
1178
1268
|
// โโ Session Replay: capture decision log โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1179
|
-
|
|
1269
|
+
type DecisionEntry = { ts: string; topic: string; outcome: string; what_worked: string };
|
|
1270
|
+
let decisionLog: DecisionEntry[] = [];
|
|
1180
1271
|
try {
|
|
1181
1272
|
const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
|
|
1182
|
-
decisionLog = dlEntries.
|
|
1273
|
+
decisionLog = dlEntries.flatMap(e => {
|
|
1274
|
+
const entry = safeJsonParse<DecisionEntry | null>(e, null);
|
|
1275
|
+
return entry ? [entry] : [];
|
|
1276
|
+
});
|
|
1183
1277
|
await redis.del('cachly:session:decision-log');
|
|
1184
1278
|
} catch { /* non-critical */ }
|
|
1185
1279
|
|