@cachly-dev/mcp-server 0.9.12 → 0.9.13
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 +290 -97
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +170 -0
package/dist/index.js
CHANGED
|
@@ -4925,6 +4925,8 @@ async function handleTool(name, args) {
|
|
|
4925
4925
|
}
|
|
4926
4926
|
catch { /* non-critical */ }
|
|
4927
4927
|
// ── Layer 1+2: CKG update (Causal Knowledge Graph + Belief Update Engine) ──
|
|
4928
|
+
// BUE: Bayesian confidence, contradiction detection, second-degree propagation, decay
|
|
4929
|
+
let beliefConflict = null;
|
|
4928
4930
|
try {
|
|
4929
4931
|
const conceptId = ckgSlug(topic);
|
|
4930
4932
|
const domain = topic.split(':')[0] ?? 'unknown';
|
|
@@ -4962,6 +4964,71 @@ async function handleTool(name, args) {
|
|
|
4962
4964
|
await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
|
|
4963
4965
|
}
|
|
4964
4966
|
}
|
|
4967
|
+
// ── BUE: Contradiction detection ──────────────────────────────────────
|
|
4968
|
+
// If this topic previously had a confirmed 'fixes' edge (confidence > 0.7)
|
|
4969
|
+
// and now outcome=failure → flag belief_conflict
|
|
4970
|
+
if (outcome === 'failure') {
|
|
4971
|
+
const existingEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
|
|
4972
|
+
for (const ek of existingEdgeKeys) {
|
|
4973
|
+
const er = await redis.get(ek);
|
|
4974
|
+
if (!er)
|
|
4975
|
+
continue;
|
|
4976
|
+
const existEdge = JSON.parse(er);
|
|
4977
|
+
if (existEdge.edgeType === 'fixes' && existEdge.confidence > 0.7 && existEdge.trials >= 3) {
|
|
4978
|
+
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.`;
|
|
4979
|
+
// Store conflict marker
|
|
4980
|
+
const conflictKey = `cachly:ckg:conflict:${conceptId}`;
|
|
4981
|
+
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);
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
// ── BUE: Second-degree propagation ────────────────────────────────────
|
|
4986
|
+
// When a 'fixes' edge gets stronger, boost co-occurring second-degree edges slightly
|
|
4987
|
+
if (outcome === 'success' && (domain === 'fix' || domain === 'debug')) {
|
|
4988
|
+
const fromEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
|
|
4989
|
+
for (const ek of fromEdgeKeys.slice(0, 10)) {
|
|
4990
|
+
const er = await redis.get(ek);
|
|
4991
|
+
if (!er)
|
|
4992
|
+
continue;
|
|
4993
|
+
const e2 = JSON.parse(er);
|
|
4994
|
+
if (e2.edgeType !== 'fixes')
|
|
4995
|
+
continue;
|
|
4996
|
+
// Boost second-degree: edges from e2.to get a small fractional success
|
|
4997
|
+
const secondKeys = await redis.smembers(`cachly:ckg:idx:from:${e2.to}`);
|
|
4998
|
+
for (const sk of secondKeys.slice(0, 5)) {
|
|
4999
|
+
const sr = await redis.get(sk);
|
|
5000
|
+
if (!sr)
|
|
5001
|
+
continue;
|
|
5002
|
+
const se = JSON.parse(sr);
|
|
5003
|
+
if (se.edgeType !== 'co-occurs')
|
|
5004
|
+
continue;
|
|
5005
|
+
// Add 0.1 fractional success (second-degree signal)
|
|
5006
|
+
se.successes = (se.successes || 0) + 0.1;
|
|
5007
|
+
se.trials = (se.trials || 0) + 0.1;
|
|
5008
|
+
se.confidence = (se.successes + 1) / (se.trials + 2);
|
|
5009
|
+
se.last_updated = new Date().toISOString();
|
|
5010
|
+
await redis.set(sk, JSON.stringify(se));
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
// ── BUE: Stale edge decay ─────────────────────────────────────────────
|
|
5015
|
+
// Edges older than 90 days with < 3 trials decay by 10% confidence.
|
|
5016
|
+
// Only run probabilistically (1% of calls) to avoid per-call overhead.
|
|
5017
|
+
if (Math.random() < 0.01) {
|
|
5018
|
+
const decayCutoff = Date.now() - 90 * 24 * 60 * 60 * 1000;
|
|
5019
|
+
const fromEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
|
|
5020
|
+
for (const ek of fromEdgeKeys) {
|
|
5021
|
+
const er = await redis.get(ek);
|
|
5022
|
+
if (!er)
|
|
5023
|
+
continue;
|
|
5024
|
+
const de = JSON.parse(er);
|
|
5025
|
+
if (de.trials < 3 && de.last_updated && new Date(de.last_updated).getTime() < decayCutoff) {
|
|
5026
|
+
de.confidence = de.confidence * 0.9;
|
|
5027
|
+
de.last_updated = new Date().toISOString();
|
|
5028
|
+
await redis.set(ek, JSON.stringify(de));
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
4965
5032
|
}
|
|
4966
5033
|
catch { /* CKG updates are non-critical */ }
|
|
4967
5034
|
const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
|
|
@@ -4969,6 +5036,7 @@ async function handleTool(name, args) {
|
|
|
4969
5036
|
const action = isUpdate ? 'updated' : 'stored';
|
|
4970
5037
|
return [
|
|
4971
5038
|
`${emoji} **Lesson ${action}:** \`${topic}\` (${outcome}) ${sevEmoji} ${severity}`,
|
|
5039
|
+
beliefConflict ?? '',
|
|
4972
5040
|
``,
|
|
4973
5041
|
`**What worked:** ${what_worked}`,
|
|
4974
5042
|
what_failed ? `**What failed:** ${what_failed}` : '',
|
|
@@ -5470,6 +5538,24 @@ async function handleTool(name, args) {
|
|
|
5470
5538
|
lines.push('');
|
|
5471
5539
|
}
|
|
5472
5540
|
}
|
|
5541
|
+
// ── Layer 7 MCM: Active belief conflicts ─────────────────────────────────
|
|
5542
|
+
try {
|
|
5543
|
+
const conflictKeys = [];
|
|
5544
|
+
const cfStream = redis.scanStream({ match: 'cachly:ckg:conflict:*', count: 50 });
|
|
5545
|
+
await new Promise((res, rej) => { cfStream.on('data', (b) => conflictKeys.push(...b)); cfStream.on('end', res); cfStream.on('error', rej); });
|
|
5546
|
+
if (conflictKeys.length > 0) {
|
|
5547
|
+
lines.push(`⚡ **Active belief conflicts (${conflictKeys.length}):**`);
|
|
5548
|
+
for (const ck of conflictKeys.slice(0, 3)) {
|
|
5549
|
+
const cr = await redis.get(ck);
|
|
5550
|
+
if (!cr)
|
|
5551
|
+
continue;
|
|
5552
|
+
const cf = JSON.parse(cr);
|
|
5553
|
+
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)}")\``);
|
|
5554
|
+
}
|
|
5555
|
+
lines.push('');
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
5558
|
+
catch { /* non-critical */ }
|
|
5473
5559
|
if (focusLessons.length > 0) {
|
|
5474
5560
|
lines.push(`🎯 **Relevant for "${focus}":**`);
|
|
5475
5561
|
for (const l of focusLessons.slice(0, 4)) {
|
|
@@ -8758,107 +8844,114 @@ smart_recall(
|
|
|
8758
8844
|
const { resolve } = await import('node:path');
|
|
8759
8845
|
const repoDir = resolve(repo_path);
|
|
8760
8846
|
const maxCommits = Math.min(Number(limit) || 100, 500);
|
|
8761
|
-
//
|
|
8847
|
+
// Semaphore: max 10 concurrent git subprocesses per MCP process
|
|
8848
|
+
await _gitSemAcquire();
|
|
8762
8849
|
try {
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
return `❌ Not a git repository: \`${repoDir}\`. Pass \`repo_path\` pointing to a git checkout.`;
|
|
8767
|
-
}
|
|
8768
|
-
// Build git log command
|
|
8769
|
-
const sinceFlag = since ? `--since="${since}"` : '';
|
|
8770
|
-
const logCmd = `git log ${branch} ${sinceFlag} --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n ${maxCommits}`;
|
|
8771
|
-
let logOutput = '';
|
|
8772
|
-
try {
|
|
8773
|
-
logOutput = execSync(logCmd, { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
8774
|
-
}
|
|
8775
|
-
catch (e) {
|
|
8776
|
-
return `❌ git log failed: ${e.message}. Check \`repo_path\` and \`branch\`.`;
|
|
8777
|
-
}
|
|
8778
|
-
const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
8779
|
-
const [sha, subject, date, author] = line.split('|||');
|
|
8780
|
-
return { sha: (sha ?? '').trim(), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
|
|
8781
|
-
});
|
|
8782
|
-
if (commits.length === 0) {
|
|
8783
|
-
return `⚠️ No commits found in \`${repoDir}\` on branch \`${branch}\`${since ? ` since ${since}` : ''}.`;
|
|
8784
|
-
}
|
|
8785
|
-
// Pattern classifiers
|
|
8786
|
-
const classifyCommit = (subject) => {
|
|
8787
|
-
const s = subject.toLowerCase();
|
|
8788
|
-
if (/\b(fix|fixed|fixes|bug|hotfix|patch|revert|resolve|closes? #\d+)\b/.test(s)) {
|
|
8789
|
-
const sev = /\b(critical|crash|security|auth|data loss|outage|prod|production)\b/.test(s) ? 'critical' : /\b(major|breaking|regression|hotfix)\b/.test(s) ? 'major' : 'minor';
|
|
8790
|
-
return { category: 'fix', outcome: 'success', severity: sev };
|
|
8791
|
-
}
|
|
8792
|
-
if (/\b(feat|feature|add|added|implement|new|introduce)\b/.test(s))
|
|
8793
|
-
return { category: 'feat', outcome: 'success', severity: 'minor' };
|
|
8794
|
-
if (/\b(refactor|clean|cleanup|improve|simplify|extract|rename)\b/.test(s))
|
|
8795
|
-
return { category: 'refactor', outcome: 'success', severity: 'minor' };
|
|
8796
|
-
if (/\b(perf|optimize|speed|cache|latency|memory|performance)\b/.test(s))
|
|
8797
|
-
return { category: 'perf', outcome: 'success', severity: 'major' };
|
|
8798
|
-
if (/\b(security|cve|auth|csrf|xss|sql|injection|sanitize|escape|encrypt)\b/.test(s))
|
|
8799
|
-
return { category: 'security', outcome: 'success', severity: 'critical' };
|
|
8800
|
-
if (/\b(deploy|ci|cd|build|docker|k8s|helm|infra|devops)\b/.test(s))
|
|
8801
|
-
return { category: 'deploy', outcome: 'success', severity: 'major' };
|
|
8802
|
-
if (/\b(test|spec|coverage|assert|mock|unit|integration)\b/.test(s))
|
|
8803
|
-
return { category: 'test', outcome: 'success', severity: 'minor' };
|
|
8804
|
-
return { category: 'chore', outcome: 'success', severity: 'minor' };
|
|
8805
|
-
};
|
|
8806
|
-
// Extract domain keywords from commit subject
|
|
8807
|
-
const extractDomain = (subject) => {
|
|
8808
|
-
const s = subject.toLowerCase();
|
|
8809
|
-
const tokens = s.replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !['that', 'this', 'with', 'from', 'when', 'into', 'also', 'some', 'were'].includes(t));
|
|
8810
|
-
return tokens.slice(0, 3).join('-') || 'general';
|
|
8811
|
-
};
|
|
8812
|
-
const ts = new Date().toISOString();
|
|
8813
|
-
let ingested = 0;
|
|
8814
|
-
let skipped = 0;
|
|
8815
|
-
const categoryCount = new Map();
|
|
8816
|
-
for (const commit of commits) {
|
|
8817
|
-
if (!commit.subject) {
|
|
8818
|
-
skipped++;
|
|
8819
|
-
continue;
|
|
8850
|
+
// Verify it's a git repo
|
|
8851
|
+
try {
|
|
8852
|
+
execSync('git rev-parse --git-dir', { cwd: repoDir, stdio: 'pipe' });
|
|
8820
8853
|
}
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
const topic = `${category}:${domain}`;
|
|
8824
|
-
categoryCount.set(category, (categoryCount.get(category) ?? 0) + 1);
|
|
8825
|
-
const lessonObj = {
|
|
8826
|
-
topic, outcome, severity,
|
|
8827
|
-
what_worked: commit.subject.slice(0, 200),
|
|
8828
|
-
what_failed: '',
|
|
8829
|
-
context: `git:${commit.sha.slice(0, 8)} by ${commit.author} on ${commit.date}`,
|
|
8830
|
-
file_paths: [], commands: [`git show ${commit.sha.slice(0, 8)}`],
|
|
8831
|
-
tags: ['brain_from_git', category, 'git-history'],
|
|
8832
|
-
depends_on: [], recall_count: 0, ts, verified_at: ts,
|
|
8833
|
-
confidence: 0.55, // lower confidence for auto-inferred lessons
|
|
8834
|
-
audit_trail: [{ ts, action: 'brain_from_git', sha: commit.sha.slice(0, 8) }],
|
|
8835
|
-
version: 3,
|
|
8836
|
-
};
|
|
8837
|
-
// Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
|
|
8838
|
-
const existing = await redis.get(`cachly:lesson:best:${topic}`);
|
|
8839
|
-
if (!existing) {
|
|
8840
|
-
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj));
|
|
8841
|
-
await redis.rpush(`cachly:lessons:${topic}`, JSON.stringify(lessonObj));
|
|
8854
|
+
catch {
|
|
8855
|
+
return `❌ Not a git repository: \`${repoDir}\`. Pass \`repo_path\` pointing to a git checkout.`;
|
|
8842
8856
|
}
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8857
|
+
// Build git log command
|
|
8858
|
+
const sinceFlag = since ? `--since="${since}"` : '';
|
|
8859
|
+
const logCmd = `git log ${branch} ${sinceFlag} --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n ${maxCommits}`;
|
|
8860
|
+
let logOutput = '';
|
|
8861
|
+
try {
|
|
8862
|
+
logOutput = execSync(logCmd, { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
8863
|
+
}
|
|
8864
|
+
catch (e) {
|
|
8865
|
+
return `❌ git log failed: ${e.message}. Check \`repo_path\` and \`branch\`.`;
|
|
8866
|
+
}
|
|
8867
|
+
const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
8868
|
+
const [sha, subject, date, author] = line.split('|||');
|
|
8869
|
+
return { sha: (sha ?? '').trim(), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
|
|
8870
|
+
});
|
|
8871
|
+
if (commits.length === 0) {
|
|
8872
|
+
return `⚠️ No commits found in \`${repoDir}\` on branch \`${branch}\`${since ? ` since ${since}` : ''}.`;
|
|
8873
|
+
}
|
|
8874
|
+
// Pattern classifiers
|
|
8875
|
+
const classifyCommit = (subject) => {
|
|
8876
|
+
const s = subject.toLowerCase();
|
|
8877
|
+
if (/\b(fix|fixed|fixes|bug|hotfix|patch|revert|resolve|closes? #\d+)\b/.test(s)) {
|
|
8878
|
+
const sev = /\b(critical|crash|security|auth|data loss|outage|prod|production)\b/.test(s) ? 'critical' : /\b(major|breaking|regression|hotfix)\b/.test(s) ? 'major' : 'minor';
|
|
8879
|
+
return { category: 'fix', outcome: 'success', severity: sev };
|
|
8880
|
+
}
|
|
8881
|
+
if (/\b(feat|feature|add|added|implement|new|introduce)\b/.test(s))
|
|
8882
|
+
return { category: 'feat', outcome: 'success', severity: 'minor' };
|
|
8883
|
+
if (/\b(refactor|clean|cleanup|improve|simplify|extract|rename)\b/.test(s))
|
|
8884
|
+
return { category: 'refactor', outcome: 'success', severity: 'minor' };
|
|
8885
|
+
if (/\b(perf|optimize|speed|cache|latency|memory|performance)\b/.test(s))
|
|
8886
|
+
return { category: 'perf', outcome: 'success', severity: 'major' };
|
|
8887
|
+
if (/\b(security|cve|auth|csrf|xss|sql|injection|sanitize|escape|encrypt)\b/.test(s))
|
|
8888
|
+
return { category: 'security', outcome: 'success', severity: 'critical' };
|
|
8889
|
+
if (/\b(deploy|ci|cd|build|docker|k8s|helm|infra|devops)\b/.test(s))
|
|
8890
|
+
return { category: 'deploy', outcome: 'success', severity: 'major' };
|
|
8891
|
+
if (/\b(test|spec|coverage|assert|mock|unit|integration)\b/.test(s))
|
|
8892
|
+
return { category: 'test', outcome: 'success', severity: 'minor' };
|
|
8893
|
+
return { category: 'chore', outcome: 'success', severity: 'minor' };
|
|
8894
|
+
};
|
|
8895
|
+
// Extract domain keywords from commit subject
|
|
8896
|
+
const extractDomain = (subject) => {
|
|
8897
|
+
const s = subject.toLowerCase();
|
|
8898
|
+
const tokens = s.replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !['that', 'this', 'with', 'from', 'when', 'into', 'also', 'some', 'were'].includes(t));
|
|
8899
|
+
return tokens.slice(0, 3).join('-') || 'general';
|
|
8900
|
+
};
|
|
8901
|
+
const ts = new Date().toISOString();
|
|
8902
|
+
let ingested = 0;
|
|
8903
|
+
let skipped = 0;
|
|
8904
|
+
const categoryCount = new Map();
|
|
8905
|
+
for (const commit of commits) {
|
|
8906
|
+
if (!commit.subject) {
|
|
8907
|
+
skipped++;
|
|
8908
|
+
continue;
|
|
8909
|
+
}
|
|
8910
|
+
const { category, outcome, severity } = classifyCommit(commit.subject);
|
|
8911
|
+
const domain = extractDomain(commit.subject);
|
|
8912
|
+
const topic = `${category}:${domain}`;
|
|
8913
|
+
categoryCount.set(category, (categoryCount.get(category) ?? 0) + 1);
|
|
8914
|
+
const lessonObj = {
|
|
8915
|
+
topic, outcome, severity,
|
|
8916
|
+
what_worked: commit.subject.slice(0, 200),
|
|
8917
|
+
what_failed: '',
|
|
8918
|
+
context: `git:${commit.sha.slice(0, 8)} by ${commit.author} on ${commit.date}`,
|
|
8919
|
+
file_paths: [], commands: [`git show ${commit.sha.slice(0, 8)}`],
|
|
8920
|
+
tags: ['brain_from_git', category, 'git-history'],
|
|
8921
|
+
depends_on: [], recall_count: 0, ts, verified_at: ts,
|
|
8922
|
+
confidence: 0.55, // lower confidence for auto-inferred lessons
|
|
8923
|
+
audit_trail: [{ ts, action: 'brain_from_git', sha: commit.sha.slice(0, 8) }],
|
|
8924
|
+
version: 3,
|
|
8925
|
+
};
|
|
8926
|
+
// Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
|
|
8927
|
+
const existing = await redis.get(`cachly:lesson:best:${topic}`);
|
|
8928
|
+
if (!existing) {
|
|
8929
|
+
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj));
|
|
8930
|
+
await redis.rpush(`cachly:lessons:${topic}`, JSON.stringify(lessonObj));
|
|
8931
|
+
}
|
|
8932
|
+
await redis.rpush('cachly:lessons:brain_from_git:all', JSON.stringify({ topic, sha: commit.sha.slice(0, 8), subject: commit.subject.slice(0, 60) }));
|
|
8933
|
+
await redis.ltrim('cachly:lessons:brain_from_git:all', -500, -1);
|
|
8934
|
+
// Update CKG
|
|
8935
|
+
const conceptId = ckgSlug(topic);
|
|
8936
|
+
await ckgUpsertNode(redis, conceptId, category, 'git-derived');
|
|
8937
|
+
ingested++;
|
|
8938
|
+
}
|
|
8939
|
+
const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
|
|
8940
|
+
return [
|
|
8941
|
+
`🔁 **brain_from_git: ${repoDir}**`, '',
|
|
8942
|
+
`📂 Branch: \`${branch}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
|
|
8943
|
+
``,
|
|
8944
|
+
`**Breakdown by category:**`,
|
|
8945
|
+
categoryBreakdown,
|
|
8946
|
+
``,
|
|
8947
|
+
`💡 New lessons are stored with confidence 0.55 (auto-inferred).`,
|
|
8948
|
+
`💡 As you confirm them via \`learn_from_attempts\`, confidence rises automatically.`,
|
|
8949
|
+
`🔍 Explore: \`brain_search(query="fix")\` | \`ckg_inspect(concept="deploy")\``,
|
|
8950
|
+
].join('\n');
|
|
8951
|
+
}
|
|
8952
|
+
finally {
|
|
8953
|
+
_gitSemRelease();
|
|
8849
8954
|
}
|
|
8850
|
-
const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
|
|
8851
|
-
return [
|
|
8852
|
-
`🔁 **brain_from_git: ${repoDir}**`, '',
|
|
8853
|
-
`📂 Branch: \`${branch}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
|
|
8854
|
-
``,
|
|
8855
|
-
`**Breakdown by category:**`,
|
|
8856
|
-
categoryBreakdown,
|
|
8857
|
-
``,
|
|
8858
|
-
`💡 New lessons are stored with confidence 0.55 (auto-inferred).`,
|
|
8859
|
-
`💡 As you confirm them via \`learn_from_attempts\`, confidence rises automatically.`,
|
|
8860
|
-
`🔍 Explore: \`brain_search(query="fix")\` | \`ckg_inspect(concept="deploy")\``,
|
|
8861
|
-
].join('\n');
|
|
8862
8955
|
}
|
|
8863
8956
|
// ── brain_predict_failures ─────────────────────────────────────────────────
|
|
8864
8957
|
case 'brain_predict_failures': {
|
|
@@ -8999,6 +9092,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
|
8999
9092
|
let _autoSessionInstanceId = null;
|
|
9000
9093
|
let _autoSessionStarted = false;
|
|
9001
9094
|
let _autoSessionToolCount = 0;
|
|
9095
|
+
// ── brain_from_git concurrency limiter ────────────────────────────────────────
|
|
9096
|
+
// Spawning many `git log` subprocesses in parallel can fork-bomb the host.
|
|
9097
|
+
// This simple semaphore caps concurrent brain_from_git calls to 10 per process.
|
|
9098
|
+
let _gitSemCount = 0;
|
|
9099
|
+
const _GIT_SEM_MAX = 10;
|
|
9100
|
+
const _gitSemQueue = [];
|
|
9101
|
+
function _gitSemAcquire() {
|
|
9102
|
+
if (_gitSemCount < _GIT_SEM_MAX) {
|
|
9103
|
+
_gitSemCount++;
|
|
9104
|
+
return Promise.resolve();
|
|
9105
|
+
}
|
|
9106
|
+
return new Promise(resolve => _gitSemQueue.push(resolve));
|
|
9107
|
+
}
|
|
9108
|
+
function _gitSemRelease() {
|
|
9109
|
+
const next = _gitSemQueue.shift();
|
|
9110
|
+
if (next) {
|
|
9111
|
+
next();
|
|
9112
|
+
}
|
|
9113
|
+
else {
|
|
9114
|
+
_gitSemCount--;
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9002
9117
|
async function autoStartSession(instanceId) {
|
|
9003
9118
|
if (_autoSessionStarted)
|
|
9004
9119
|
return;
|
|
@@ -9235,6 +9350,47 @@ if (process.argv[2] === 'init') {
|
|
|
9235
9350
|
const result = await writeClaudeMd(projectDir, instanceId);
|
|
9236
9351
|
const action = result === 'updated' ? '✅ Updated' : result === 'appended' ? '✅ Appended to' : '✅ Written';
|
|
9237
9352
|
console.log(`${action}: CLAUDE.md`);
|
|
9353
|
+
// ── CLS Phase 4: Auto-install git post-commit hook ─────────────────────────
|
|
9354
|
+
try {
|
|
9355
|
+
const { existsSync } = await import('node:fs');
|
|
9356
|
+
const gitHookDir = resolve(projectDir, '.git', 'hooks');
|
|
9357
|
+
const hookPath = resolve(gitHookDir, 'post-commit');
|
|
9358
|
+
if (existsSync(resolve(projectDir, '.git'))) {
|
|
9359
|
+
await mkdir(gitHookDir, { recursive: true });
|
|
9360
|
+
const hookScript = [
|
|
9361
|
+
`#!/bin/sh`,
|
|
9362
|
+
`# cachly CLS — Continuous Learning Stream (installed by cachly init)`,
|
|
9363
|
+
`# Runs silently on every commit to keep your brain up to date.`,
|
|
9364
|
+
`CACHLY_INSTANCE="${instanceId}"`,
|
|
9365
|
+
`SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
|
|
9366
|
+
`MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | tr '"' "'" | cut -c1-200)`,
|
|
9367
|
+
`FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
|
|
9368
|
+
`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 &`,
|
|
9369
|
+
`exit 0`,
|
|
9370
|
+
].join('\n');
|
|
9371
|
+
let existing = '';
|
|
9372
|
+
try {
|
|
9373
|
+
const { readFile } = await import('node:fs/promises');
|
|
9374
|
+
existing = await readFile(hookPath, 'utf-8');
|
|
9375
|
+
}
|
|
9376
|
+
catch { /* no existing hook */ }
|
|
9377
|
+
if (existing && !existing.includes('cachly CLS')) {
|
|
9378
|
+
// Append to existing hook
|
|
9379
|
+
await writeFile(hookPath, existing.trimEnd() + '\n\n' + hookScript + '\n', 'utf-8');
|
|
9380
|
+
console.log(`✅ Appended: .git/hooks/post-commit (CLS hook)`);
|
|
9381
|
+
}
|
|
9382
|
+
else if (!existing) {
|
|
9383
|
+
await writeFile(hookPath, hookScript + '\n', 'utf-8');
|
|
9384
|
+
const { chmod } = await import('node:fs/promises');
|
|
9385
|
+
await chmod(hookPath, 0o755);
|
|
9386
|
+
console.log(`✅ Written: .git/hooks/post-commit (CLS hook)`);
|
|
9387
|
+
}
|
|
9388
|
+
else {
|
|
9389
|
+
console.log(`✓ CLS hook already present in .git/hooks/post-commit`);
|
|
9390
|
+
}
|
|
9391
|
+
}
|
|
9392
|
+
}
|
|
9393
|
+
catch { /* non-critical — git hook is a best-effort feature */ }
|
|
9238
9394
|
console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}!`);
|
|
9239
9395
|
console.log(` Restart your editor — the \`cachly\` MCP tools will appear.\n`);
|
|
9240
9396
|
process.exit(0);
|
|
@@ -9562,6 +9718,43 @@ if (process.argv[2] === 'setup') {
|
|
|
9562
9718
|
catch { /* fire and forget */ }
|
|
9563
9719
|
}
|
|
9564
9720
|
}
|
|
9721
|
+
// ── CLS Phase 4: Auto-install git post-commit hook in setup flow ─────────
|
|
9722
|
+
try {
|
|
9723
|
+
const { existsSync } = await import('node:fs');
|
|
9724
|
+
const { writeFile: wf2, mkdir: mk2, chmod: ch2, readFile: rf2 } = await import('node:fs/promises');
|
|
9725
|
+
const { resolve: res2 } = await import('node:path');
|
|
9726
|
+
const setupProjectDir = process.cwd();
|
|
9727
|
+
const gitHookDir2 = res2(setupProjectDir, '.git', 'hooks');
|
|
9728
|
+
const hookPath2 = res2(gitHookDir2, 'post-commit');
|
|
9729
|
+
if (existsSync(res2(setupProjectDir, '.git'))) {
|
|
9730
|
+
await mk2(gitHookDir2, { recursive: true });
|
|
9731
|
+
const hs2 = [
|
|
9732
|
+
`#!/bin/sh`,
|
|
9733
|
+
`# cachly CLS — Continuous Learning Stream (installed by cachly setup)`,
|
|
9734
|
+
`CACHLY_INSTANCE="${instance.id}"`,
|
|
9735
|
+
`SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
|
|
9736
|
+
`MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | tr '"' "'" | cut -c1-200)`,
|
|
9737
|
+
`FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
|
|
9738
|
+
`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 &`,
|
|
9739
|
+
`exit 0`,
|
|
9740
|
+
].join('\n');
|
|
9741
|
+
let ex2 = '';
|
|
9742
|
+
try {
|
|
9743
|
+
ex2 = await rf2(hookPath2, 'utf-8');
|
|
9744
|
+
}
|
|
9745
|
+
catch { /* no existing */ }
|
|
9746
|
+
if (!ex2) {
|
|
9747
|
+
await wf2(hookPath2, hs2 + '\n', 'utf-8');
|
|
9748
|
+
await ch2(hookPath2, 0o755);
|
|
9749
|
+
console.log(`\n✅ CLS git hook installed — your brain will learn from every commit.`);
|
|
9750
|
+
}
|
|
9751
|
+
else if (!ex2.includes('cachly CLS')) {
|
|
9752
|
+
await wf2(hookPath2, ex2.trimEnd() + '\n\n' + hs2 + '\n', 'utf-8');
|
|
9753
|
+
console.log(`\n✅ CLS git hook appended to existing post-commit.`);
|
|
9754
|
+
}
|
|
9755
|
+
}
|
|
9756
|
+
}
|
|
9757
|
+
catch { /* non-critical */ }
|
|
9565
9758
|
rl.close();
|
|
9566
9759
|
process.exit(0);
|
|
9567
9760
|
}
|