@cachly-dev/mcp-server 0.10.13 → 0.10.15
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/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
- package/.github/workflows/publish.yml +72 -0
- package/CHANGELOG.md +28 -0
- package/LAUNCH_TEXTS.md +182 -0
- package/LICENSE +202 -0
- package/README.md +106 -53
- package/dist/__tests__/auth.test.d.ts +1 -0
- package/dist/__tests__/auth.test.js +107 -0
- package/dist/__tests__/auth.test.js.map +1 -0
- package/dist/handlers/brain.js +7 -67
- package/dist/handlers/brain.js.map +1 -1
- package/dist/handlers/fedbrain.js +93 -44
- package/dist/handlers/fedbrain.js.map +1 -1
- package/dist/handlers/roadmap.js +8 -2
- package/dist/handlers/roadmap.js.map +1 -1
- package/dist/handlers/syndicate.js +15 -7
- package/dist/handlers/syndicate.js.map +1 -1
- package/dist/handlers/team.js +62 -50
- package/dist/handlers/team.js.map +1 -1
- package/dist/index.js +555 -93
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +3 -54
- package/dist/tools.js +8 -62
- package/dist/tools.js.map +1 -1
- package/glama.json +29 -0
- package/llms.txt +155 -0
- package/package.json +29 -13
- package/scripts/postinstall.js +12 -12
- package/server.json +3 -9
- package/smithery.yaml +6 -8
- package/src/__tests__/auth.test.ts +147 -0
- package/src/handlers/brain.ts +7 -64
- package/src/handlers/fedbrain.ts +88 -38
- package/src/handlers/roadmap.ts +7 -2
- package/src/handlers/syndicate.ts +13 -7
- package/src/handlers/team.ts +57 -33
- package/src/index.ts +599 -106
- package/src/tools.ts +8 -66
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import { Redis } from 'ioredis';
|
|
|
42
42
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
43
43
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
44
44
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
45
|
-
const CURRENT_VERSION = '0.10.
|
|
45
|
+
const CURRENT_VERSION = '0.10.15';
|
|
46
46
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
47
47
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
48
48
|
// When neither is set, we auto-fetch the first running instance once per process.
|
|
@@ -557,32 +557,7 @@ async function handleTool(name, args) {
|
|
|
557
557
|
}
|
|
558
558
|
}
|
|
559
559
|
const server = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} } });
|
|
560
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
561
|
-
if (!JWT) {
|
|
562
|
-
// No credentials — inject sign-up CTA into session_start description
|
|
563
|
-
const tools = TOOLS.map(t => {
|
|
564
|
-
if (t.name !== 'session_start')
|
|
565
|
-
return t;
|
|
566
|
-
return {
|
|
567
|
-
...t,
|
|
568
|
-
description: `⚡ FREE AI BRAIN — sign up once at https://cachly.dev/sign-up (no credit card) ` +
|
|
569
|
-
`then run: npx @cachly-dev/mcp-server@latest setup\n\n` + t.description,
|
|
570
|
-
};
|
|
571
|
-
});
|
|
572
|
-
return { tools };
|
|
573
|
-
}
|
|
574
|
-
// ── Auto-start session on connection (fire and forget) ────────────────────
|
|
575
|
-
// When the AI reads the tools list, it has just connected. Proactively start
|
|
576
|
-
// the Brain session in the background so by the time the first tool call
|
|
577
|
-
// arrives, session context is already loaded and ready to inject.
|
|
578
|
-
if (!_autoSessionStarted && !_autoSessionStarting) {
|
|
579
|
-
void resolveDefaultInstanceId().then(instanceId => {
|
|
580
|
-
if (instanceId)
|
|
581
|
-
void autoStartSession(instanceId).catch(() => undefined);
|
|
582
|
-
}).catch(() => undefined);
|
|
583
|
-
}
|
|
584
|
-
return { tools: TOOLS };
|
|
585
|
-
});
|
|
560
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
586
561
|
// ── Auto-session management ───────────────────────────────────────────────────
|
|
587
562
|
// Transparently starts a Brain session on the first tool call of a process
|
|
588
563
|
// and saves a session_end summary on SIGTERM/exit so users never have to call
|
|
@@ -591,87 +566,62 @@ let _autoSessionInstanceId = null;
|
|
|
591
566
|
let _autoSessionStarted = false;
|
|
592
567
|
let _autoSessionStarting = null; // guard against double-start race
|
|
593
568
|
let _autoSessionToolCount = 0;
|
|
594
|
-
let _autoSessionContext = null; // result of session_start, injected into first tool response
|
|
595
|
-
let _autoSessionContextInjected = false; // only inject once
|
|
596
569
|
// ── brain_from_git concurrency limiter ────────────────────────────────────────
|
|
597
570
|
// Spawning many `git log` subprocesses in parallel can fork-bomb the host.
|
|
598
571
|
// This simple semaphore caps concurrent brain_from_git calls to 10 per process.
|
|
599
572
|
async function autoStartSession(instanceId) {
|
|
600
573
|
if (_autoSessionStarted)
|
|
601
|
-
return
|
|
574
|
+
return;
|
|
602
575
|
// If another call already started the session, await it instead of double-starting.
|
|
603
576
|
if (_autoSessionStarting) {
|
|
604
|
-
|
|
577
|
+
await _autoSessionStarting;
|
|
578
|
+
return;
|
|
605
579
|
}
|
|
606
580
|
_autoSessionStarting = (async () => {
|
|
607
581
|
_autoSessionStarted = true;
|
|
608
582
|
_autoSessionInstanceId = instanceId;
|
|
609
|
-
let sessionResult = null;
|
|
610
583
|
try {
|
|
611
|
-
|
|
612
|
-
let focus = 'auto (MCP session)';
|
|
613
|
-
try {
|
|
614
|
-
const { execSync } = await import('node:child_process');
|
|
615
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
|
|
616
|
-
const lastMsg = execSync('git log -1 --pretty=%s', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
|
|
617
|
-
if (branch && branch !== 'HEAD') {
|
|
618
|
-
focus = lastMsg ? `${branch}: ${lastMsg.slice(0, 80)}` : `branch: ${branch}`;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
catch { /* git not available in this workspace */ }
|
|
622
|
-
sessionResult = await handleTool('session_start', { instance_id: instanceId, focus });
|
|
623
|
-
_autoSessionContext = sessionResult;
|
|
584
|
+
await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)' });
|
|
624
585
|
}
|
|
625
586
|
catch { /* non-fatal — session tracking is a best-effort feature */ }
|
|
626
587
|
finally {
|
|
627
588
|
_autoSessionStarting = null;
|
|
628
589
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
590
|
+
})();
|
|
591
|
+
await _autoSessionStarting;
|
|
592
|
+
// Auto-index the project if it hasn't been indexed in the last 24h.
|
|
593
|
+
// This is the main lever for growing token savings: more indexed code →
|
|
594
|
+
// more semantic cache hits → fewer LLM calls for repeated questions.
|
|
595
|
+
if (process.env.CACHLY_AUTO_INDEX !== 'false') {
|
|
596
|
+
try {
|
|
597
|
+
const redis = await getConnection(instanceId);
|
|
598
|
+
const lastIndexed = await redis.get(`cachly:index:last_indexed:${instanceId}`);
|
|
599
|
+
const staleMs = 24 * 60 * 60 * 1000;
|
|
600
|
+
const isStale = !lastIndexed || (Date.now() - parseInt(lastIndexed, 10)) > staleMs;
|
|
601
|
+
if (isStale) {
|
|
602
|
+
// Mark as indexing now to prevent concurrent re-runs.
|
|
603
|
+
await redis.set(`cachly:index:last_indexed:${instanceId}`, String(Date.now()), 'EX', 90000);
|
|
604
|
+
// Run in background — don't block the first tool call.
|
|
605
|
+
handleTool('index_project', {
|
|
606
|
+
instance_id: instanceId,
|
|
607
|
+
dir: process.cwd(),
|
|
608
|
+
max_files: 150,
|
|
609
|
+
ttl: 86400 * 7, // cache for 7 days
|
|
610
|
+
namespace: 'cachly:sem:code',
|
|
611
|
+
}).catch(() => undefined);
|
|
650
612
|
}
|
|
651
|
-
catch { /* never block the session on indexing errors */ }
|
|
652
613
|
}
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
return _autoSessionStarting;
|
|
614
|
+
catch { /* never block the session on indexing errors */ }
|
|
615
|
+
}
|
|
656
616
|
}
|
|
657
617
|
async function autoEndSession() {
|
|
658
618
|
if (!_autoSessionStarted || !_autoSessionInstanceId)
|
|
659
619
|
return;
|
|
660
620
|
_autoSessionStarted = false;
|
|
661
621
|
try {
|
|
662
|
-
// Try to derive a meaningful summary from git
|
|
663
|
-
let summary = `Auto-ended after ${_autoSessionToolCount} tool call(s).`;
|
|
664
|
-
try {
|
|
665
|
-
const { execSync } = await import('node:child_process');
|
|
666
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
|
|
667
|
-
const lastMsg = execSync('git log -1 --pretty=%s', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
|
|
668
|
-
if (lastMsg)
|
|
669
|
-
summary = `${branch ? `[${branch}] ` : ''}${lastMsg} · ${_autoSessionToolCount} tool calls`;
|
|
670
|
-
}
|
|
671
|
-
catch { /* git unavailable */ }
|
|
672
622
|
await handleTool('session_end', {
|
|
673
623
|
instance_id: _autoSessionInstanceId,
|
|
674
|
-
summary
|
|
624
|
+
summary: `Auto-ended after ${_autoSessionToolCount} tool call(s). Session was started automatically.`,
|
|
675
625
|
files_changed: [],
|
|
676
626
|
lessons_learned: 0,
|
|
677
627
|
});
|
|
@@ -680,7 +630,7 @@ async function autoEndSession() {
|
|
|
680
630
|
}
|
|
681
631
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
682
632
|
const { name, arguments: args } = request.params;
|
|
683
|
-
// Auto-start brain session
|
|
633
|
+
// Auto-start brain session on first tool call that has an instance_id.
|
|
684
634
|
// Skip session management tools to avoid recursion.
|
|
685
635
|
const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
|
|
686
636
|
if (!sessionTools.has(name) && !_autoSessionStarted && !_autoSessionStarting) {
|
|
@@ -699,16 +649,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
699
649
|
_autoSessionToolCount++;
|
|
700
650
|
try {
|
|
701
651
|
const text = await handleTool(name, (args ?? {}));
|
|
702
|
-
// ── Inject session context into the FIRST tool response ─────────────────
|
|
703
|
-
// This is the key: the AI sees the previous session summary, handoff tasks,
|
|
704
|
-
// and recent lessons automatically — without needing to call session_start.
|
|
705
|
-
// Only injected once per process (on the very first tool call).
|
|
706
|
-
if (!_autoSessionContextInjected && _autoSessionContext && !sessionTools.has(name)) {
|
|
707
|
-
_autoSessionContextInjected = true;
|
|
708
|
-
const ctx = _autoSessionContext;
|
|
709
|
-
_autoSessionContext = null; // free memory
|
|
710
|
-
return { content: [{ type: 'text', text: `${ctx}\n\n---\n\n${text}` }] };
|
|
711
|
-
}
|
|
712
652
|
return { content: [{ type: 'text', text }] };
|
|
713
653
|
}
|
|
714
654
|
catch (err) {
|
|
@@ -849,6 +789,368 @@ async function writeClaudeMd(projectDir, instanceId) {
|
|
|
849
789
|
await writeFile(claudeMdPath, block.trimStart(), 'utf-8');
|
|
850
790
|
return 'written';
|
|
851
791
|
}
|
|
792
|
+
// ── CLI: cachly digest ────────────────────────────────────────────────────────
|
|
793
|
+
// Usage: npx @cachly-dev/mcp-server@latest digest
|
|
794
|
+
// Weekly brain summary — what your AI learned this week. Shareable output.
|
|
795
|
+
if (process.argv[2] === 'digest') {
|
|
796
|
+
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
797
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
798
|
+
if (!apiKey || !instanceId) {
|
|
799
|
+
console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
800
|
+
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
process.stdout.write('\n🧠 Fetching your weekly Brain digest...\n\n');
|
|
804
|
+
try {
|
|
805
|
+
// Fetch current stats
|
|
806
|
+
const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
807
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
808
|
+
signal: AbortSignal.timeout(8000),
|
|
809
|
+
});
|
|
810
|
+
if (!statsRes.ok)
|
|
811
|
+
throw new Error(`stats HTTP ${statsRes.status}`);
|
|
812
|
+
const stats = await statsRes.json();
|
|
813
|
+
const lessons = stats.lesson_count ?? 0;
|
|
814
|
+
const recalls = stats.total_recall_count ?? 0;
|
|
815
|
+
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
816
|
+
const topLessons = (stats.top_lessons ?? []).slice(0, 5);
|
|
817
|
+
const teamAuthors = stats.team_authors ?? [];
|
|
818
|
+
const tokensSaved = recalls * 1200;
|
|
819
|
+
const costSaved = (tokensSaved * 0.000003).toFixed(2);
|
|
820
|
+
const level = lessons < 10 ? 'Junior Dev 🔧' : lessons < 30 ? 'Mid Dev ⚡' :
|
|
821
|
+
lessons < 60 ? 'Senior Dev 🧠' : lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
822
|
+
const now = new Date();
|
|
823
|
+
const weekStart = new Date(now);
|
|
824
|
+
weekStart.setDate(now.getDate() - 7);
|
|
825
|
+
const fmt = (d) => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
826
|
+
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
827
|
+
console.log(`│ \x1b[1m🧠 Brain Weekly Digest\x1b[0m \x1b[90m${fmt(weekStart)} – ${fmt(now)}\x1b[0m │`);
|
|
828
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
829
|
+
console.log(`│ Total lessons : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level : \x1b[32m${level.padEnd(18)}\x1b[0m│`);
|
|
830
|
+
console.log(`│ Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m │`);
|
|
831
|
+
console.log(`│ Tokens saved : \x1b[32m${String(Math.round(tokensSaved / 1000) + 'K').padEnd(8)}\x1b[0m Cost saved : \x1b[32m$${costSaved.padEnd(20)}\x1b[0m│`);
|
|
832
|
+
if (teamAuthors.length > 0) {
|
|
833
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
834
|
+
console.log(`│ \x1b[36m👥 Team contributors:\x1b[0m ${teamAuthors.slice(0, 5).join(', ').slice(0, 42).padEnd(42)} │`);
|
|
835
|
+
}
|
|
836
|
+
if (topLessons.length > 0) {
|
|
837
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
838
|
+
console.log('│ \x1b[33m🔥 Most recalled lessons this week:\x1b[0m │');
|
|
839
|
+
for (const l of topLessons) {
|
|
840
|
+
const line = `${l.topic}: ${l.what_worked}`.slice(0, 57);
|
|
841
|
+
console.log(`│ \x1b[90m• ${line.padEnd(58)}\x1b[0m│`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
845
|
+
console.log('│ \x1b[32m📋 Share this digest:\x1b[0m │');
|
|
846
|
+
console.log('│ \x1b[90m npx @cachly-dev/mcp-server@latest share\x1b[0m │');
|
|
847
|
+
console.log('│ \x1b[90m Add more: npx @cachly-dev/mcp-server@latest invite\x1b[0m │');
|
|
848
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
849
|
+
console.log('');
|
|
850
|
+
// Pro tip for cron
|
|
851
|
+
console.log(' \x1b[2m💡 Automate: add to crontab for a weekly team email\x1b[0m');
|
|
852
|
+
console.log(' \x1b[2m 0 9 * * 1 npx @cachly-dev/mcp-server@latest digest\x1b[0m');
|
|
853
|
+
console.log('');
|
|
854
|
+
}
|
|
855
|
+
catch (e) {
|
|
856
|
+
console.log(`\n❌ Could not fetch digest: ${e.message}\n`);
|
|
857
|
+
process.exit(1);
|
|
858
|
+
}
|
|
859
|
+
process.exit(0);
|
|
860
|
+
}
|
|
861
|
+
// ── CLI: cachly invite ────────────────────────────────────────────────────────
|
|
862
|
+
// Usage: npx @cachly-dev/mcp-server@latest invite [email]
|
|
863
|
+
// Invites a teammate to share your Brain. Fastest referral loop.
|
|
864
|
+
if (process.argv[2] === 'invite') {
|
|
865
|
+
const { createInterface } = await import('node:readline');
|
|
866
|
+
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
867
|
+
if (!apiKey) {
|
|
868
|
+
console.log('\n⚠️ CACHLY_JWT must be set.');
|
|
869
|
+
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
let email = process.argv[3] ?? '';
|
|
873
|
+
if (!email) {
|
|
874
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
875
|
+
email = await new Promise((resolve) => {
|
|
876
|
+
rl.question('\n 📬 Teammate email to invite: ', (ans) => { rl.close(); resolve(ans.trim()); });
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
if (!email || !email.includes('@')) {
|
|
880
|
+
console.log('\n❌ Invalid email address.\n');
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
console.log(`\n ⏳ Inviting ${email}...`);
|
|
884
|
+
try {
|
|
885
|
+
const res = await fetch(`${API_URL}/api/v1/team/invite`, {
|
|
886
|
+
method: 'POST',
|
|
887
|
+
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
888
|
+
body: JSON.stringify({ email, role: 'member', source: 'cli-invite' }),
|
|
889
|
+
signal: AbortSignal.timeout(8000),
|
|
890
|
+
});
|
|
891
|
+
if (res.ok) {
|
|
892
|
+
console.log(`\n ✅ Invite sent to \x1b[32m${email}\x1b[0m`);
|
|
893
|
+
console.log(` They'll get a link to join your Brain — one click, 30 seconds.\n`);
|
|
894
|
+
console.log(` 💡 Once they join, your AI assistants share lessons automatically.\n`);
|
|
895
|
+
}
|
|
896
|
+
else if (res.status === 409) {
|
|
897
|
+
console.log(`\n ✓ ${email} is already a team member.\n`);
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
const body = await res.json().catch(() => ({}));
|
|
901
|
+
console.log(`\n ❌ Invite failed: ${body.error ?? `HTTP ${res.status}`}\n`);
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch (e) {
|
|
906
|
+
console.log(`\n ❌ Network error: ${e.message}\n`);
|
|
907
|
+
process.exit(1);
|
|
908
|
+
}
|
|
909
|
+
process.exit(0);
|
|
910
|
+
}
|
|
911
|
+
// ── CLI: cachly demo ──────────────────────────────────────────────────────────
|
|
912
|
+
// Usage: npx @cachly-dev/mcp-server@latest demo
|
|
913
|
+
// Zero-signup. Reads local git history and shows what the AI Brain would know.
|
|
914
|
+
// The fastest path from "what is this?" to "I need this".
|
|
915
|
+
if (process.argv[2] === 'demo') {
|
|
916
|
+
const { execSync } = await import('node:child_process');
|
|
917
|
+
const { existsSync } = await import('node:fs');
|
|
918
|
+
const { resolve } = await import('node:path');
|
|
919
|
+
const cwd = process.cwd();
|
|
920
|
+
console.log('\n\x1b[35m🧠 cachly Brain — Live Demo\x1b[0m');
|
|
921
|
+
console.log('\x1b[90mNo account needed. Reading your git history...\x1b[0m\n');
|
|
922
|
+
// Verify git repo
|
|
923
|
+
if (!existsSync(resolve(cwd, '.git'))) {
|
|
924
|
+
console.log('⚠️ No git repository found here.');
|
|
925
|
+
console.log(' Run this command inside a project directory with git history.\n');
|
|
926
|
+
console.log(' Example: cd ~/my-project && npx @cachly-dev/mcp-server@latest demo\n');
|
|
927
|
+
process.exit(0);
|
|
928
|
+
}
|
|
929
|
+
// Read git log
|
|
930
|
+
let logOutput = '';
|
|
931
|
+
try {
|
|
932
|
+
logOutput = execSync('git log HEAD --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n 200', { cwd, encoding: 'utf-8', stdio: 'pipe' });
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
console.log('⚠️ Could not read git log. Make sure you have commits.\n');
|
|
936
|
+
process.exit(0);
|
|
937
|
+
}
|
|
938
|
+
const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
939
|
+
const [sha, subject, date, author] = line.split('|||');
|
|
940
|
+
return { sha: (sha ?? '').trim().slice(0, 8), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
|
|
941
|
+
});
|
|
942
|
+
if (commits.length === 0) {
|
|
943
|
+
console.log('⚠️ No commits found.\n');
|
|
944
|
+
process.exit(0);
|
|
945
|
+
}
|
|
946
|
+
// Classify commits (same logic as brain_from_git)
|
|
947
|
+
const classify = (s) => {
|
|
948
|
+
const t = s.toLowerCase();
|
|
949
|
+
if (/\b(fix|fixed|bug|hotfix|patch|revert|resolve)\b/.test(t))
|
|
950
|
+
return 'fix';
|
|
951
|
+
if (/\b(feat|feature|add|implement|new|introduce)\b/.test(t))
|
|
952
|
+
return 'feat';
|
|
953
|
+
if (/\b(perf|optim|speed|cache|latency)\b/.test(t))
|
|
954
|
+
return 'perf';
|
|
955
|
+
if (/\b(security|cve|auth|csrf|xss|inject|sanitize)\b/.test(t))
|
|
956
|
+
return 'security';
|
|
957
|
+
if (/\b(deploy|ci|cd|docker|k8s|infra)\b/.test(t))
|
|
958
|
+
return 'deploy';
|
|
959
|
+
if (/\b(refactor|clean|simplify|extract)\b/.test(t))
|
|
960
|
+
return 'refactor';
|
|
961
|
+
return 'chore';
|
|
962
|
+
};
|
|
963
|
+
// Count categories
|
|
964
|
+
const cats = new Map();
|
|
965
|
+
const fixes = [];
|
|
966
|
+
const feats = [];
|
|
967
|
+
const security = [];
|
|
968
|
+
const authors = new Set();
|
|
969
|
+
for (const c of commits) {
|
|
970
|
+
const cat = classify(c.subject);
|
|
971
|
+
cats.set(cat, (cats.get(cat) ?? 0) + 1);
|
|
972
|
+
if (cat === 'fix' && fixes.length < 5)
|
|
973
|
+
fixes.push(c.subject.slice(0, 72));
|
|
974
|
+
if (cat === 'feat' && feats.length < 5)
|
|
975
|
+
feats.push(c.subject.slice(0, 72));
|
|
976
|
+
if (cat === 'security' && security.length < 3)
|
|
977
|
+
security.push(c.subject.slice(0, 72));
|
|
978
|
+
if (c.author)
|
|
979
|
+
authors.add(c.author.split(' ')[0]);
|
|
980
|
+
}
|
|
981
|
+
const totalLessons = commits.length - (cats.get('chore') ?? 0);
|
|
982
|
+
const dateRange = `${commits[commits.length - 1]?.date ?? '?'} → ${commits[0]?.date ?? '?'}`;
|
|
983
|
+
// Display the demo
|
|
984
|
+
process.stdout.write('\x1b[2K\r');
|
|
985
|
+
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
986
|
+
console.log('│ \x1b[1m🧠 Brain Preview — What your AI would know\x1b[0m │');
|
|
987
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
988
|
+
console.log(`│ Commits analysed : \x1b[33m${String(commits.length).padEnd(6)}\x1b[0m Date range: \x1b[90m${dateRange.slice(0, 23).padEnd(23)}\x1b[0m │`);
|
|
989
|
+
console.log(`│ Lessons extracted: \x1b[32m${String(totalLessons).padEnd(6)}\x1b[0m Contributors: \x1b[36m${String(authors.size).padEnd(20)}\x1b[0m│`);
|
|
990
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
991
|
+
// Category breakdown
|
|
992
|
+
const sorted = [...cats.entries()].filter(([k]) => k !== 'chore').sort((a, b) => b[1] - a[1]);
|
|
993
|
+
const bar = (n, max) => '█'.repeat(Math.round((n / max) * 20)).padEnd(20);
|
|
994
|
+
const maxVal = Math.max(...sorted.map(([, v]) => v), 1);
|
|
995
|
+
console.log('│ \x1b[90mCategory breakdown:\x1b[0m │');
|
|
996
|
+
for (const [cat, count] of sorted) {
|
|
997
|
+
const emoji = { fix: '🔧', feat: '✨', perf: '⚡', security: '🔒', deploy: '🚀', refactor: '🔄' }[cat] ?? '•';
|
|
998
|
+
console.log(`│ ${emoji} \x1b[36m${cat.padEnd(10)}\x1b[0m \x1b[35m${bar(count, maxVal)}\x1b[0m \x1b[33m${String(count).padStart(3)}\x1b[0m │`);
|
|
999
|
+
}
|
|
1000
|
+
if (security.length > 0) {
|
|
1001
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1002
|
+
console.log('│ \x1b[31m🔒 Security fixes your AI would know:\x1b[0m │');
|
|
1003
|
+
for (const s of security)
|
|
1004
|
+
console.log(`│ \x1b[90m• ${s.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1005
|
+
}
|
|
1006
|
+
if (fixes.length > 0) {
|
|
1007
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1008
|
+
console.log('│ \x1b[33m🔧 Bug fixes your AI would remember:\x1b[0m │');
|
|
1009
|
+
for (const f of fixes.slice(0, 4))
|
|
1010
|
+
console.log(`│ \x1b[90m• ${f.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1011
|
+
}
|
|
1012
|
+
if (feats.length > 0) {
|
|
1013
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1014
|
+
console.log('│ \x1b[32m✨ Features & decisions it would recall:\x1b[0m │');
|
|
1015
|
+
for (const f of feats.slice(0, 3))
|
|
1016
|
+
console.log(`│ \x1b[90m• ${f.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1017
|
+
}
|
|
1018
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1019
|
+
console.log('│ \x1b[32m💡 With cachly, your AI arrives pre-briefed every session.\x1b[0m │');
|
|
1020
|
+
console.log('│ \x1b[32m No more re-explaining. No more repeated mistakes.\x1b[0m │');
|
|
1021
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1022
|
+
console.log('');
|
|
1023
|
+
console.log(' \x1b[1mMake this permanent (free, 30 seconds):\x1b[0m');
|
|
1024
|
+
console.log(' \x1b[32m$ npx @cachly-dev/mcp-server@latest setup\x1b[0m');
|
|
1025
|
+
console.log('');
|
|
1026
|
+
console.log(' Works with: Claude Code · Cursor · Windsurf · Copilot · Cline · Zed');
|
|
1027
|
+
console.log(' Free forever · GDPR · German servers · No credit card');
|
|
1028
|
+
console.log('');
|
|
1029
|
+
process.exit(0);
|
|
1030
|
+
}
|
|
1031
|
+
// ── CLI: cachly share ─────────────────────────────────────────────────────────
|
|
1032
|
+
// Usage: npx @cachly-dev/mcp-server@latest share
|
|
1033
|
+
// Generates a shareable ASCII card + tweet text with your Brain stats.
|
|
1034
|
+
if (process.argv[2] === 'share') {
|
|
1035
|
+
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1036
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1037
|
+
if (!apiKey || !instanceId) {
|
|
1038
|
+
console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1039
|
+
console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 30 seconds)\n');
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
process.stdout.write('\n🧠 Fetching your Brain stats...\n');
|
|
1043
|
+
try {
|
|
1044
|
+
const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
1045
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1046
|
+
signal: AbortSignal.timeout(8000),
|
|
1047
|
+
});
|
|
1048
|
+
if (!statsRes.ok)
|
|
1049
|
+
throw new Error(`HTTP ${statsRes.status}`);
|
|
1050
|
+
const stats = await statsRes.json();
|
|
1051
|
+
const lessons = stats.lesson_count ?? 0;
|
|
1052
|
+
const recalls = stats.total_recall_count ?? 0;
|
|
1053
|
+
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
1054
|
+
const tokensSaved = recalls * 1200;
|
|
1055
|
+
const costSaved = (tokensSaved * 0.000003).toFixed(2);
|
|
1056
|
+
const level = lessons === 0 ? 'Intern 🌱' :
|
|
1057
|
+
lessons < 10 ? 'Junior Dev 🔧' :
|
|
1058
|
+
lessons < 30 ? 'Mid Dev ⚡' :
|
|
1059
|
+
lessons < 60 ? 'Senior Dev 🧠' :
|
|
1060
|
+
lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
1061
|
+
console.log('');
|
|
1062
|
+
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
1063
|
+
console.log('│ 🧠 My AI Brain · powered by \x1b[35mcachly.dev\x1b[0m │');
|
|
1064
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1065
|
+
console.log(`│ Lessons stored : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level: \x1b[32m${level.padEnd(18)}\x1b[0m│`);
|
|
1066
|
+
console.log(`│ Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m │`);
|
|
1067
|
+
console.log(`│ Tokens saved : \x1b[32m${String((tokensSaved / 1000).toFixed(0) + 'K').padEnd(8)}\x1b[0m Cost saved: \x1b[32m$${costSaved.padEnd(20)}\x1b[0m│`);
|
|
1068
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1069
|
+
console.log('│ Works with: Claude Code · Cursor · Windsurf · Copilot │');
|
|
1070
|
+
console.log('│ Free · GDPR · German servers │');
|
|
1071
|
+
console.log('│ \x1b[35mnpx @cachly-dev/mcp-server@latest setup\x1b[0m │');
|
|
1072
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1073
|
+
console.log('');
|
|
1074
|
+
// Tweet text
|
|
1075
|
+
const tweet = `🧠 My AI coding assistant now has persistent memory.\n\n${lessons} lessons stored · ${recalls} recalls · ${level}\n\nFix a bug once → AI remembers forever. Across Claude Code, Cursor, Windsurf, Copilot.\n\nnpx @cachly-dev/mcp-server@latest setup\n\n#AIMemory #ClaudeCode #Cursor #DeveloperTools`;
|
|
1076
|
+
console.log(' \x1b[1m📋 Share on Twitter/X (copy this):\x1b[0m');
|
|
1077
|
+
console.log(' ─────────────────────────────────────');
|
|
1078
|
+
console.log(tweet.split('\n').map(l => ` ${l}`).join('\n'));
|
|
1079
|
+
console.log(' ─────────────────────────────────────');
|
|
1080
|
+
console.log('');
|
|
1081
|
+
console.log(' \x1b[2mPro tip: add a screenshot of your Brain panel for 3x more engagement\x1b[0m');
|
|
1082
|
+
console.log(' \x1b[2mAlso add a live badge to your README:\x1b[0m');
|
|
1083
|
+
console.log(` \x1b[90m npx @cachly-dev/mcp-server@latest badge\x1b[0m`);
|
|
1084
|
+
console.log('');
|
|
1085
|
+
}
|
|
1086
|
+
catch (e) {
|
|
1087
|
+
console.log(`\n❌ Could not fetch stats: ${e.message}\n`);
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
process.exit(0);
|
|
1091
|
+
}
|
|
1092
|
+
// ── CLI: cachly badge ─────────────────────────────────────────────────────────
|
|
1093
|
+
// Outputs the Markdown + HTML snippet for embedding a live Brain lesson-count
|
|
1094
|
+
// badge in any README or website. Badge SVG served by cachly API (public, no auth).
|
|
1095
|
+
if (process.argv[2] === 'badge') {
|
|
1096
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1097
|
+
if (!instanceId) {
|
|
1098
|
+
console.log('\n⚠️ CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1099
|
+
console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 30 seconds)\n');
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
const badgeUrl = `${API_URL}/api/v1/badge/${instanceId}`;
|
|
1103
|
+
const targetUrl = 'https://cachly.dev';
|
|
1104
|
+
const markdown = `[](${targetUrl})`;
|
|
1105
|
+
const htmlBadge = `<a href="${targetUrl}"><img src="${badgeUrl}" alt="cachly Brain" /></a>`;
|
|
1106
|
+
console.log('');
|
|
1107
|
+
console.log('\x1b[35m ╔═══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1108
|
+
console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly Brain Badge\x1b[0m \x1b[35m║\x1b[0m');
|
|
1109
|
+
console.log('\x1b[35m ╚═══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1110
|
+
console.log('');
|
|
1111
|
+
console.log(' Paste into your README.md:');
|
|
1112
|
+
console.log('');
|
|
1113
|
+
console.log(` \x1b[32m${markdown}\x1b[0m`);
|
|
1114
|
+
console.log('');
|
|
1115
|
+
console.log(' ─────────────────────────────────────────────────────');
|
|
1116
|
+
console.log(' Or paste into HTML:');
|
|
1117
|
+
console.log('');
|
|
1118
|
+
console.log(` \x1b[36m${htmlBadge}\x1b[0m`);
|
|
1119
|
+
console.log('');
|
|
1120
|
+
console.log(' ─────────────────────────────────────────────────────');
|
|
1121
|
+
console.log(' Badge URL (live, updates every hour):');
|
|
1122
|
+
console.log(` \x1b[90m${badgeUrl}\x1b[0m`);
|
|
1123
|
+
console.log('');
|
|
1124
|
+
console.log(' \x1b[2mThe badge shows your current lesson count — no auth required,');
|
|
1125
|
+
console.log(' safe to embed in public repos. Updates hourly.\x1b[0m');
|
|
1126
|
+
console.log('');
|
|
1127
|
+
process.exit(0);
|
|
1128
|
+
}
|
|
1129
|
+
// ── CLI: no-args splash screen ────────────────────────────────────────────────
|
|
1130
|
+
// When run with no recognized subcommand, show a compelling pitch + help.
|
|
1131
|
+
if (!process.argv[2] && process.stdout.isTTY) {
|
|
1132
|
+
console.log('');
|
|
1133
|
+
console.log('\x1b[35m ╔═══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1134
|
+
console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly — Persistent AI Memory for Developers\x1b[0m \x1b[35m║\x1b[0m');
|
|
1135
|
+
console.log('\x1b[35m ╚═══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1136
|
+
console.log('');
|
|
1137
|
+
console.log(' Your AI forgets everything when a session ends.');
|
|
1138
|
+
console.log(' cachly gives it a brain that survives forever.');
|
|
1139
|
+
console.log('');
|
|
1140
|
+
console.log(' \x1b[1mCommands:\x1b[0m');
|
|
1141
|
+
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest demo\x1b[0m ← Start here (no account)');
|
|
1142
|
+
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m ← Wire up your AI editors');
|
|
1143
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest health\x1b[0m ← Check everything works');
|
|
1144
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest digest\x1b[0m ← Weekly Brain summary');
|
|
1145
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest share\x1b[0m ← Share your Brain stats');
|
|
1146
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest badge\x1b[0m ← README badge for your Brain');
|
|
1147
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest invite\x1b[0m ← Invite a teammate');
|
|
1148
|
+
console.log('');
|
|
1149
|
+
console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
|
|
1150
|
+
console.log(' \x1b[90mFree forever · GDPR · German servers · 89 MCP tools\x1b[0m');
|
|
1151
|
+
console.log('');
|
|
1152
|
+
process.exit(0);
|
|
1153
|
+
}
|
|
852
1154
|
// ── CLI: cachly init ──────────────────────────────────────────────────────────
|
|
853
1155
|
// Usage: npx @cachly-dev/mcp-server init --instance-id <id> --api-key <key> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]
|
|
854
1156
|
if (process.argv[2] === 'init') {
|
|
@@ -920,6 +1222,164 @@ if (process.argv[2] === 'init') {
|
|
|
920
1222
|
console.log(` Restart your editor — the \`cachly\` MCP tools will appear.\n`);
|
|
921
1223
|
process.exit(0);
|
|
922
1224
|
}
|
|
1225
|
+
// ── CLI: cachly health ────────────────────────────────────────────────────────
|
|
1226
|
+
// Usage: npx @cachly-dev/mcp-server@latest health
|
|
1227
|
+
// Checks: JWT valid, Brain API reachable, editor configs found, git hook present.
|
|
1228
|
+
if (process.argv[2] === 'health') {
|
|
1229
|
+
const { existsSync } = await import('node:fs');
|
|
1230
|
+
const { readFile } = await import('node:fs/promises');
|
|
1231
|
+
const { resolve, join: pJoin } = await import('node:path');
|
|
1232
|
+
const cwd = process.cwd();
|
|
1233
|
+
let passed = 0;
|
|
1234
|
+
let failed = 0;
|
|
1235
|
+
const ok = (msg) => { console.log(` ✅ ${msg}`); passed++; };
|
|
1236
|
+
const warn = (msg) => { console.log(` ⚠️ ${msg}`); };
|
|
1237
|
+
const fail = (msg) => { console.log(` ❌ ${msg}`); failed++; };
|
|
1238
|
+
console.log('\n🧠 cachly health check\n');
|
|
1239
|
+
// ── 1. JWT token ────────────────────────────────────────────────────────────
|
|
1240
|
+
console.log('🔑 Auth token');
|
|
1241
|
+
const jwt = process.env.CACHLY_JWT ?? '';
|
|
1242
|
+
if (!jwt) {
|
|
1243
|
+
fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
try {
|
|
1247
|
+
const parts = jwt.split('.');
|
|
1248
|
+
if (parts.length !== 3)
|
|
1249
|
+
throw new Error('not a JWT');
|
|
1250
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
1251
|
+
const expMs = payload.exp ? payload.exp * 1000 : null;
|
|
1252
|
+
const minsLeft = expMs ? Math.floor((expMs - Date.now()) / 60_000) : null;
|
|
1253
|
+
if (expMs && expMs < Date.now()) {
|
|
1254
|
+
fail(`JWT expired ${Math.abs(minsLeft)} minute(s) ago — get a new one: https://cachly.dev/setup-ai`);
|
|
1255
|
+
}
|
|
1256
|
+
else if (minsLeft !== null && minsLeft < 30) {
|
|
1257
|
+
warn(`JWT expires in ${minsLeft} minute(s) — refresh soon at https://cachly.dev/setup-ai`);
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
catch {
|
|
1264
|
+
fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
// ── 2. Brain API reachability ───────────────────────────────────────────────
|
|
1268
|
+
console.log('\n🌐 Brain API');
|
|
1269
|
+
const apiUrl = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
1270
|
+
try {
|
|
1271
|
+
const res = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(5000) });
|
|
1272
|
+
if (res.ok) {
|
|
1273
|
+
const body = await res.json();
|
|
1274
|
+
ok(`${apiUrl} reachable (status: ${body.status ?? 'ok'})`);
|
|
1275
|
+
}
|
|
1276
|
+
else if (res.status === 401 || res.status === 403) {
|
|
1277
|
+
ok(`${apiUrl} reachable (auth required — expected)`);
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
fail(`${apiUrl}/health returned HTTP ${res.status}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
catch (e) {
|
|
1284
|
+
fail(`${apiUrl} unreachable — ${e.message}`);
|
|
1285
|
+
}
|
|
1286
|
+
// ── 3. Brain instance ───────────────────────────────────────────────────────
|
|
1287
|
+
console.log('\n🧠 Brain instance');
|
|
1288
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1289
|
+
if (!instanceId) {
|
|
1290
|
+
warn('CACHLY_BRAIN_INSTANCE_ID not set — will auto-resolve on first tool call');
|
|
1291
|
+
}
|
|
1292
|
+
else if (jwt) {
|
|
1293
|
+
try {
|
|
1294
|
+
const instRes = await fetch(`${apiUrl}/api/v1/instances/${instanceId}`, {
|
|
1295
|
+
headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' },
|
|
1296
|
+
signal: AbortSignal.timeout(5000),
|
|
1297
|
+
});
|
|
1298
|
+
if (instRes.ok) {
|
|
1299
|
+
const inst = await instRes.json();
|
|
1300
|
+
if (inst.status === 'running') {
|
|
1301
|
+
ok(`Instance "${inst.name}" (${instanceId.slice(0, 8)}…) is running`);
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
warn(`Instance "${inst.name}" status: ${inst.status}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
fail(`Instance ${instanceId.slice(0, 8)}… not found (HTTP ${instRes.status})`);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
catch (e) {
|
|
1312
|
+
fail(`Could not check instance — ${e.message}`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// ── 4. Editor configs ───────────────────────────────────────────────────────
|
|
1316
|
+
console.log('\n🛠 Editor configs (project dir)');
|
|
1317
|
+
const editorConfigs = [
|
|
1318
|
+
{ editor: 'Claude Code', path: '.mcp.json' },
|
|
1319
|
+
{ editor: 'Cursor', path: '.cursor/mcp.json' },
|
|
1320
|
+
{ editor: 'Windsurf', path: '.windsurf/mcp.json' },
|
|
1321
|
+
{ editor: 'Copilot / Cline', path: '.vscode/mcp.json' },
|
|
1322
|
+
{ editor: 'Zed', path: '.zed/settings.json' },
|
|
1323
|
+
{ editor: 'Continue.dev', path: '.continue/config.json' },
|
|
1324
|
+
];
|
|
1325
|
+
let configsFound = 0;
|
|
1326
|
+
for (const { editor, path } of editorConfigs) {
|
|
1327
|
+
const fullPath = resolve(cwd, path);
|
|
1328
|
+
if (existsSync(fullPath)) {
|
|
1329
|
+
try {
|
|
1330
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
1331
|
+
const hasCachly = content.includes('@cachly-dev/mcp-server') || content.includes('cachly');
|
|
1332
|
+
if (hasCachly) {
|
|
1333
|
+
ok(`${editor}: ${path}`);
|
|
1334
|
+
configsFound++;
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
warn(`${editor}: ${path} exists but cachly not found inside`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
catch {
|
|
1341
|
+
warn(`${editor}: ${path} exists but could not read`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (configsFound === 0) {
|
|
1346
|
+
fail('No editor MCP configs found — run: npx @cachly-dev/mcp-server@latest setup');
|
|
1347
|
+
}
|
|
1348
|
+
// ── 5. Git hook ─────────────────────────────────────────────────────────────
|
|
1349
|
+
console.log('\n🪝 Git hook (ambient learning)');
|
|
1350
|
+
const hookPath = resolve(cwd, '.git', 'hooks', 'post-commit');
|
|
1351
|
+
if (!existsSync(resolve(cwd, '.git'))) {
|
|
1352
|
+
warn('Not in a git repository — skip');
|
|
1353
|
+
}
|
|
1354
|
+
else if (existsSync(hookPath)) {
|
|
1355
|
+
try {
|
|
1356
|
+
const hook = await readFile(hookPath, 'utf-8');
|
|
1357
|
+
if (hook.includes('cachly')) {
|
|
1358
|
+
ok('.git/hooks/post-commit (cachly CLS hook present)');
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
warn('.git/hooks/post-commit exists but cachly hook not found — run setup to add it');
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
catch {
|
|
1365
|
+
warn('.git/hooks/post-commit exists but could not read');
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
else {
|
|
1369
|
+
warn('.git/hooks/post-commit not found — run: npx @cachly-dev/mcp-server@latest setup');
|
|
1370
|
+
}
|
|
1371
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
1372
|
+
console.log(`\n${'─'.repeat(40)}`);
|
|
1373
|
+
if (failed === 0) {
|
|
1374
|
+
console.log(`✅ All checks passed (${passed} ok)\n`);
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
console.log(`❌ ${failed} check(s) failed, ${passed} passed\n`);
|
|
1378
|
+
console.log(`💡 Fix issues with: npx @cachly-dev/mcp-server@latest setup\n`);
|
|
1379
|
+
process.exit(1);
|
|
1380
|
+
}
|
|
1381
|
+
process.exit(0);
|
|
1382
|
+
}
|
|
923
1383
|
// ── CLI: cachly setup (interactive — no flags required) ───────────────────────
|
|
924
1384
|
// Usage: npx @cachly-dev/mcp-server setup
|
|
925
1385
|
if (process.argv[2] === 'setup') {
|
|
@@ -1397,7 +1857,9 @@ if (process.argv[2] === 'index') {
|
|
|
1397
1857
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
1398
1858
|
// Warn on stderr when credentials are missing so the user sees a clear
|
|
1399
1859
|
// actionable message in their editor's MCP log instead of silent failures.
|
|
1400
|
-
|
|
1860
|
+
// Skip for CLI commands that intentionally run without credentials.
|
|
1861
|
+
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge'];
|
|
1862
|
+
if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
1401
1863
|
process.stderr.write('\n' +
|
|
1402
1864
|
'╔══════════════════════════════════════════════════════════════════╗\n' +
|
|
1403
1865
|
'║ 🧠 cachly AI Brain — Setup required ║\n' +
|