@cachly-dev/mcp-server 0.9.1 → 0.9.2
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 -28
- package/Dockerfile +2 -4
- package/README.md +173 -310
- package/dist/index.js +419 -329
- package/dist/index.js.map +1 -1
- package/package.json +4 -10
- package/server.json +3 -3
- package/src/index.ts +448 -356
- package/test-features.cjs +23 -0
- package/.github/workflows/publish.yml +0 -42
- package/LICENSE +0 -202
- package/glama.json +0 -6
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ import { Redis } from 'ioredis';
|
|
|
47
47
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
48
48
|
const JWT = process.env.CACHLY_JWT ?? '';
|
|
49
49
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
50
|
-
const CURRENT_VERSION = '0.
|
|
50
|
+
const CURRENT_VERSION = '0.9.2';
|
|
51
51
|
/**
|
|
52
52
|
* ┌──────────────────────────────────────────────────────────────────────┐
|
|
53
53
|
* │ EMBEDDING PROVIDER — pluggable, client-side first │
|
|
@@ -2171,6 +2171,45 @@ async function apiFetch(path, options = {}) {
|
|
|
2171
2171
|
return res.json();
|
|
2172
2172
|
}
|
|
2173
2173
|
import { detectNamespace } from './namespace.js';
|
|
2174
|
+
const STOPWORDS_CKG = new Set(['that', 'this', 'with', 'from', 'when', 'then', 'also', 'have', 'been', 'will', 'were', 'they', 'them', 'than', 'more', 'some', 'into', 'over', 'only', 'just', 'where', 'while', 'which', 'there', 'their', 'would', 'could', 'should', 'after', 'before', 'about']);
|
|
2175
|
+
function ckgSlug(text) {
|
|
2176
|
+
return text.toLowerCase().replace(/[^a-z0-9\-:]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
|
|
2177
|
+
}
|
|
2178
|
+
/** Extract 1-3 significant keywords from free text for a problem concept */
|
|
2179
|
+
function extractProblemConcept(text) {
|
|
2180
|
+
const words = text.toLowerCase()
|
|
2181
|
+
.replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/)
|
|
2182
|
+
.filter(w => w.length > 3 && !STOPWORDS_CKG.has(w))
|
|
2183
|
+
.slice(0, 5);
|
|
2184
|
+
if (words.length === 0)
|
|
2185
|
+
return null;
|
|
2186
|
+
return words.slice(0, 2).join('-');
|
|
2187
|
+
}
|
|
2188
|
+
async function ckgUpsertNode(redis, id, domain, type) {
|
|
2189
|
+
const key = `cachly:ckg:node:${id}`;
|
|
2190
|
+
const raw = await redis.get(key);
|
|
2191
|
+
const node = raw ? JSON.parse(raw) : { id, domain, type, count: 0, ts: new Date().toISOString() };
|
|
2192
|
+
node.count = (node.count || 0) + 1;
|
|
2193
|
+
node.ts = new Date().toISOString();
|
|
2194
|
+
await redis.set(key, JSON.stringify(node));
|
|
2195
|
+
}
|
|
2196
|
+
async function ckgUpdateEdge(redis, from, edgeType, to, success, partial = false) {
|
|
2197
|
+
const key = `cachly:ckg:edge:${from}:${edgeType}:${to}`;
|
|
2198
|
+
const raw = await redis.get(key);
|
|
2199
|
+
const edge = raw ? JSON.parse(raw) : { from, to, edgeType, successes: 0, trials: 0, confidence: 0, last_updated: '' };
|
|
2200
|
+
edge.trials = (edge.trials || 0) + 1;
|
|
2201
|
+
if (success)
|
|
2202
|
+
edge.successes = (edge.successes || 0) + 1;
|
|
2203
|
+
else if (partial)
|
|
2204
|
+
edge.successes = (edge.successes || 0) + 0.5;
|
|
2205
|
+
// Beta distribution smoothed confidence: (successes+1) / (trials+2)
|
|
2206
|
+
edge.confidence = (edge.successes + 1) / (edge.trials + 2);
|
|
2207
|
+
edge.last_updated = new Date().toISOString();
|
|
2208
|
+
await redis.set(key, JSON.stringify(edge));
|
|
2209
|
+
// Index: set of edge keys per source node (for fast traversal)
|
|
2210
|
+
await redis.sadd(`cachly:ckg:idx:from:${from}`, key);
|
|
2211
|
+
await redis.sadd(`cachly:ckg:idx:to:${to}`, key);
|
|
2212
|
+
}
|
|
2174
2213
|
// ── Tools ─────────────────────────────────────────────────────────────────────
|
|
2175
2214
|
const TOOLS = [
|
|
2176
2215
|
// ── Instance Management ──────────────────────────────────────────────────
|
|
@@ -2976,21 +3015,6 @@ const TOOLS = [
|
|
|
2976
3015
|
required: ['instance_id'],
|
|
2977
3016
|
},
|
|
2978
3017
|
},
|
|
2979
|
-
// ── crystal_view — inspect current Memory Crystal ──────────────────────
|
|
2980
|
-
{
|
|
2981
|
-
name: 'crystal_view',
|
|
2982
|
-
description: 'View the current Memory Crystal — the distilled snapshot of everything your AI brain has learned. ' +
|
|
2983
|
-
'Shows all top patterns grouped by category, lesson count, crystal age, and when to refresh. ' +
|
|
2984
|
-
'A crystal is created by memory_crystalize and injected into every session_start automatically. ' +
|
|
2985
|
-
'Call this to review what institutional knowledge is preserved, or to decide if a refresh is due.',
|
|
2986
|
-
inputSchema: {
|
|
2987
|
-
type: 'object',
|
|
2988
|
-
properties: {
|
|
2989
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
2990
|
-
},
|
|
2991
|
-
required: ['instance_id'],
|
|
2992
|
-
},
|
|
2993
|
-
},
|
|
2994
3018
|
// ── Roadmap — Persistent project plan tracker ───────────────────────────
|
|
2995
3019
|
{
|
|
2996
3020
|
name: 'roadmap_add',
|
|
@@ -3480,58 +3504,50 @@ const TOOLS = [
|
|
|
3480
3504
|
required: [],
|
|
3481
3505
|
},
|
|
3482
3506
|
},
|
|
3483
|
-
// ──
|
|
3507
|
+
// ── Layer 1: Causal Knowledge Graph ────────────────────────────────────────
|
|
3484
3508
|
{
|
|
3485
|
-
name: '
|
|
3486
|
-
description: '
|
|
3487
|
-
'
|
|
3488
|
-
'
|
|
3489
|
-
'Unlike session_start, this is instant — no full brain scan, no streak logic, just the critical recovery data. ' +
|
|
3490
|
-
'Typical use in CLAUDE.md: "If context is compacted, call compact_recover immediately."',
|
|
3509
|
+
name: 'brain_search',
|
|
3510
|
+
description: 'BM25+ full-text search over ALL brain data: lessons, context entries, session history, CKG nodes, roadmap items. ' +
|
|
3511
|
+
'Unlike smart_recall (which focuses on lessons + context), brain_search casts a wider net. ' +
|
|
3512
|
+
'Use when smart_recall returns nothing or when you want to find anything the brain knows about a topic.',
|
|
3491
3513
|
inputSchema: {
|
|
3492
3514
|
type: 'object',
|
|
3493
3515
|
properties: {
|
|
3494
|
-
instance_id: { type: 'string', description: '
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
description: 'What you are trying to recover context for — used to surface relevant lessons (optional)',
|
|
3498
|
-
},
|
|
3516
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3517
|
+
query: { type: 'string', description: 'What to search for' },
|
|
3518
|
+
limit: { type: 'number', description: 'Max results (default: 15)' },
|
|
3499
3519
|
},
|
|
3500
|
-
required: ['instance_id'],
|
|
3520
|
+
required: ['instance_id', 'query'],
|
|
3501
3521
|
},
|
|
3502
3522
|
},
|
|
3503
|
-
// ── v0.9 — brain_from_git ─────────────────────────────────────────────────
|
|
3504
3523
|
{
|
|
3505
|
-
name: '
|
|
3506
|
-
description: '
|
|
3507
|
-
'
|
|
3508
|
-
'
|
|
3509
|
-
'Run once to pre-load months of team knowledge before the first session_start. ' +
|
|
3510
|
-
'Skips commits already in the Brain (safe to re-run). ' +
|
|
3511
|
-
'Example: brain_from_git(instance_id="...", workspace_path="/home/you/project", days=180) ' +
|
|
3512
|
-
'→ "📊 Scanned 342 commits · ✅ 28 lessons stored · ❌ 9 failure lessons · 0 duplicates skipped"',
|
|
3524
|
+
name: 'ckg_inspect',
|
|
3525
|
+
description: 'Inspect the Causal Knowledge Graph (CKG) for a concept. Shows all typed edges (fixes, requires, co-occurs, causes) ' +
|
|
3526
|
+
'with Bayesian confidence scores. Use to understand what the brain knows about a topic and which fixes have the ' +
|
|
3527
|
+
'highest confidence. Also shows related concepts via graph traversal.',
|
|
3513
3528
|
inputSchema: {
|
|
3514
3529
|
type: 'object',
|
|
3515
3530
|
properties: {
|
|
3516
|
-
instance_id: { type: 'string', description: '
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
description: 'Absolute path to the git repository root (e.g. "/home/you/myproject")',
|
|
3520
|
-
},
|
|
3521
|
-
days: {
|
|
3522
|
-
type: 'number',
|
|
3523
|
-
description: 'How many days of history to scan (default: 180, max: 365)',
|
|
3524
|
-
},
|
|
3525
|
-
max_commits: {
|
|
3526
|
-
type: 'number',
|
|
3527
|
-
description: 'Maximum number of commits to process (default: 200, max: 500)',
|
|
3528
|
-
},
|
|
3529
|
-
author: {
|
|
3530
|
-
type: 'string',
|
|
3531
|
-
description: 'Filter to commits by this git author email or name (optional — omit to scan all authors)',
|
|
3532
|
-
},
|
|
3531
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3532
|
+
concept: { type: 'string', description: 'Concept to inspect, e.g. "fix:clickhouse-ipv6" or "docker"' },
|
|
3533
|
+
max_hops: { type: 'number', description: 'Traversal depth (default: 2)' },
|
|
3533
3534
|
},
|
|
3534
|
-
required: ['instance_id', '
|
|
3535
|
+
required: ['instance_id', 'concept'],
|
|
3536
|
+
},
|
|
3537
|
+
},
|
|
3538
|
+
{
|
|
3539
|
+
name: 'brain_predict',
|
|
3540
|
+
description: 'Predictive Pre-fetch Engine (PPE): given your current context (what you\'re working on), traverses the CKG ' +
|
|
3541
|
+
'to predict likely failures and pre-load relevant fixes. Returns top predicted pitfalls + highest-confidence fixes. ' +
|
|
3542
|
+
'Call at session_start when working on a specific feature or debugging area.',
|
|
3543
|
+
inputSchema: {
|
|
3544
|
+
type: 'object',
|
|
3545
|
+
properties: {
|
|
3546
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3547
|
+
context: { type: 'string', description: 'What you\'re working on, e.g. "upgrading Keycloak from 21 to 24"' },
|
|
3548
|
+
top_k: { type: 'number', description: 'Max predictions to return (default: 5)' },
|
|
3549
|
+
},
|
|
3550
|
+
required: ['instance_id', 'context'],
|
|
3535
3551
|
},
|
|
3536
3552
|
},
|
|
3537
3553
|
];
|
|
@@ -4442,6 +4458,46 @@ async function handleTool(name, args) {
|
|
|
4442
4458
|
await redis.ltrim(dlKey, -50, -1);
|
|
4443
4459
|
}
|
|
4444
4460
|
catch { /* non-critical */ }
|
|
4461
|
+
// ── Layer 1+2: CKG update (Causal Knowledge Graph + Belief Update Engine) ──
|
|
4462
|
+
try {
|
|
4463
|
+
const conceptId = ckgSlug(topic);
|
|
4464
|
+
const domain = topic.split(':')[0] ?? 'unknown';
|
|
4465
|
+
const conceptType = domain; // fix, debug, deploy, infra, api, etc.
|
|
4466
|
+
// Upsert concept node
|
|
4467
|
+
await ckgUpsertNode(redis, conceptId, domain, conceptType);
|
|
4468
|
+
// Tag co-occurrence edges
|
|
4469
|
+
for (const tag of tags) {
|
|
4470
|
+
const tagId = ckgSlug(`tag:${tag}`);
|
|
4471
|
+
await ckgUpsertNode(redis, tagId, 'tag', 'tag');
|
|
4472
|
+
await ckgUpdateEdge(redis, conceptId, 'co-occurs', tagId, outcome === 'success', outcome === 'partial');
|
|
4473
|
+
}
|
|
4474
|
+
// depends_on → requires edges (structural, always confidence 1.0 direction)
|
|
4475
|
+
for (const dep of depends_on) {
|
|
4476
|
+
const depId = ckgSlug(dep);
|
|
4477
|
+
await ckgUpdateEdge(redis, conceptId, 'requires', depId, true);
|
|
4478
|
+
}
|
|
4479
|
+
// fixes edge: if category=fix and outcome=success, link to problem concept
|
|
4480
|
+
if ((domain === 'fix' || domain === 'debug') && (outcome === 'success' || outcome === 'partial')) {
|
|
4481
|
+
const problemText = what_failed || ctx || '';
|
|
4482
|
+
const problemConcept = problemText ? extractProblemConcept(problemText) : null;
|
|
4483
|
+
if (problemConcept) {
|
|
4484
|
+
const problemId = ckgSlug(`problem:${problemConcept}`);
|
|
4485
|
+
await ckgUpsertNode(redis, problemId, 'problem', 'problem');
|
|
4486
|
+
await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, outcome === 'success', outcome === 'partial');
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
// causes edge: if outcome=failure, link topic concept to the problem context
|
|
4490
|
+
if (outcome === 'failure' && (what_failed || what_worked)) {
|
|
4491
|
+
const causeText = what_failed || what_worked;
|
|
4492
|
+
const causeConcept = extractProblemConcept(causeText);
|
|
4493
|
+
if (causeConcept) {
|
|
4494
|
+
const causeId = ckgSlug(`cause:${causeConcept}`);
|
|
4495
|
+
await ckgUpsertNode(redis, causeId, 'cause', 'cause');
|
|
4496
|
+
await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
catch { /* CKG updates are non-critical */ }
|
|
4445
4501
|
const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
|
|
4446
4502
|
const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
|
|
4447
4503
|
const action = isUpdate ? 'updated' : 'stored';
|
|
@@ -4917,7 +4973,37 @@ async function handleTool(name, args) {
|
|
|
4917
4973
|
}
|
|
4918
4974
|
// Brain health
|
|
4919
4975
|
lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
|
|
4920
|
-
//
|
|
4976
|
+
// ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
|
|
4977
|
+
if (lessons.length >= 3) {
|
|
4978
|
+
const domainMap = new Map();
|
|
4979
|
+
for (const l of lessons) {
|
|
4980
|
+
const dom = l.topic.split(':')[0] ?? 'other';
|
|
4981
|
+
if (!domainMap.has(dom))
|
|
4982
|
+
domainMap.set(dom, { total: 0, success: 0, critical: 0 });
|
|
4983
|
+
const d = domainMap.get(dom);
|
|
4984
|
+
d.total++;
|
|
4985
|
+
if (l.outcome === 'success')
|
|
4986
|
+
d.success++;
|
|
4987
|
+
if (l.severity === 'critical')
|
|
4988
|
+
d.critical++;
|
|
4989
|
+
}
|
|
4990
|
+
const sorted = [...domainMap.entries()].sort((a, b) => b[1].total - a[1].total).slice(0, 6);
|
|
4991
|
+
const hasContestedDomains = sorted.some(([, d]) => d.success < d.total * 0.4 && d.total >= 2);
|
|
4992
|
+
if (sorted.length > 0) {
|
|
4993
|
+
lines.push(`🗺️ **Knowledge Coverage:**`);
|
|
4994
|
+
for (const [dom, d] of sorted) {
|
|
4995
|
+
const pct = Math.round((d.success / d.total) * 100);
|
|
4996
|
+
const filled = Math.round(pct / 10);
|
|
4997
|
+
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
4998
|
+
const flag = d.critical > 0 ? ' 🔴' : pct < 40 && d.total >= 2 ? ' ⚠️' : '';
|
|
4999
|
+
lines.push(` ${bar} ${dom.padEnd(18)} ${String(pct).padStart(3)}% (${d.success}/${d.total} confirmed)${flag}`);
|
|
5000
|
+
}
|
|
5001
|
+
if (hasContestedDomains) {
|
|
5002
|
+
lines.push(` ⚠️ _Some domains have contested beliefs — use \`ckg_inspect\` to review_`);
|
|
5003
|
+
}
|
|
5004
|
+
lines.push('');
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
4921
5007
|
if (focusLessons.length > 0) {
|
|
4922
5008
|
lines.push(`🎯 **Relevant for "${focus}":**`);
|
|
4923
5009
|
for (const l of focusLessons.slice(0, 4)) {
|
|
@@ -5255,22 +5341,6 @@ async function handleTool(name, args) {
|
|
|
5255
5341
|
}
|
|
5256
5342
|
catch { /* git not available or not a repo — silent skip */ }
|
|
5257
5343
|
}
|
|
5258
|
-
// ── Crystal age check — remind if > 30 days old or missing ──────────────
|
|
5259
|
-
let crystalReminder = '';
|
|
5260
|
-
try {
|
|
5261
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
5262
|
-
if (!crystalRaw) {
|
|
5263
|
-
crystalReminder = `💎 **Memory Crystal:** None exists yet. Run \`memory_crystalize(instance_id="${instance_id}")\` to distill your brain into a permanent snapshot — injected into every future session automatically.`;
|
|
5264
|
-
}
|
|
5265
|
-
else {
|
|
5266
|
-
const crystal = JSON.parse(crystalRaw);
|
|
5267
|
-
const crystalAgeDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
5268
|
-
if (crystalAgeDays >= 30) {
|
|
5269
|
-
crystalReminder = `💎 **Memory Crystal is ${crystalAgeDays} days old** (${crystal.label ?? 'unnamed'}). Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh it with your latest ${crystal.lesson_count ?? '?'} lessons.`;
|
|
5270
|
-
}
|
|
5271
|
-
}
|
|
5272
|
-
}
|
|
5273
|
-
catch { /* non-critical */ }
|
|
5274
5344
|
const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
|
|
5275
5345
|
return [
|
|
5276
5346
|
`✅ **Session saved**${durationStr}`,
|
|
@@ -5280,7 +5350,6 @@ async function handleTool(name, args) {
|
|
|
5280
5350
|
lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
|
|
5281
5351
|
autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
|
|
5282
5352
|
ambientLearned.length > 0 ? `🌿 **Ambient git learning:** ${ambientLearned.length} commit${ambientLearned.length > 1 ? 's' : ''} auto-learned (${ambientLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${ambientLearned.length > 3 ? '…' : ''})` : '',
|
|
5283
|
-
crystalReminder,
|
|
5284
5353
|
``,
|
|
5285
5354
|
`💡 Next session: \`session_start(focus="...")\` to see this summary.`,
|
|
5286
5355
|
].filter(l => l !== '').join('\n');
|
|
@@ -5669,52 +5738,6 @@ async function handleTool(name, args) {
|
|
|
5669
5738
|
];
|
|
5670
5739
|
return lines.join('\n');
|
|
5671
5740
|
}
|
|
5672
|
-
// ── crystal_view — show current Memory Crystal ───────────────────────────
|
|
5673
|
-
case 'crystal_view': {
|
|
5674
|
-
const { instance_id } = args;
|
|
5675
|
-
const redis = await getConnection(instance_id);
|
|
5676
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
5677
|
-
if (!crystalRaw) {
|
|
5678
|
-
return [
|
|
5679
|
-
`💎 **No Memory Crystal yet**`,
|
|
5680
|
-
``,
|
|
5681
|
-
`Your brain has no crystal — all knowledge lives in raw lessons only.`,
|
|
5682
|
-
``,
|
|
5683
|
-
`Run \`memory_crystalize(instance_id="${instance_id}")\` to:`,
|
|
5684
|
-
` • Distill everything learned into a permanent, compact snapshot`,
|
|
5685
|
-
` • Inject it into every future \`session_start\` automatically`,
|
|
5686
|
-
` • Preserve team knowledge even after lesson TTLs expire`,
|
|
5687
|
-
``,
|
|
5688
|
-
`💡 Recommended: run monthly or after any major milestone.`,
|
|
5689
|
-
].join('\n');
|
|
5690
|
-
}
|
|
5691
|
-
const crystal = JSON.parse(crystalRaw);
|
|
5692
|
-
const ageDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
5693
|
-
const ageStr = ageDays === 0 ? 'today' : ageDays === 1 ? '1 day ago' : `${ageDays} days ago`;
|
|
5694
|
-
const freshness = ageDays < 14 ? '🟢 Fresh' : ageDays < 30 ? '🟡 Aging' : '🔴 Stale — refresh recommended';
|
|
5695
|
-
const lines = [
|
|
5696
|
-
`💎 **Memory Crystal: ${crystal.label}**`,
|
|
5697
|
-
``,
|
|
5698
|
-
`📅 Created: **${ageStr}** · Freshness: ${freshness}`,
|
|
5699
|
-
`📊 Compressed: **${crystal.session_count} sessions** + **${crystal.lesson_count} lessons** → ${crystal.top_patterns.length} patterns`,
|
|
5700
|
-
`🗂️ Categories: ${crystal.categories.slice(0, 10).join(', ')}${crystal.categories.length > 10 ? `… +${crystal.categories.length - 10}` : ''}`,
|
|
5701
|
-
``,
|
|
5702
|
-
`**Top patterns by category:**`,
|
|
5703
|
-
``,
|
|
5704
|
-
];
|
|
5705
|
-
for (const p of crystal.top_patterns) {
|
|
5706
|
-
lines.push(`**${p.category}** (${p.count} lesson${p.count > 1 ? 's' : ''})`);
|
|
5707
|
-
lines.push(` → ${p.insight.slice(0, 150)}${p.insight.length > 150 ? '…' : ''}`);
|
|
5708
|
-
lines.push('');
|
|
5709
|
-
}
|
|
5710
|
-
if (ageDays >= 30) {
|
|
5711
|
-
lines.push(`---`, `⚠️ Crystal is ${ageDays} days old. Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh with your latest lessons.`);
|
|
5712
|
-
}
|
|
5713
|
-
else {
|
|
5714
|
-
lines.push(`---`, `💡 This crystal is injected automatically into every \`session_start\`. Refresh monthly with \`memory_crystalize\`.`);
|
|
5715
|
-
}
|
|
5716
|
-
return lines.filter(l => l !== undefined).join('\n');
|
|
5717
|
-
}
|
|
5718
5741
|
case 'brain_doctor': {
|
|
5719
5742
|
const { instance_id, workspace_path: drWorkspacePath = '' } = args;
|
|
5720
5743
|
const redis = await getConnection(instance_id);
|
|
@@ -6944,7 +6967,59 @@ async function handleBulkLockStream(name, args) {
|
|
|
6944
6967
|
.replace(/[^a-z0-9\s\-_:]/g, ' ')
|
|
6945
6968
|
.split(/\s+/)
|
|
6946
6969
|
.filter(t => t.length > 2);
|
|
6947
|
-
|
|
6970
|
+
const SEV_ICON = { critical: '🔴', major: '🟠', minor: '🟡' };
|
|
6971
|
+
const ckgResults = [];
|
|
6972
|
+
try {
|
|
6973
|
+
for (const token of tokens.slice(0, 4)) {
|
|
6974
|
+
// Search for CKG nodes matching this token
|
|
6975
|
+
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(token)}`);
|
|
6976
|
+
const toKeys = await redis.smembers(`cachly:ckg:idx:to:${ckgSlug(token)}`);
|
|
6977
|
+
// Also try pattern: scan nodes containing the token
|
|
6978
|
+
const nodeKeys = [];
|
|
6979
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
|
|
6980
|
+
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
6981
|
+
for (const nodeKey of nodeKeys.slice(0, 10)) {
|
|
6982
|
+
const nodeRaw = await redis.get(nodeKey);
|
|
6983
|
+
if (!nodeRaw)
|
|
6984
|
+
continue;
|
|
6985
|
+
const node = JSON.parse(nodeRaw);
|
|
6986
|
+
// Get edges from this node
|
|
6987
|
+
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
6988
|
+
for (const ek of edgeKeys.slice(0, 20)) {
|
|
6989
|
+
const edgeRaw = await redis.get(ek);
|
|
6990
|
+
if (!edgeRaw)
|
|
6991
|
+
continue;
|
|
6992
|
+
const edge = JSON.parse(edgeRaw);
|
|
6993
|
+
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'requires')
|
|
6994
|
+
continue;
|
|
6995
|
+
// Find lesson for this concept
|
|
6996
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from.replace(/-/g, ':').replace(/^fix:/, 'fix:')}`);
|
|
6997
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
6998
|
+
ckgResults.push({ conceptId: node.id, edge, lesson });
|
|
6999
|
+
}
|
|
7000
|
+
}
|
|
7001
|
+
for (const ek of [...fromKeys, ...toKeys].slice(0, 20)) {
|
|
7002
|
+
const edgeRaw = await redis.get(ek);
|
|
7003
|
+
if (!edgeRaw)
|
|
7004
|
+
continue;
|
|
7005
|
+
const edge = JSON.parse(edgeRaw);
|
|
7006
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
7007
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
7008
|
+
ckgResults.push({ conceptId: edge.from, edge, lesson });
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
catch { /* CKG traversal non-critical */ }
|
|
7013
|
+
// Deduplicate CKG results and sort by confidence
|
|
7014
|
+
const ckgSeen = new Set();
|
|
7015
|
+
const ckgDeduped = ckgResults.filter(r => {
|
|
7016
|
+
const key = `${r.edge.from}:${r.edge.edgeType}:${r.edge.to}`;
|
|
7017
|
+
if (ckgSeen.has(key))
|
|
7018
|
+
return false;
|
|
7019
|
+
ckgSeen.add(key);
|
|
7020
|
+
return true;
|
|
7021
|
+
}).sort((a, b) => b.edge.confidence - a.edge.confidence).slice(0, max_depth);
|
|
7022
|
+
// ── Layer 2 (fallback): text similarity over all lessons ─────────────────
|
|
6948
7023
|
let cursor = 0;
|
|
6949
7024
|
const lessonKeys = [];
|
|
6950
7025
|
do {
|
|
@@ -6960,7 +7035,6 @@ async function handleBulkLockStream(name, args) {
|
|
|
6960
7035
|
continue;
|
|
6961
7036
|
try {
|
|
6962
7037
|
const l = JSON.parse(raw);
|
|
6963
|
-
// Skip if filter tags given and lesson has none of them
|
|
6964
7038
|
if (filterTags.length > 0 && !(l.tags ?? []).some((t) => filterTags.includes(t)))
|
|
6965
7039
|
continue;
|
|
6966
7040
|
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? '', l.context ?? '']
|
|
@@ -6973,7 +7047,7 @@ async function handleBulkLockStream(name, args) {
|
|
|
6973
7047
|
}
|
|
6974
7048
|
scored.sort((a, b) => b.score - a.score);
|
|
6975
7049
|
const chain = scored.slice(0, max_depth);
|
|
6976
|
-
if (chain.length === 0) {
|
|
7050
|
+
if (chain.length === 0 && ckgDeduped.length === 0) {
|
|
6977
7051
|
return [
|
|
6978
7052
|
`🔍 **Causal Trace: "${problem}"**`,
|
|
6979
7053
|
``,
|
|
@@ -6991,50 +7065,64 @@ async function handleBulkLockStream(name, args) {
|
|
|
6991
7065
|
`\`\`\``,
|
|
6992
7066
|
].join('\n');
|
|
6993
7067
|
}
|
|
6994
|
-
// Build causal chain narrative: failures first → then successes
|
|
6995
|
-
const failures = chain.filter(c => c.lesson.outcome !== 'success');
|
|
6996
|
-
const solutions = chain.filter(c => c.lesson.outcome === 'success');
|
|
6997
|
-
const SEV_ICON = { critical: '🔴', major: '🟠', minor: '🟡' };
|
|
6998
7068
|
const lines = [
|
|
6999
7069
|
`🔍 **Causal Trace: "${problem}"**`,
|
|
7000
7070
|
``,
|
|
7001
|
-
`Found **${chain.length}** related lessons. Reconstructed causal chain:`,
|
|
7002
|
-
``,
|
|
7003
7071
|
];
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
const
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
lines.push(`**Solutions that worked before:**`);
|
|
7017
|
-
solutions.forEach((c, i) => {
|
|
7018
|
-
const l = c.lesson;
|
|
7019
|
-
const date = new Date(l.ts).toLocaleDateString('de-DE');
|
|
7020
|
-
lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}× `);
|
|
7021
|
-
if (l.what_worked)
|
|
7022
|
-
lines.push(` ${l.what_worked.slice(0, 200)}`);
|
|
7023
|
-
});
|
|
7024
|
-
lines.push('');
|
|
7072
|
+
// Show CKG graph results first if available
|
|
7073
|
+
if (ckgDeduped.length > 0) {
|
|
7074
|
+
lines.push(`### 🕸️ CKG Graph (confidence-ranked)`);
|
|
7075
|
+
for (const r of ckgDeduped) {
|
|
7076
|
+
const confPct = Math.round(r.edge.confidence * 100);
|
|
7077
|
+
const confBar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
|
|
7078
|
+
lines.push(` ${r.edge.from} **→[${r.edge.edgeType}]→** ${r.edge.to}`);
|
|
7079
|
+
lines.push(` ${confBar} ${confPct}% confidence (${r.edge.successes}/${r.edge.trials} confirmed)`);
|
|
7080
|
+
if (r.lesson?.what_worked)
|
|
7081
|
+
lines.push(` ✅ Fix: ${r.lesson.what_worked.slice(0, 150)}`);
|
|
7082
|
+
lines.push('');
|
|
7083
|
+
}
|
|
7025
7084
|
}
|
|
7026
|
-
|
|
7027
|
-
if (
|
|
7028
|
-
lines.push(
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7085
|
+
// Build text-based causal chain narrative
|
|
7086
|
+
if (chain.length > 0) {
|
|
7087
|
+
lines.push(ckgDeduped.length > 0 ? `### 📚 Text Search (${chain.length} related lessons)` : `Found **${chain.length}** related lessons. Reconstructed causal chain:`, '');
|
|
7088
|
+
const failures = chain.filter(c => c.lesson.outcome !== 'success');
|
|
7089
|
+
const solutions = chain.filter(c => c.lesson.outcome === 'success');
|
|
7090
|
+
if (failures.length > 0) {
|
|
7091
|
+
lines.push(`**Root causes & failure chain:**`);
|
|
7092
|
+
failures.forEach((c, i) => {
|
|
7093
|
+
const l = c.lesson;
|
|
7094
|
+
const sev = SEV_ICON[l.severity ?? 'minor'] ?? '🟡';
|
|
7095
|
+
lines.push(`${i === 0 ? ' Root:' : ' → :'} ${sev} \`${l.topic}\``);
|
|
7096
|
+
if (l.what_failed)
|
|
7097
|
+
lines.push(` ↳ ${l.what_failed.slice(0, 120)}`);
|
|
7098
|
+
});
|
|
7099
|
+
lines.push('');
|
|
7100
|
+
}
|
|
7101
|
+
if (solutions.length > 0) {
|
|
7102
|
+
lines.push(`**Solutions that worked before:**`);
|
|
7103
|
+
solutions.forEach((c, i) => {
|
|
7104
|
+
const l = c.lesson;
|
|
7105
|
+
const date = new Date(l.ts).toLocaleDateString('de-DE');
|
|
7106
|
+
lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}×`);
|
|
7107
|
+
if (l.what_worked)
|
|
7108
|
+
lines.push(` ${l.what_worked.slice(0, 200)}`);
|
|
7109
|
+
});
|
|
7110
|
+
lines.push('');
|
|
7111
|
+
}
|
|
7112
|
+
const topSolution = solutions[0]?.lesson;
|
|
7113
|
+
if (topSolution?.what_worked) {
|
|
7114
|
+
lines.push(`**⚡ Most likely fix:**`);
|
|
7115
|
+
lines.push(`\`\`\``);
|
|
7116
|
+
lines.push(topSolution.what_worked.slice(0, 500));
|
|
7117
|
+
lines.push(`\`\`\``);
|
|
7118
|
+
lines.push('');
|
|
7119
|
+
}
|
|
7033
7120
|
}
|
|
7034
7121
|
lines.push(`💡 After applying: \`learn_from_attempts(topic="fix:${tokens[0] ?? 'issue'}", outcome="success", ...)\``);
|
|
7122
|
+
if (ckgDeduped.length > 0)
|
|
7123
|
+
lines.push(`🕸️ Explore graph: \`ckg_inspect(concept="${tokens[0] ?? 'fix'}")\``);
|
|
7035
7124
|
return lines.join('\n');
|
|
7036
7125
|
}
|
|
7037
|
-
// ── v0.6 Cognitive Cache: knowledge_decay ────────────────────────────────
|
|
7038
7126
|
case 'knowledge_decay': {
|
|
7039
7127
|
const { instance_id, min_age_days = 0, show_top = 20 } = args;
|
|
7040
7128
|
const redis = await getConnection(instance_id);
|
|
@@ -7114,11 +7202,9 @@ async function handleBulkLockStream(name, args) {
|
|
|
7114
7202
|
> Sessions are fully automatic. The Brain starts when you call any tool and saves a summary when you close.
|
|
7115
7203
|
|
|
7116
7204
|
### MANDATORY hooks (always fire, no exceptions):
|
|
7117
|
-
- **Context compacted** → call \`compact_recover(instance_id="${instance_id}", focus="<current task>")\` FIRST
|
|
7118
7205
|
- **Window opens** → call \`session_start(instance_id="${instance_id}", focus="<today's goal>")\`
|
|
7119
7206
|
- **Bug fixed / deployed / discovered** → call \`learn_from_attempts(instance_id="${instance_id}", topic="fix:<area>", outcome="success", what_worked="...")\`
|
|
7120
7207
|
- **Window closes** → call \`session_end(instance_id="${instance_id}", summary="...", files_changed=[...])\`
|
|
7121
|
-
- **Monthly** → call \`memory_crystalize(instance_id="${instance_id}")\` to distill knowledge into a permanent Crystal
|
|
7122
7208
|
`;
|
|
7123
7209
|
const full = `## Cachly AI Brain — Autopilot for ${project_name}
|
|
7124
7210
|
|
|
@@ -7178,40 +7264,18 @@ smart_recall(
|
|
|
7178
7264
|
|
|
7179
7265
|
---
|
|
7180
7266
|
|
|
7181
|
-
## ⚡
|
|
7182
|
-
|
|
7183
|
-
**If your context window was compacted**, call this FIRST — before reading any file:
|
|
7184
|
-
\`\`\`
|
|
7185
|
-
compact_recover(
|
|
7186
|
-
instance_id = "${instance_id}",
|
|
7187
|
-
focus = "<what you were working on>",
|
|
7188
|
-
)
|
|
7189
|
-
\`\`\`
|
|
7190
|
-
Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain scan.
|
|
7191
|
-
|
|
7192
|
-
---
|
|
7193
|
-
|
|
7194
|
-
## 🔁 Scheduled Maintenance (run on schedule)
|
|
7195
|
-
|
|
7196
|
-
| When | Call |
|
|
7197
|
-
|---|---|
|
|
7198
|
-
| **Monthly** | \`memory_crystalize(instance_id="${instance_id}")\` — distill brain into a permanent snapshot |
|
|
7199
|
-
| Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` — see what your AI learned this week |
|
|
7200
|
-
| Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` — see which lessons to still trust |
|
|
7201
|
-
| After major milestone | \`memory_crystalize(instance_id="${instance_id}", label="v2 launch")\` — preserve milestone knowledge |
|
|
7202
|
-
|
|
7203
|
-
## 🧠 Cognitive Tools (use anytime)
|
|
7267
|
+
## ⚡ Cognitive Tools (v0.6)
|
|
7204
7268
|
|
|
7205
7269
|
| When | Call |
|
|
7206
7270
|
|---|---|
|
|
7207
7271
|
| Brain feeling cluttered | \`memory_consolidate(instance_id="${instance_id}")\` |
|
|
7272
|
+
| Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` |
|
|
7208
7273
|
| Weird bug, no idea why | \`causal_trace(instance_id="${instance_id}", problem="<symptom>")\` |
|
|
7209
|
-
|
|
|
7210
|
-
| Unknown error that you solved | \`syndicate(instance_id="${instance_id}", topic="fix:<area>", what_worked="...")\` |
|
|
7274
|
+
| Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` |
|
|
7211
7275
|
|
|
7212
7276
|
---
|
|
7213
7277
|
|
|
7214
|
-
*Cachly v0.
|
|
7278
|
+
*Cachly v0.6 · Generated ${new Date().toISOString().slice(0, 10)}*
|
|
7215
7279
|
`;
|
|
7216
7280
|
const content = style === 'minimal' ? minimal : full;
|
|
7217
7281
|
const filename = editor === 'copilot'
|
|
@@ -7370,172 +7434,198 @@ Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain
|
|
|
7370
7434
|
lines.push(`---`, `**Confirm** (if this helped you): \`syndicate(topic="${res.results[0]?.topic ?? '...'}", what_worked="...")\` → auto-deduped, trust +1`, `**All trending:** \`syndicate_trending(limit=50)\` | **Search:** \`syndicate_search(q="...")\``);
|
|
7371
7435
|
return lines.filter(l => l !== '').join('\n');
|
|
7372
7436
|
}
|
|
7373
|
-
// ──
|
|
7374
|
-
case '
|
|
7375
|
-
const { instance_id,
|
|
7437
|
+
// ── Layer 1: brain_search ─────────────────────────────────────────────────
|
|
7438
|
+
case 'brain_search': {
|
|
7439
|
+
const { instance_id, query, limit = 15 } = args;
|
|
7376
7440
|
const redis = await getConnection(instance_id);
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7441
|
+
// BM25+ over ALL brain key namespaces
|
|
7442
|
+
const allMatches = await keywordSearch(redis, [
|
|
7443
|
+
'cachly:lesson:best:*',
|
|
7444
|
+
'cachly:ctx:*',
|
|
7445
|
+
'cachly:idx:*',
|
|
7446
|
+
'cachly:session:last',
|
|
7447
|
+
'cachly:session:handoff',
|
|
7448
|
+
'cachly:roadmap:*',
|
|
7449
|
+
'cachly:ckg:node:*',
|
|
7450
|
+
], query, limit);
|
|
7451
|
+
if (allMatches.length === 0) {
|
|
7452
|
+
return [`🔎 **Brain Search: "${query}"**`, '', `No results found across all brain data.`, '', `💡 Try \`smart_recall\` or check \`list_remembered\`.`].join('\n');
|
|
7453
|
+
}
|
|
7454
|
+
const lines = [`🔎 **Brain Search: "${query}"** — ${allMatches.length} result${allMatches.length !== 1 ? 's' : ''} across all brain data\n`];
|
|
7455
|
+
for (const m of allMatches.slice(0, limit)) {
|
|
7456
|
+
const ns = m.key.startsWith('cachly:lesson:') ? '💡 lesson'
|
|
7457
|
+
: m.key.startsWith('cachly:ctx:') ? '📝 context'
|
|
7458
|
+
: m.key.startsWith('cachly:idx:') ? '📂 index'
|
|
7459
|
+
: m.key.startsWith('cachly:session:') ? '🕐 session'
|
|
7460
|
+
: m.key.startsWith('cachly:roadmap:') ? '🗺️ roadmap'
|
|
7461
|
+
: m.key.startsWith('cachly:ckg:node:') ? '🕸️ ckg-node'
|
|
7462
|
+
: '🗄️ data';
|
|
7463
|
+
const preview = m.content.slice(0, 280).replace(/\n/g, ' ');
|
|
7464
|
+
lines.push(`**${ns}** \`${m.key.split(':').slice(2).join(':')}\` _(BM25: ${m.score.toFixed(2)})_`);
|
|
7465
|
+
lines.push(`> ${preview}${m.content.length > 280 ? '…' : ''}\n`);
|
|
7466
|
+
}
|
|
7467
|
+
return lines.join('\n');
|
|
7468
|
+
}
|
|
7469
|
+
// ── Layer 1: ckg_inspect ─────────────────────────────────────────────────
|
|
7470
|
+
case 'ckg_inspect': {
|
|
7471
|
+
const { instance_id, concept, max_hops = 2 } = args;
|
|
7472
|
+
const redis = await getConnection(instance_id);
|
|
7473
|
+
const conceptId = ckgSlug(concept);
|
|
7474
|
+
const visited = new Set();
|
|
7475
|
+
const allEdges = [];
|
|
7476
|
+
// BFS traversal of CKG
|
|
7477
|
+
const queue = [{ id: conceptId, hop: 0 }];
|
|
7478
|
+
while (queue.length > 0) {
|
|
7479
|
+
const { id, hop } = queue.shift();
|
|
7480
|
+
if (visited.has(id) || hop > max_hops)
|
|
7481
|
+
continue;
|
|
7482
|
+
visited.add(id);
|
|
7483
|
+
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${id}`);
|
|
7484
|
+
const toKeys = await redis.smembers(`cachly:ckg:idx:to:${id}`);
|
|
7485
|
+
for (const ek of [...fromKeys, ...toKeys].slice(0, 50)) {
|
|
7486
|
+
const raw = await redis.get(ek);
|
|
7487
|
+
if (!raw)
|
|
7488
|
+
continue;
|
|
7489
|
+
const edge = JSON.parse(raw);
|
|
7490
|
+
allEdges.push(edge);
|
|
7491
|
+
if (hop < max_hops) {
|
|
7492
|
+
if (!visited.has(edge.from))
|
|
7493
|
+
queue.push({ id: edge.from, hop: hop + 1 });
|
|
7494
|
+
if (!visited.has(edge.to))
|
|
7495
|
+
queue.push({ id: edge.to, hop: hop + 1 });
|
|
7388
7496
|
}
|
|
7389
|
-
if (cp.next_step)
|
|
7390
|
-
lines.push(`📍 **Resume with:** ${cp.next_step}`);
|
|
7391
|
-
lines.push('');
|
|
7392
7497
|
}
|
|
7393
|
-
catch { /* ignore corrupt */ }
|
|
7394
7498
|
}
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7499
|
+
if (allEdges.length === 0) {
|
|
7500
|
+
// Try fuzzy: scan for nodes matching the concept as substring
|
|
7501
|
+
const nodeKeys = [];
|
|
7502
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${conceptId}*`, count: 100 });
|
|
7503
|
+
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
7504
|
+
if (nodeKeys.length === 0) {
|
|
7505
|
+
return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No CKG nodes found. The graph builds automatically as you call \`learn_from_attempts\`.`, '', `💡 Once you have lessons stored, CKG edges will appear here.`].join('\n');
|
|
7506
|
+
}
|
|
7507
|
+
const nodeList = nodeKeys.slice(0, 10).map(k => ` • \`${k.replace('cachly:ckg:node:', '')}\``).join('\n');
|
|
7508
|
+
return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No edges found for \`${conceptId}\`, but found similar nodes:`, nodeList, '', `Try: \`ckg_inspect(concept="<exact-node-id>")\``].join('\n');
|
|
7509
|
+
}
|
|
7510
|
+
// Sort by confidence desc, deduplicate
|
|
7511
|
+
const edgeSeen = new Set();
|
|
7512
|
+
const unique = allEdges.filter(e => {
|
|
7513
|
+
const k = `${e.from}:${e.edgeType}:${e.to}`;
|
|
7514
|
+
if (edgeSeen.has(k))
|
|
7515
|
+
return false;
|
|
7516
|
+
edgeSeen.add(k);
|
|
7517
|
+
return true;
|
|
7518
|
+
}).sort((a, b) => b.confidence - a.confidence);
|
|
7519
|
+
const EDGE_ICON = { fixes: '🔧', requires: '🔗', 'co-occurs': '🔄', causes: '⚡', contradicts: '⚠️', degrades_under: '📉' };
|
|
7520
|
+
const lines = [`🕸️ **CKG Inspect: "${concept}"** (${unique.length} edge${unique.length !== 1 ? 's' : ''}, ${visited.size} node${visited.size !== 1 ? 's' : ''} traversed)\n`];
|
|
7521
|
+
// Group by edge type
|
|
7522
|
+
const byType = new Map();
|
|
7523
|
+
for (const e of unique) {
|
|
7524
|
+
if (!byType.has(e.edgeType))
|
|
7525
|
+
byType.set(e.edgeType, []);
|
|
7526
|
+
byType.get(e.edgeType).push(e);
|
|
7527
|
+
}
|
|
7528
|
+
for (const [eType, edges] of byType) {
|
|
7529
|
+
const icon = EDGE_ICON[eType] ?? '→';
|
|
7530
|
+
lines.push(`**${icon} ${eType}** (${edges.length})`);
|
|
7531
|
+
for (const e of edges.slice(0, 8)) {
|
|
7532
|
+
const confPct = Math.round(e.confidence * 100);
|
|
7533
|
+
const bar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
|
|
7534
|
+
lines.push(` \`${e.from}\` → \`${e.to}\` ${bar} ${confPct}% (${e.successes.toFixed(1)}/${e.trials} trials)`);
|
|
7535
|
+
}
|
|
7536
|
+
lines.push('');
|
|
7537
|
+
}
|
|
7538
|
+
lines.push(`💡 Expand: \`ckg_inspect(concept="${concept}", max_hops=3)\` | Predict: \`brain_predict(context="${concept}")\``);
|
|
7539
|
+
return lines.join('\n');
|
|
7540
|
+
}
|
|
7541
|
+
// ── Layer 4: brain_predict (PPE) ─────────────────────────────────────────
|
|
7542
|
+
case 'brain_predict': {
|
|
7543
|
+
const { instance_id, context: ctx, top_k = 5 } = args;
|
|
7544
|
+
const redis = await getConnection(instance_id);
|
|
7545
|
+
const ctxTokens = ctx.toLowerCase().replace(/[^a-z0-9\s\-_:]/g, ' ').split(/\s+/).filter(t => t.length > 2);
|
|
7546
|
+
const predictions = [];
|
|
7547
|
+
for (const token of ctxTokens.slice(0, 6)) {
|
|
7548
|
+
const nodeKeys = [];
|
|
7549
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
|
|
7550
|
+
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
7551
|
+
for (const nk of nodeKeys.slice(0, 5)) {
|
|
7552
|
+
const nodeRaw = await redis.get(nk);
|
|
7553
|
+
if (!nodeRaw)
|
|
7554
|
+
continue;
|
|
7555
|
+
const node = JSON.parse(nodeRaw);
|
|
7556
|
+
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
7557
|
+
for (const ek of edgeKeys.slice(0, 20)) {
|
|
7558
|
+
const edgeRaw = await redis.get(ek);
|
|
7559
|
+
if (!edgeRaw)
|
|
7560
|
+
continue;
|
|
7561
|
+
const edge = JSON.parse(edgeRaw);
|
|
7562
|
+
// Only interested in fixes and co-occurs for prediction
|
|
7563
|
+
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes')
|
|
7564
|
+
continue;
|
|
7565
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
7566
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
7567
|
+
predictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
|
|
7410
7568
|
}
|
|
7411
7569
|
}
|
|
7412
|
-
catch { /* ignore */ }
|
|
7413
7570
|
}
|
|
7414
|
-
//
|
|
7571
|
+
// Step 2: Text-based fallback — scan lessons for matching topics
|
|
7572
|
+
const textPredictions = [];
|
|
7415
7573
|
const lessonKeys = [];
|
|
7416
7574
|
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
7417
|
-
await new Promise((
|
|
7418
|
-
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
7419
|
-
lStream.on('end', resolve);
|
|
7420
|
-
lStream.on('error', reject);
|
|
7421
|
-
});
|
|
7422
|
-
const allLessons = [];
|
|
7575
|
+
await new Promise((res, rej) => { lStream.on('data', (b) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
|
|
7423
7576
|
for (const k of lessonKeys) {
|
|
7424
7577
|
const raw = await redis.get(k);
|
|
7425
7578
|
if (!raw)
|
|
7426
7579
|
continue;
|
|
7427
7580
|
try {
|
|
7428
|
-
|
|
7581
|
+
const l = JSON.parse(raw);
|
|
7582
|
+
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? ''].join(' ').toLowerCase();
|
|
7583
|
+
const score = ctxTokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
|
|
7584
|
+
if (score >= 1 && l.outcome !== 'failure') {
|
|
7585
|
+
const conf = calculateConfidence(l);
|
|
7586
|
+
textPredictions.push({ ...l, confidence: conf });
|
|
7587
|
+
}
|
|
7429
7588
|
}
|
|
7430
7589
|
catch { /* skip */ }
|
|
7431
7590
|
}
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7591
|
+
textPredictions.sort((a, b) => b.confidence - a.confidence);
|
|
7592
|
+
if (predictions.length === 0 && textPredictions.length === 0) {
|
|
7593
|
+
return [
|
|
7594
|
+
`🔮 **Brain Predict: "${ctx}"**`,
|
|
7595
|
+
``,
|
|
7596
|
+
`No predictions yet — the brain hasn't seen this domain.`,
|
|
7597
|
+
``,
|
|
7598
|
+
`💡 As you solve problems in this area and call \`learn_from_attempts\`, the CKG builds up and predictions become available.`,
|
|
7599
|
+
].join('\n');
|
|
7600
|
+
}
|
|
7601
|
+
const lines = [`🔮 **Brain Predict: "${ctx}"**\n`];
|
|
7602
|
+
// CKG-based predictions
|
|
7603
|
+
if (predictions.length > 0) {
|
|
7604
|
+
const pSeen = new Set();
|
|
7605
|
+
const pUniq = predictions.filter(p => { const k = `${p.concept}:${p.edgeType}:${p.target}`; if (pSeen.has(k))
|
|
7606
|
+
return false; pSeen.add(k); return true; })
|
|
7607
|
+
.sort((a, b) => b.confidence - a.confidence).slice(0, top_k);
|
|
7608
|
+
lines.push(`### 🕸️ CKG Predictions (based on ${pUniq.length} known edges)`);
|
|
7609
|
+
for (const p of pUniq) {
|
|
7610
|
+
const confPct = Math.round(p.confidence * 100);
|
|
7611
|
+
const icon = p.edgeType === 'fixes' ? '🔧' : p.edgeType === 'co-occurs' ? '🔄' : '⚡';
|
|
7612
|
+
lines.push(`${icon} **${confPct}%** \`${p.concept}\` _${p.edgeType}_ \`${p.target}\``);
|
|
7613
|
+
if (p.lesson?.what_worked)
|
|
7614
|
+
lines.push(` ✅ ${p.lesson.what_worked.slice(0, 120)}`);
|
|
7441
7615
|
}
|
|
7442
7616
|
lines.push('');
|
|
7443
7617
|
}
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
const redis = await getConnection(instance_id);
|
|
7451
|
-
const { execSync } = await import('node:child_process');
|
|
7452
|
-
const clampedDays = Math.min(Math.max(1, days), 365);
|
|
7453
|
-
const clampedMax = Math.min(Math.max(1, max_commits), 500);
|
|
7454
|
-
const authorArg = author ? `--author="${author}"` : '';
|
|
7455
|
-
let gitRaw = '';
|
|
7456
|
-
try {
|
|
7457
|
-
gitRaw = execSync(`git -C "${workspace_path}" log --since="${clampedDays} days ago" ${authorArg} --oneline --format="%H|||%s|||%ai|||%ae" -n ${clampedMax}`, { timeout: 15000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
7458
|
-
}
|
|
7459
|
-
catch {
|
|
7460
|
-
return `❌ **brain_from_git failed**: Could not read git log from \`${workspace_path}\`.\nMake sure the path exists and is a git repository.`;
|
|
7461
|
-
}
|
|
7462
|
-
if (!gitRaw) {
|
|
7463
|
-
return `📭 **No commits found** in the last ${clampedDays} days${author ? ` by "${author}"` : ''}.\nTry increasing \`days\` or removing the \`author\` filter.`;
|
|
7464
|
-
}
|
|
7465
|
-
const commitLines = gitRaw.split('\n');
|
|
7466
|
-
const now = new Date();
|
|
7467
|
-
const actionRe = /\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|rewrite|deploy|feat|revert|break|merge|bump)\b/i;
|
|
7468
|
-
const failRe = /\b(revert|broken|broke|breaking|rollback|undo|hotfix|crash|oops|wrong|mistake|bad)\b/i;
|
|
7469
|
-
let stored = 0;
|
|
7470
|
-
let skipped = 0;
|
|
7471
|
-
let failures = 0;
|
|
7472
|
-
const topLessons = [];
|
|
7473
|
-
for (const line of commitLines) {
|
|
7474
|
-
const parts = line.split('|||');
|
|
7475
|
-
if (parts.length < 2)
|
|
7476
|
-
continue;
|
|
7477
|
-
const [hash, msg, dateStr] = parts;
|
|
7478
|
-
if (!msg || !actionRe.test(msg))
|
|
7479
|
-
continue;
|
|
7480
|
-
const isFail = failRe.test(msg);
|
|
7481
|
-
const cleanMsg = msg
|
|
7482
|
-
.replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert|hotfix|bump)[:(\s!]+/i, '')
|
|
7483
|
-
.trim();
|
|
7484
|
-
if (!cleanMsg || cleanMsg.length < 5)
|
|
7485
|
-
continue;
|
|
7486
|
-
const slug = cleanMsg
|
|
7487
|
-
.toLowerCase()
|
|
7488
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
7489
|
-
.replace(/^-+|-+$/g, '')
|
|
7490
|
-
.slice(0, 40);
|
|
7491
|
-
if (!slug)
|
|
7492
|
-
continue;
|
|
7493
|
-
const topic = `git:${slug}`;
|
|
7494
|
-
const key = `cachly:lesson:best:${topic}`;
|
|
7495
|
-
const existing = await redis.get(key);
|
|
7496
|
-
if (existing) {
|
|
7497
|
-
skipped++;
|
|
7498
|
-
continue;
|
|
7618
|
+
// Text-based lesson predictions
|
|
7619
|
+
if (textPredictions.length > 0) {
|
|
7620
|
+
lines.push(`### 📚 Relevant Lessons (${Math.min(textPredictions.length, top_k)} pre-loaded)`);
|
|
7621
|
+
for (const l of textPredictions.slice(0, top_k)) {
|
|
7622
|
+
const confPct = Math.round(l.confidence * 100);
|
|
7623
|
+
lines.push(` ✅ **${confPct}%** \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 120)}`);
|
|
7499
7624
|
}
|
|
7500
|
-
const lesson = {
|
|
7501
|
-
topic,
|
|
7502
|
-
outcome: isFail ? 'failure' : 'success',
|
|
7503
|
-
what_worked: isFail ? '' : cleanMsg.slice(0, 300),
|
|
7504
|
-
what_failed: isFail ? cleanMsg.slice(0, 300) : undefined,
|
|
7505
|
-
context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)}${dateStr ? ` at ${dateStr}` : ''} in ${workspace_path}${author ? ` (author: ${author})` : ''}`,
|
|
7506
|
-
severity: 'minor',
|
|
7507
|
-
ts: dateStr ? new Date(dateStr).toISOString() : now.toISOString(),
|
|
7508
|
-
recall_count: 0,
|
|
7509
|
-
auto_learned: true,
|
|
7510
|
-
source: 'brain_from_git',
|
|
7511
|
-
version: 3,
|
|
7512
|
-
};
|
|
7513
|
-
await redis.set(key, JSON.stringify(lesson));
|
|
7514
|
-
await redis.expire(key, 90 * 86400); // 90-day TTL for git-sourced lessons
|
|
7515
|
-
stored++;
|
|
7516
|
-
if (isFail)
|
|
7517
|
-
failures++;
|
|
7518
|
-
if (topLessons.length < 5)
|
|
7519
|
-
topLessons.push(` ${isFail ? '❌' : '✅'} \`${topic}\` — ${cleanMsg.slice(0, 80)}`);
|
|
7520
|
-
}
|
|
7521
|
-
const lines = [
|
|
7522
|
-
`🧠 **Brain bootstrapped from git history**`,
|
|
7523
|
-
``,
|
|
7524
|
-
`📊 **Scanned:** ${commitLines.filter(l => l.trim()).length} commits over last ${clampedDays} days`,
|
|
7525
|
-
`✅ **Success lessons stored:** ${stored - failures}`,
|
|
7526
|
-
`❌ **Failure/revert lessons:** ${failures}`,
|
|
7527
|
-
skipped > 0 ? `⏭️ **Already in brain (skipped):** ${skipped}` : '',
|
|
7528
|
-
``,
|
|
7529
|
-
];
|
|
7530
|
-
if (topLessons.length > 0) {
|
|
7531
|
-
lines.push(`**Top lessons extracted:**`);
|
|
7532
|
-
lines.push(...topLessons);
|
|
7533
|
-
if (stored > 5)
|
|
7534
|
-
lines.push(` …and ${stored - 5} more`);
|
|
7535
7625
|
lines.push('');
|
|
7536
7626
|
}
|
|
7537
|
-
lines.push(`💡
|
|
7538
|
-
return lines.
|
|
7627
|
+
lines.push(`💡 Outcome confirmed? \`learn_from_attempts(topic="fix:...", outcome="success", ...)\` → improves future predictions`);
|
|
7628
|
+
return lines.join('\n');
|
|
7539
7629
|
}
|
|
7540
7630
|
default:
|
|
7541
7631
|
return null;
|