@cachly-dev/mcp-server 0.9.11 → 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 +319 -105
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +10663 -0
package/dist/index.js
CHANGED
|
@@ -2434,7 +2434,11 @@ const TOOLS = [
|
|
|
2434
2434
|
{
|
|
2435
2435
|
name: 'cache_get',
|
|
2436
2436
|
description: 'Get a value from a running cache instance by key. ' +
|
|
2437
|
-
'Returns the value (string or JSON) or null if the key does not exist.'
|
|
2437
|
+
'Returns the stored value (string or deserialized JSON object) or null if the key does not exist or has expired. ' +
|
|
2438
|
+
'Read-only — no side effects. ' +
|
|
2439
|
+
'Use cache_mget when you need multiple keys in one round-trip. ' +
|
|
2440
|
+
'Use cache_exists to check existence without retrieving the value. ' +
|
|
2441
|
+
'Use semantic_search when you need fuzzy/vector search across stored values.',
|
|
2438
2442
|
inputSchema: {
|
|
2439
2443
|
type: 'object',
|
|
2440
2444
|
properties: {
|
|
@@ -2503,12 +2507,15 @@ const TOOLS = [
|
|
|
2503
2507
|
},
|
|
2504
2508
|
{
|
|
2505
2509
|
name: 'cache_ttl',
|
|
2506
|
-
description: 'Get the remaining time-to-live (TTL) of a key in seconds.
|
|
2510
|
+
description: 'Get the remaining time-to-live (TTL) of a key in seconds. ' +
|
|
2511
|
+
'Returns -1 if the key exists but has no expiry, -2 if the key does not exist. ' +
|
|
2512
|
+
'Read-only — no side effects. ' +
|
|
2513
|
+
'Use cache_set with a ttl parameter to set or update the expiry.',
|
|
2507
2514
|
inputSchema: {
|
|
2508
2515
|
type: 'object',
|
|
2509
2516
|
properties: {
|
|
2510
|
-
instance_id: { type: 'string' },
|
|
2511
|
-
key: { type: 'string' },
|
|
2517
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance (from list_instances)' },
|
|
2518
|
+
key: { type: 'string', description: 'Cache key to inspect' },
|
|
2512
2519
|
},
|
|
2513
2520
|
required: ['instance_id', 'key'],
|
|
2514
2521
|
},
|
|
@@ -2688,7 +2695,11 @@ const TOOLS = [
|
|
|
2688
2695
|
name: 'cache_mset',
|
|
2689
2696
|
description: 'Set multiple key-value pairs in a single pipeline round-trip. ' +
|
|
2690
2697
|
'Supports per-key TTL – unlike native MSET. ' +
|
|
2691
|
-
'Uses one TCP round-trip for N keys via Redis pipeline.'
|
|
2698
|
+
'Uses one TCP round-trip for N keys via Redis pipeline. ' +
|
|
2699
|
+
'Each item overwrites any existing value for that key. ' +
|
|
2700
|
+
'On partial failure the successfully pipelined keys are committed; a per-key error list is returned for any that failed. ' +
|
|
2701
|
+
'Returns a summary: { set: N, errors: [...] }. ' +
|
|
2702
|
+
'Use cache_set for a single key; use cache_stream_set for large streaming payloads.',
|
|
2692
2703
|
inputSchema: {
|
|
2693
2704
|
type: 'object',
|
|
2694
2705
|
properties: {
|
|
@@ -2771,8 +2782,11 @@ const TOOLS = [
|
|
|
2771
2782
|
'Perfect for caching: codebase overviews, file summaries, project structure, ' +
|
|
2772
2783
|
'frequently-accessed data, or "thinking" results like dependency analysis. ' +
|
|
2773
2784
|
'The AI assistant can use this to avoid re-reading the entire codebase every time. ' +
|
|
2785
|
+
'Overwrites any existing value stored under the same key. ' +
|
|
2786
|
+
'Returns { key, stored_at, ttl } confirming the saved context. ' +
|
|
2774
2787
|
'Example: remember_context("project overview", "This is a Next.js app with...") ' +
|
|
2775
|
-
'then later: recall_context("project overview")'
|
|
2788
|
+
'then later: recall_context("project overview"). ' +
|
|
2789
|
+
'Use recall_context to retrieve; use list_remembered to see all stored keys.',
|
|
2776
2790
|
inputSchema: {
|
|
2777
2791
|
type: 'object',
|
|
2778
2792
|
properties: {
|
|
@@ -3347,7 +3361,11 @@ const TOOLS = [
|
|
|
3347
3361
|
name: 'publish_lesson',
|
|
3348
3362
|
description: 'Publish a lesson to the Cachly Public Brain (anonymized community knowledge base). ' +
|
|
3349
3363
|
'Published lessons can be imported by other developers via import_public_brain. ' +
|
|
3350
|
-
'PII is stripped automatically. Visible under the framework/category tag.'
|
|
3364
|
+
'PII is stripped automatically. Visible under the framework/category tag. ' +
|
|
3365
|
+
'Returns { lesson_id, topic, framework, published_at } confirming the publish. ' +
|
|
3366
|
+
'Irreversible — once published to the public brain, lessons cannot be deleted via the MCP interface. ' +
|
|
3367
|
+
'Use learn_from_attempts or global_learn for private lessons; ' +
|
|
3368
|
+
'use syndicate for anonymized global sharing without framework tagging.',
|
|
3351
3369
|
inputSchema: {
|
|
3352
3370
|
type: 'object',
|
|
3353
3371
|
properties: {
|
|
@@ -3644,7 +3662,10 @@ const TOOLS = [
|
|
|
3644
3662
|
'This is how individual knowledge becomes collective intelligence. ' +
|
|
3645
3663
|
'Call this AFTER every learn_from_attempts that is worth sharing universally ' +
|
|
3646
3664
|
'(critical bugs, deployment gotchas, architecture discoveries). ' +
|
|
3647
|
-
'
|
|
3665
|
+
'If a lesson with the same topic already exists in the commons, it is updated in place (idempotent). ' +
|
|
3666
|
+
'Returns { key, confirm_count, scope } confirming the stored lesson. ' +
|
|
3667
|
+
'Use scope="org" to keep the lesson private to your organisation. ' +
|
|
3668
|
+
'Do NOT use for secrets or PII — content is stored in a shared knowledge base.',
|
|
3648
3669
|
inputSchema: {
|
|
3649
3670
|
type: 'object',
|
|
3650
3671
|
properties: {
|
|
@@ -4904,6 +4925,8 @@ async function handleTool(name, args) {
|
|
|
4904
4925
|
}
|
|
4905
4926
|
catch { /* non-critical */ }
|
|
4906
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;
|
|
4907
4930
|
try {
|
|
4908
4931
|
const conceptId = ckgSlug(topic);
|
|
4909
4932
|
const domain = topic.split(':')[0] ?? 'unknown';
|
|
@@ -4941,6 +4964,71 @@ async function handleTool(name, args) {
|
|
|
4941
4964
|
await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
|
|
4942
4965
|
}
|
|
4943
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
|
+
}
|
|
4944
5032
|
}
|
|
4945
5033
|
catch { /* CKG updates are non-critical */ }
|
|
4946
5034
|
const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
|
|
@@ -4948,6 +5036,7 @@ async function handleTool(name, args) {
|
|
|
4948
5036
|
const action = isUpdate ? 'updated' : 'stored';
|
|
4949
5037
|
return [
|
|
4950
5038
|
`${emoji} **Lesson ${action}:** \`${topic}\` (${outcome}) ${sevEmoji} ${severity}`,
|
|
5039
|
+
beliefConflict ?? '',
|
|
4951
5040
|
``,
|
|
4952
5041
|
`**What worked:** ${what_worked}`,
|
|
4953
5042
|
what_failed ? `**What failed:** ${what_failed}` : '',
|
|
@@ -5449,6 +5538,24 @@ async function handleTool(name, args) {
|
|
|
5449
5538
|
lines.push('');
|
|
5450
5539
|
}
|
|
5451
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 */ }
|
|
5452
5559
|
if (focusLessons.length > 0) {
|
|
5453
5560
|
lines.push(`🎯 **Relevant for "${focus}":**`);
|
|
5454
5561
|
for (const l of focusLessons.slice(0, 4)) {
|
|
@@ -8737,107 +8844,114 @@ smart_recall(
|
|
|
8737
8844
|
const { resolve } = await import('node:path');
|
|
8738
8845
|
const repoDir = resolve(repo_path);
|
|
8739
8846
|
const maxCommits = Math.min(Number(limit) || 100, 500);
|
|
8740
|
-
//
|
|
8741
|
-
|
|
8742
|
-
execSync('git rev-parse --git-dir', { cwd: repoDir, stdio: 'pipe' });
|
|
8743
|
-
}
|
|
8744
|
-
catch {
|
|
8745
|
-
return `❌ Not a git repository: \`${repoDir}\`. Pass \`repo_path\` pointing to a git checkout.`;
|
|
8746
|
-
}
|
|
8747
|
-
// Build git log command
|
|
8748
|
-
const sinceFlag = since ? `--since="${since}"` : '';
|
|
8749
|
-
const logCmd = `git log ${branch} ${sinceFlag} --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n ${maxCommits}`;
|
|
8750
|
-
let logOutput = '';
|
|
8847
|
+
// Semaphore: max 10 concurrent git subprocesses per MCP process
|
|
8848
|
+
await _gitSemAcquire();
|
|
8751
8849
|
try {
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
return `❌ git log failed: ${e.message}. Check \`repo_path\` and \`branch\`.`;
|
|
8756
|
-
}
|
|
8757
|
-
const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
8758
|
-
const [sha, subject, date, author] = line.split('|||');
|
|
8759
|
-
return { sha: (sha ?? '').trim(), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
|
|
8760
|
-
});
|
|
8761
|
-
if (commits.length === 0) {
|
|
8762
|
-
return `⚠️ No commits found in \`${repoDir}\` on branch \`${branch}\`${since ? ` since ${since}` : ''}.`;
|
|
8763
|
-
}
|
|
8764
|
-
// Pattern classifiers
|
|
8765
|
-
const classifyCommit = (subject) => {
|
|
8766
|
-
const s = subject.toLowerCase();
|
|
8767
|
-
if (/\b(fix|fixed|fixes|bug|hotfix|patch|revert|resolve|closes? #\d+)\b/.test(s)) {
|
|
8768
|
-
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';
|
|
8769
|
-
return { category: 'fix', outcome: 'success', severity: sev };
|
|
8770
|
-
}
|
|
8771
|
-
if (/\b(feat|feature|add|added|implement|new|introduce)\b/.test(s))
|
|
8772
|
-
return { category: 'feat', outcome: 'success', severity: 'minor' };
|
|
8773
|
-
if (/\b(refactor|clean|cleanup|improve|simplify|extract|rename)\b/.test(s))
|
|
8774
|
-
return { category: 'refactor', outcome: 'success', severity: 'minor' };
|
|
8775
|
-
if (/\b(perf|optimize|speed|cache|latency|memory|performance)\b/.test(s))
|
|
8776
|
-
return { category: 'perf', outcome: 'success', severity: 'major' };
|
|
8777
|
-
if (/\b(security|cve|auth|csrf|xss|sql|injection|sanitize|escape|encrypt)\b/.test(s))
|
|
8778
|
-
return { category: 'security', outcome: 'success', severity: 'critical' };
|
|
8779
|
-
if (/\b(deploy|ci|cd|build|docker|k8s|helm|infra|devops)\b/.test(s))
|
|
8780
|
-
return { category: 'deploy', outcome: 'success', severity: 'major' };
|
|
8781
|
-
if (/\b(test|spec|coverage|assert|mock|unit|integration)\b/.test(s))
|
|
8782
|
-
return { category: 'test', outcome: 'success', severity: 'minor' };
|
|
8783
|
-
return { category: 'chore', outcome: 'success', severity: 'minor' };
|
|
8784
|
-
};
|
|
8785
|
-
// Extract domain keywords from commit subject
|
|
8786
|
-
const extractDomain = (subject) => {
|
|
8787
|
-
const s = subject.toLowerCase();
|
|
8788
|
-
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));
|
|
8789
|
-
return tokens.slice(0, 3).join('-') || 'general';
|
|
8790
|
-
};
|
|
8791
|
-
const ts = new Date().toISOString();
|
|
8792
|
-
let ingested = 0;
|
|
8793
|
-
let skipped = 0;
|
|
8794
|
-
const categoryCount = new Map();
|
|
8795
|
-
for (const commit of commits) {
|
|
8796
|
-
if (!commit.subject) {
|
|
8797
|
-
skipped++;
|
|
8798
|
-
continue;
|
|
8850
|
+
// Verify it's a git repo
|
|
8851
|
+
try {
|
|
8852
|
+
execSync('git rev-parse --git-dir', { cwd: repoDir, stdio: 'pipe' });
|
|
8799
8853
|
}
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
const topic = `${category}:${domain}`;
|
|
8803
|
-
categoryCount.set(category, (categoryCount.get(category) ?? 0) + 1);
|
|
8804
|
-
const lessonObj = {
|
|
8805
|
-
topic, outcome, severity,
|
|
8806
|
-
what_worked: commit.subject.slice(0, 200),
|
|
8807
|
-
what_failed: '',
|
|
8808
|
-
context: `git:${commit.sha.slice(0, 8)} by ${commit.author} on ${commit.date}`,
|
|
8809
|
-
file_paths: [], commands: [`git show ${commit.sha.slice(0, 8)}`],
|
|
8810
|
-
tags: ['brain_from_git', category, 'git-history'],
|
|
8811
|
-
depends_on: [], recall_count: 0, ts, verified_at: ts,
|
|
8812
|
-
confidence: 0.55, // lower confidence for auto-inferred lessons
|
|
8813
|
-
audit_trail: [{ ts, action: 'brain_from_git', sha: commit.sha.slice(0, 8) }],
|
|
8814
|
-
version: 3,
|
|
8815
|
-
};
|
|
8816
|
-
// Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
|
|
8817
|
-
const existing = await redis.get(`cachly:lesson:best:${topic}`);
|
|
8818
|
-
if (!existing) {
|
|
8819
|
-
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj));
|
|
8820
|
-
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.`;
|
|
8821
8856
|
}
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
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();
|
|
8828
8954
|
}
|
|
8829
|
-
const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
|
|
8830
|
-
return [
|
|
8831
|
-
`🔁 **brain_from_git: ${repoDir}**`, '',
|
|
8832
|
-
`📂 Branch: \`${branch}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
|
|
8833
|
-
``,
|
|
8834
|
-
`**Breakdown by category:**`,
|
|
8835
|
-
categoryBreakdown,
|
|
8836
|
-
``,
|
|
8837
|
-
`💡 New lessons are stored with confidence 0.55 (auto-inferred).`,
|
|
8838
|
-
`💡 As you confirm them via \`learn_from_attempts\`, confidence rises automatically.`,
|
|
8839
|
-
`🔍 Explore: \`brain_search(query="fix")\` | \`ckg_inspect(concept="deploy")\``,
|
|
8840
|
-
].join('\n');
|
|
8841
8955
|
}
|
|
8842
8956
|
// ── brain_predict_failures ─────────────────────────────────────────────────
|
|
8843
8957
|
case 'brain_predict_failures': {
|
|
@@ -8978,6 +9092,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
|
8978
9092
|
let _autoSessionInstanceId = null;
|
|
8979
9093
|
let _autoSessionStarted = false;
|
|
8980
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
|
+
}
|
|
8981
9117
|
async function autoStartSession(instanceId) {
|
|
8982
9118
|
if (_autoSessionStarted)
|
|
8983
9119
|
return;
|
|
@@ -9214,6 +9350,47 @@ if (process.argv[2] === 'init') {
|
|
|
9214
9350
|
const result = await writeClaudeMd(projectDir, instanceId);
|
|
9215
9351
|
const action = result === 'updated' ? '✅ Updated' : result === 'appended' ? '✅ Appended to' : '✅ Written';
|
|
9216
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 */ }
|
|
9217
9394
|
console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}!`);
|
|
9218
9395
|
console.log(` Restart your editor — the \`cachly\` MCP tools will appear.\n`);
|
|
9219
9396
|
process.exit(0);
|
|
@@ -9541,6 +9718,43 @@ if (process.argv[2] === 'setup') {
|
|
|
9541
9718
|
catch { /* fire and forget */ }
|
|
9542
9719
|
}
|
|
9543
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 */ }
|
|
9544
9758
|
rl.close();
|
|
9545
9759
|
process.exit(0);
|
|
9546
9760
|
}
|