@cachly-dev/mcp-server 0.9.12 → 0.9.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +310 -103
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +186 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cachly-dev/mcp-server",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
4
4
|
"mcpName": "io.github.cachly-dev/cachly-mcp",
|
|
5
5
|
"description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 63 MCP tools · Claude Code · Cursor · GitHub Copilot · Windsurf · Free tier · EU servers.",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -5243,6 +5243,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
5243
5243
|
} catch { /* non-critical */ }
|
|
5244
5244
|
|
|
5245
5245
|
// ── Layer 1+2: CKG update (Causal Knowledge Graph + Belief Update Engine) ──
|
|
5246
|
+
// BUE: Bayesian confidence, contradiction detection, second-degree propagation, decay
|
|
5247
|
+
let beliefConflict: string | null = null;
|
|
5246
5248
|
try {
|
|
5247
5249
|
const conceptId = ckgSlug(topic);
|
|
5248
5250
|
const domain = topic.split(':')[0] ?? 'unknown';
|
|
@@ -5285,6 +5287,68 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
5285
5287
|
await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
|
|
5286
5288
|
}
|
|
5287
5289
|
}
|
|
5290
|
+
|
|
5291
|
+
// ── BUE: Contradiction detection ──────────────────────────────────────
|
|
5292
|
+
// If this topic previously had a confirmed 'fixes' edge (confidence > 0.7)
|
|
5293
|
+
// and now outcome=failure → flag belief_conflict
|
|
5294
|
+
if (outcome === 'failure') {
|
|
5295
|
+
const existingEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
|
|
5296
|
+
for (const ek of existingEdgeKeys) {
|
|
5297
|
+
const er = await redis.get(ek);
|
|
5298
|
+
if (!er) continue;
|
|
5299
|
+
const existEdge: CKGEdge = JSON.parse(er);
|
|
5300
|
+
if (existEdge.edgeType === 'fixes' && existEdge.confidence > 0.7 && existEdge.trials >= 3) {
|
|
5301
|
+
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.`;
|
|
5302
|
+
// Store conflict marker
|
|
5303
|
+
const conflictKey = `cachly:ckg:conflict:${conceptId}`;
|
|
5304
|
+
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);
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
// ── BUE: Second-degree propagation ────────────────────────────────────
|
|
5310
|
+
// When a 'fixes' edge gets stronger, boost co-occurring second-degree edges slightly
|
|
5311
|
+
if (outcome === 'success' && (domain === 'fix' || domain === 'debug')) {
|
|
5312
|
+
const fromEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
|
|
5313
|
+
for (const ek of fromEdgeKeys.slice(0, 10)) {
|
|
5314
|
+
const er = await redis.get(ek);
|
|
5315
|
+
if (!er) continue;
|
|
5316
|
+
const e2: CKGEdge = JSON.parse(er);
|
|
5317
|
+
if (e2.edgeType !== 'fixes') continue;
|
|
5318
|
+
// Boost second-degree: edges from e2.to get a small fractional success
|
|
5319
|
+
const secondKeys = await redis.smembers(`cachly:ckg:idx:from:${e2.to}`);
|
|
5320
|
+
for (const sk of secondKeys.slice(0, 5)) {
|
|
5321
|
+
const sr = await redis.get(sk);
|
|
5322
|
+
if (!sr) continue;
|
|
5323
|
+
const se: CKGEdge = JSON.parse(sr);
|
|
5324
|
+
if (se.edgeType !== 'co-occurs') continue;
|
|
5325
|
+
// Add 0.1 fractional success (second-degree signal)
|
|
5326
|
+
se.successes = (se.successes || 0) + 0.1;
|
|
5327
|
+
se.trials = (se.trials || 0) + 0.1;
|
|
5328
|
+
se.confidence = (se.successes + 1) / (se.trials + 2);
|
|
5329
|
+
se.last_updated = new Date().toISOString();
|
|
5330
|
+
await redis.set(sk, JSON.stringify(se));
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
|
|
5335
|
+
// ── BUE: Stale edge decay ─────────────────────────────────────────────
|
|
5336
|
+
// Edges older than 90 days with < 3 trials decay by 10% confidence.
|
|
5337
|
+
// Only run probabilistically (1% of calls) to avoid per-call overhead.
|
|
5338
|
+
if (Math.random() < 0.01) {
|
|
5339
|
+
const decayCutoff = Date.now() - 90 * 24 * 60 * 60 * 1000;
|
|
5340
|
+
const fromEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
|
|
5341
|
+
for (const ek of fromEdgeKeys) {
|
|
5342
|
+
const er = await redis.get(ek);
|
|
5343
|
+
if (!er) continue;
|
|
5344
|
+
const de: CKGEdge = JSON.parse(er);
|
|
5345
|
+
if (de.trials < 3 && de.last_updated && new Date(de.last_updated).getTime() < decayCutoff) {
|
|
5346
|
+
de.confidence = de.confidence * 0.9;
|
|
5347
|
+
de.last_updated = new Date().toISOString();
|
|
5348
|
+
await redis.set(ek, JSON.stringify(de));
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5288
5352
|
} catch { /* CKG updates are non-critical */ }
|
|
5289
5353
|
|
|
5290
5354
|
const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
|
|
@@ -5292,6 +5356,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
5292
5356
|
const action = isUpdate ? 'updated' : 'stored';
|
|
5293
5357
|
return [
|
|
5294
5358
|
`${emoji} **Lesson ${action}:** \`${topic}\` (${outcome}) ${sevEmoji} ${severity}`,
|
|
5359
|
+
beliefConflict ?? '',
|
|
5295
5360
|
``,
|
|
5296
5361
|
`**What worked:** ${what_worked}`,
|
|
5297
5362
|
what_failed ? `**What failed:** ${what_failed}` : '',
|
|
@@ -5836,6 +5901,22 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
5836
5901
|
}
|
|
5837
5902
|
}
|
|
5838
5903
|
|
|
5904
|
+
// ── Layer 7 MCM: Active belief conflicts ─────────────────────────────────
|
|
5905
|
+
try {
|
|
5906
|
+
const conflictKeys: string[] = [];
|
|
5907
|
+
const cfStream = redis.scanStream({ match: 'cachly:ckg:conflict:*', count: 50 });
|
|
5908
|
+
await new Promise<void>((res, rej) => { cfStream.on('data', (b: string[]) => conflictKeys.push(...b)); cfStream.on('end', res); cfStream.on('error', rej); });
|
|
5909
|
+
if (conflictKeys.length > 0) {
|
|
5910
|
+
lines.push(`⚡ **Active belief conflicts (${conflictKeys.length}):**`);
|
|
5911
|
+
for (const ck of conflictKeys.slice(0, 3)) {
|
|
5912
|
+
const cr = await redis.get(ck);
|
|
5913
|
+
if (!cr) continue;
|
|
5914
|
+
const cf = JSON.parse(cr) as { topic: string; fix_confidence: number; fix_trials: number };
|
|
5915
|
+
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)}")\``);
|
|
5916
|
+
}
|
|
5917
|
+
lines.push('');
|
|
5918
|
+
}
|
|
5919
|
+
} catch { /* non-critical */ }
|
|
5839
5920
|
|
|
5840
5921
|
if (focusLessons.length > 0) {
|
|
5841
5922
|
lines.push(`🎯 **Relevant for "${focus}":**`);
|
|
@@ -9517,6 +9598,9 @@ smart_recall(
|
|
|
9517
9598
|
const repoDir = resolve(repo_path);
|
|
9518
9599
|
const maxCommits = Math.min(Number(limit) || 100, 500);
|
|
9519
9600
|
|
|
9601
|
+
// Semaphore: max 10 concurrent git subprocesses per MCP process
|
|
9602
|
+
await _gitSemAcquire();
|
|
9603
|
+
try {
|
|
9520
9604
|
// Verify it's a git repo
|
|
9521
9605
|
try {
|
|
9522
9606
|
execSync('git rev-parse --git-dir', { cwd: repoDir, stdio: 'pipe' });
|
|
@@ -9620,6 +9704,9 @@ smart_recall(
|
|
|
9620
9704
|
`💡 As you confirm them via \`learn_from_attempts\`, confidence rises automatically.`,
|
|
9621
9705
|
`🔍 Explore: \`brain_search(query="fix")\` | \`ckg_inspect(concept="deploy")\``,
|
|
9622
9706
|
].join('\n');
|
|
9707
|
+
} finally {
|
|
9708
|
+
_gitSemRelease();
|
|
9709
|
+
}
|
|
9623
9710
|
}
|
|
9624
9711
|
|
|
9625
9712
|
// ── brain_predict_failures ─────────────────────────────────────────────────
|
|
@@ -9774,15 +9861,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
|
9774
9861
|
|
|
9775
9862
|
let _autoSessionInstanceId: string | null = null;
|
|
9776
9863
|
let _autoSessionStarted = false;
|
|
9864
|
+
let _autoSessionStarting: Promise<void> | null = null; // guard against double-start race
|
|
9777
9865
|
let _autoSessionToolCount = 0;
|
|
9778
9866
|
|
|
9867
|
+
// ── brain_from_git concurrency limiter ────────────────────────────────────────
|
|
9868
|
+
// Spawning many `git log` subprocesses in parallel can fork-bomb the host.
|
|
9869
|
+
// This simple semaphore caps concurrent brain_from_git calls to 10 per process.
|
|
9870
|
+
let _gitSemCount = 0;
|
|
9871
|
+
const _GIT_SEM_MAX = 10;
|
|
9872
|
+
const _gitSemQueue: Array<() => void> = [];
|
|
9873
|
+
function _gitSemAcquire(): Promise<void> {
|
|
9874
|
+
if (_gitSemCount < _GIT_SEM_MAX) { _gitSemCount++; return Promise.resolve(); }
|
|
9875
|
+
return new Promise(resolve => _gitSemQueue.push(resolve));
|
|
9876
|
+
}
|
|
9877
|
+
function _gitSemRelease(): void {
|
|
9878
|
+
const next = _gitSemQueue.shift();
|
|
9879
|
+
if (next) { next(); } else { _gitSemCount--; }
|
|
9880
|
+
}
|
|
9881
|
+
|
|
9779
9882
|
async function autoStartSession(instanceId: string): Promise<void> {
|
|
9780
9883
|
if (_autoSessionStarted) return;
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9884
|
+
// If another call already started the session, await it instead of double-starting.
|
|
9885
|
+
if (_autoSessionStarting) { await _autoSessionStarting; return; }
|
|
9886
|
+
_autoSessionStarting = (async () => {
|
|
9887
|
+
_autoSessionStarted = true;
|
|
9888
|
+
_autoSessionInstanceId = instanceId;
|
|
9889
|
+
try {
|
|
9890
|
+
await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)' });
|
|
9891
|
+
} catch { /* non-fatal — session tracking is a best-effort feature */ }
|
|
9892
|
+
_autoSessionStarting = null;
|
|
9893
|
+
})();
|
|
9894
|
+
await _autoSessionStarting;
|
|
9786
9895
|
|
|
9787
9896
|
// Auto-index the project if it hasn't been indexed in the last 24h.
|
|
9788
9897
|
// This is the main lever for growing token savings: more indexed code →
|
|
@@ -9828,11 +9937,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
9828
9937
|
// Auto-start brain session on first tool call that has an instance_id.
|
|
9829
9938
|
// Skip session management tools to avoid recursion.
|
|
9830
9939
|
const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
|
|
9831
|
-
if (!sessionTools.has(name) && !_autoSessionStarted) {
|
|
9940
|
+
if (!sessionTools.has(name) && !_autoSessionStarted && !_autoSessionStarting) {
|
|
9832
9941
|
const instanceId = (args as Record<string, unknown>)?.instance_id as string | undefined;
|
|
9833
9942
|
if (instanceId) {
|
|
9834
9943
|
await autoStartSession(instanceId).catch(() => undefined);
|
|
9835
9944
|
}
|
|
9945
|
+
} else if (_autoSessionStarting) {
|
|
9946
|
+
// Another concurrent call is mid-start — wait for it before proceeding.
|
|
9947
|
+
await _autoSessionStarting.catch(() => undefined);
|
|
9836
9948
|
}
|
|
9837
9949
|
if (!sessionTools.has(name)) _autoSessionToolCount++;
|
|
9838
9950
|
|
|
@@ -10029,6 +10141,41 @@ if (process.argv[2] === 'init') {
|
|
|
10029
10141
|
const action = result === 'updated' ? '✅ Updated' : result === 'appended' ? '✅ Appended to' : '✅ Written';
|
|
10030
10142
|
console.log(`${action}: CLAUDE.md`);
|
|
10031
10143
|
|
|
10144
|
+
// ── CLS Phase 4: Auto-install git post-commit hook ─────────────────────────
|
|
10145
|
+
try {
|
|
10146
|
+
const { existsSync } = await import('node:fs');
|
|
10147
|
+
const gitHookDir = resolve(projectDir, '.git', 'hooks');
|
|
10148
|
+
const hookPath = resolve(gitHookDir, 'post-commit');
|
|
10149
|
+
if (existsSync(resolve(projectDir, '.git'))) {
|
|
10150
|
+
await mkdir(gitHookDir, { recursive: true });
|
|
10151
|
+
const hookScript = [
|
|
10152
|
+
`#!/bin/sh`,
|
|
10153
|
+
`# cachly CLS — Continuous Learning Stream (installed by cachly init)`,
|
|
10154
|
+
`# Runs silently on every commit to keep your brain up to date.`,
|
|
10155
|
+
`CACHLY_INSTANCE="${instanceId}"`,
|
|
10156
|
+
`SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
|
|
10157
|
+
`MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | tr '"' "'" | cut -c1-200)`,
|
|
10158
|
+
`FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
|
|
10159
|
+
`node -e "try{require('child_process').execSync('npx @cachly-dev/mcp-server@latest cls-ingest \\''+ JSON.stringify({instance_id:'$CACHLY_INSTANCE',source:'git_commit',payload:{message:'$MSG',sha:'$SHA',files:'$FILES'.split(',').filter(Boolean)}})+'\\'' ,{stdio:'ignore',timeout:5000})}catch(e){}" 2>/dev/null &`,
|
|
10160
|
+
`exit 0`,
|
|
10161
|
+
].join('\n');
|
|
10162
|
+
let existing = '';
|
|
10163
|
+
try { const { readFile } = await import('node:fs/promises'); existing = await readFile(hookPath, 'utf-8'); } catch { /* no existing hook */ }
|
|
10164
|
+
if (existing && !existing.includes('cachly CLS')) {
|
|
10165
|
+
// Append to existing hook
|
|
10166
|
+
await writeFile(hookPath, existing.trimEnd() + '\n\n' + hookScript + '\n', 'utf-8');
|
|
10167
|
+
console.log(`✅ Appended: .git/hooks/post-commit (CLS hook)`);
|
|
10168
|
+
} else if (!existing) {
|
|
10169
|
+
await writeFile(hookPath, hookScript + '\n', 'utf-8');
|
|
10170
|
+
const { chmod } = await import('node:fs/promises');
|
|
10171
|
+
await chmod(hookPath, 0o755);
|
|
10172
|
+
console.log(`✅ Written: .git/hooks/post-commit (CLS hook)`);
|
|
10173
|
+
} else {
|
|
10174
|
+
console.log(`✓ CLS hook already present in .git/hooks/post-commit`);
|
|
10175
|
+
}
|
|
10176
|
+
}
|
|
10177
|
+
} catch { /* non-critical — git hook is a best-effort feature */ }
|
|
10178
|
+
|
|
10032
10179
|
console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}!`);
|
|
10033
10180
|
console.log(` Restart your editor — the \`cachly\` MCP tools will appear.\n`);
|
|
10034
10181
|
process.exit(0);
|
|
@@ -10341,6 +10488,39 @@ if (process.argv[2] === 'setup') {
|
|
|
10341
10488
|
}
|
|
10342
10489
|
}
|
|
10343
10490
|
|
|
10491
|
+
// ── CLS Phase 4: Auto-install git post-commit hook in setup flow ─────────
|
|
10492
|
+
try {
|
|
10493
|
+
const { existsSync } = await import('node:fs');
|
|
10494
|
+
const { writeFile: wf2, mkdir: mk2, chmod: ch2, readFile: rf2 } = await import('node:fs/promises');
|
|
10495
|
+
const { resolve: res2 } = await import('node:path');
|
|
10496
|
+
const setupProjectDir = process.cwd();
|
|
10497
|
+
const gitHookDir2 = res2(setupProjectDir, '.git', 'hooks');
|
|
10498
|
+
const hookPath2 = res2(gitHookDir2, 'post-commit');
|
|
10499
|
+
if (existsSync(res2(setupProjectDir, '.git'))) {
|
|
10500
|
+
await mk2(gitHookDir2, { recursive: true });
|
|
10501
|
+
const hs2 = [
|
|
10502
|
+
`#!/bin/sh`,
|
|
10503
|
+
`# cachly CLS — Continuous Learning Stream (installed by cachly setup)`,
|
|
10504
|
+
`CACHLY_INSTANCE="${instance.id}"`,
|
|
10505
|
+
`SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
|
|
10506
|
+
`MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | tr '"' "'" | cut -c1-200)`,
|
|
10507
|
+
`FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
|
|
10508
|
+
`node -e "try{require('child_process').execSync('npx @cachly-dev/mcp-server@latest cls-ingest \\''+ JSON.stringify({instance_id:'$CACHLY_INSTANCE',source:'git_commit',payload:{message:'$MSG',sha:'$SHA',files:'$FILES'.split(',').filter(Boolean)}})+'\\'' ,{stdio:'ignore',timeout:5000})}catch(e){}" 2>/dev/null &`,
|
|
10509
|
+
`exit 0`,
|
|
10510
|
+
].join('\n');
|
|
10511
|
+
let ex2 = '';
|
|
10512
|
+
try { ex2 = await rf2(hookPath2, 'utf-8'); } catch { /* no existing */ }
|
|
10513
|
+
if (!ex2) {
|
|
10514
|
+
await wf2(hookPath2, hs2 + '\n', 'utf-8');
|
|
10515
|
+
await ch2(hookPath2, 0o755);
|
|
10516
|
+
console.log(`\n✅ CLS git hook installed — your brain will learn from every commit.`);
|
|
10517
|
+
} else if (!ex2.includes('cachly CLS')) {
|
|
10518
|
+
await wf2(hookPath2, ex2.trimEnd() + '\n\n' + hs2 + '\n', 'utf-8');
|
|
10519
|
+
console.log(`\n✅ CLS git hook appended to existing post-commit.`);
|
|
10520
|
+
}
|
|
10521
|
+
}
|
|
10522
|
+
} catch { /* non-critical */ }
|
|
10523
|
+
|
|
10344
10524
|
rl.close();
|
|
10345
10525
|
process.exit(0);
|
|
10346
10526
|
}
|