@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.9.12",
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
- _autoSessionStarted = true;
9782
- _autoSessionInstanceId = instanceId;
9783
- try {
9784
- await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)' });
9785
- } catch { /* non-fatal — session tracking is a best-effort feature */ }
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
  }