@cachly-dev/mcp-server 0.10.24 → 0.10.26
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/CHANGELOG.md +0 -131
- package/README.md +112 -136
- package/dist/__tests__/brain-predict-failures.test.d.ts +17 -0
- package/dist/__tests__/brain-predict-failures.test.js +339 -0
- package/dist/__tests__/brain-predict-failures.test.js.map +1 -0
- package/dist/handlers/brain.js +67 -7
- package/dist/handlers/brain.js.map +1 -1
- package/dist/handlers/fedbrain.js +44 -93
- package/dist/handlers/fedbrain.js.map +1 -1
- package/dist/handlers/roadmap.js +2 -8
- package/dist/handlers/roadmap.js.map +1 -1
- package/dist/handlers/syndicate.js +7 -15
- package/dist/handlers/syndicate.js.map +1 -1
- package/dist/handlers/team.js +50 -62
- package/dist/handlers/team.js.map +1 -1
- package/dist/index.js +223 -966
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +54 -3
- package/dist/tools.js +62 -8
- package/dist/tools.js.map +1 -1
- package/package.json +13 -29
- package/scripts/postinstall.js +12 -12
- package/server.json +9 -3
- package/smithery.yaml +8 -15
- package/src/__tests__/brain-predict-failures.test.ts +430 -0
- package/src/handlers/brain.ts +64 -7
- package/src/handlers/fedbrain.ts +38 -88
- package/src/handlers/roadmap.ts +2 -7
- package/src/handlers/syndicate.ts +7 -13
- package/src/handlers/team.ts +33 -57
- package/src/index.ts +245 -995
- package/src/tools.ts +66 -8
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/workflows/publish.yml +0 -72
- package/LAUNCH_TEXTS.md +0 -182
- package/LICENSE +0 -202
- package/dist/__tests__/auth.test.d.ts +0 -1
- package/dist/__tests__/auth.test.js +0 -107
- package/dist/__tests__/auth.test.js.map +0 -1
- package/glama.json +0 -36
- package/llms.txt +0 -155
- package/src/__tests__/auth.test.ts +0 -147
package/src/index.ts
CHANGED
|
@@ -53,7 +53,7 @@ import { Redis } from 'ioredis';
|
|
|
53
53
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
54
54
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
55
55
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
56
|
-
const CURRENT_VERSION = '0.10.
|
|
56
|
+
const CURRENT_VERSION = '0.10.10';
|
|
57
57
|
|
|
58
58
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
59
59
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
@@ -159,7 +159,6 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
159
159
|
JWT = apiKey;
|
|
160
160
|
setEmbedJwt(apiKey); // keep embeddings.ts in sync
|
|
161
161
|
_deviceFlow = null;
|
|
162
|
-
sendFunnelEvent('device_flow_completed');
|
|
163
162
|
// Auto-provision: find or create the user's brain instance.
|
|
164
163
|
_defaultInstanceLastAttempt = 0;
|
|
165
164
|
await resolveDefaultInstanceId();
|
|
@@ -178,16 +177,8 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
178
177
|
};
|
|
179
178
|
const id = body.instance?.id ?? body.instance_id;
|
|
180
179
|
if (id) _defaultInstanceId = id;
|
|
181
|
-
} else {
|
|
182
|
-
// Log so telemetry can detect free-tier auto-provision failures
|
|
183
|
-
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
184
|
-
method: 'POST',
|
|
185
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
186
|
-
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
187
|
-
signal: AbortSignal.timeout(3000),
|
|
188
|
-
}).catch(() => {});
|
|
189
180
|
}
|
|
190
|
-
} catch { /* non-fatal
|
|
181
|
+
} catch { /* non-fatal */ }
|
|
191
182
|
}
|
|
192
183
|
// Give a provisioning instance a head-start so getConnection's wait is shorter.
|
|
193
184
|
// The instance is almost always ready by the time the user's tool re-enters.
|
|
@@ -298,24 +289,11 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
298
289
|
body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
|
|
299
290
|
signal: AbortSignal.timeout(3000),
|
|
300
291
|
}).catch(() => {});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
} else if (inst.status === 'failed') {
|
|
305
|
-
hint = `❌ Brain instance "${inst.name}" failed to start.\n\n` +
|
|
306
|
-
`Our system will retry automatically. Check status at: https://cachly.dev/instances\n` +
|
|
307
|
-
`If this persists, contact support@cachly.dev.`;
|
|
308
|
-
} else if (inst.status === 'suspended') {
|
|
309
|
-
hint = `⏸ Brain instance "${inst.name}" is suspended (billing issue).\n\n` +
|
|
310
|
-
`Update your payment method at: https://cachly.dev/billing`;
|
|
311
|
-
} else if (inst.status === 'pending_payment') {
|
|
312
|
-
hint = `💳 Brain instance "${inst.name}" is waiting for payment.\n\n` +
|
|
313
|
-
`Complete your checkout at: https://cachly.dev/instances`;
|
|
314
|
-
} else {
|
|
315
|
-
hint = `Brain instance "${inst.name}" is not reachable (status: ${inst.status}).\n\n` +
|
|
292
|
+
const hint = inst.status === 'provisioning'
|
|
293
|
+
? `⏳ Brain instance "${inst.name}" is still starting up.\n\nThis usually finishes within 30 seconds. Please try again in a moment.`
|
|
294
|
+
: `Brain instance "${inst.name}" is not reachable (status: ${inst.status}).\n\n` +
|
|
316
295
|
`• View your instance at: https://cachly.dev/instances\n` +
|
|
317
296
|
`• Run \`get_api_status\` for a full diagnostic.`;
|
|
318
|
-
}
|
|
319
297
|
throw new McpError(ErrorCode.InvalidRequest, hint);
|
|
320
298
|
}
|
|
321
299
|
if (!inst.host || !inst.port) {
|
|
@@ -332,15 +310,8 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
332
310
|
);
|
|
333
311
|
password = conn.password ?? undefined;
|
|
334
312
|
tlsEnabled = conn.tls_enabled !== false;
|
|
335
|
-
} catch
|
|
336
|
-
//
|
|
337
|
-
// Surface the error so the user knows why Redis auth will fail rather than getting a cryptic NOAUTH.
|
|
338
|
-
const msg = connErr instanceof Error ? connErr.message : String(connErr);
|
|
339
|
-
if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
|
|
340
|
-
throw new McpError(ErrorCode.InvalidRequest,
|
|
341
|
-
`API key rejected — run \`cachly setup\` to refresh your credentials.`);
|
|
342
|
-
}
|
|
343
|
-
// Other failures (timeout, 500): proceed without password, Redis will surface NOAUTH if needed
|
|
313
|
+
} catch {
|
|
314
|
+
// Fallback: use TLS flag from instance metadata
|
|
344
315
|
}
|
|
345
316
|
|
|
346
317
|
const client = new Redis({
|
|
@@ -429,40 +400,21 @@ import { TOOLS } from './tools.js';
|
|
|
429
400
|
|
|
430
401
|
// Fires once per process when no JWT is set (anonymous, opt-out via CACHLY_NO_TELEMETRY=1)
|
|
431
402
|
let _telemetryPingSent = false;
|
|
432
|
-
// Fires once per process on the first successful Brain tool call — key activation metric
|
|
433
|
-
let _firstCallSuccessSent = false;
|
|
434
|
-
|
|
435
|
-
function detectEditor(): string {
|
|
436
|
-
return process.env.CURSOR_TRACE_ID ? 'cursor'
|
|
437
|
-
: process.env.WINDSURF_SESSION_ID ? 'windsurf'
|
|
438
|
-
: process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
|
|
439
|
-
: process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
|
|
440
|
-
: 'unknown';
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function sendFunnelEvent(event: string, extra?: Record<string, unknown>): void {
|
|
444
|
-
if (process.env.CACHLY_NO_TELEMETRY === '1') return;
|
|
445
|
-
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
446
|
-
method: 'POST',
|
|
447
|
-
headers: { 'Content-Type': 'application/json' },
|
|
448
|
-
body: JSON.stringify({
|
|
449
|
-
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
450
|
-
...(JWT ? { jwt: JWT } : {}),
|
|
451
|
-
...extra,
|
|
452
|
-
}),
|
|
453
|
-
signal: AbortSignal.timeout(3000),
|
|
454
|
-
}).catch(() => {/* fire-and-forget */});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
403
|
async function sendAnonymousTelemetry(toolName: string): Promise<void> {
|
|
458
404
|
if (_telemetryPingSent) return;
|
|
459
405
|
if (process.env.CACHLY_NO_TELEMETRY === '1') return;
|
|
460
406
|
_telemetryPingSent = true;
|
|
407
|
+
// Detect editor from common env vars injected by IDE extensions
|
|
408
|
+
const editor = process.env.CURSOR_TRACE_ID ? 'cursor'
|
|
409
|
+
: process.env.WINDSURF_SESSION_ID ? 'windsurf'
|
|
410
|
+
: process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
|
|
411
|
+
: process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
|
|
412
|
+
: 'unknown';
|
|
461
413
|
try {
|
|
462
414
|
await fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
463
415
|
method: 'POST',
|
|
464
416
|
headers: { 'Content-Type': 'application/json' },
|
|
465
|
-
body: JSON.stringify({ event: 'first_call_no_jwt', version: CURRENT_VERSION, editor
|
|
417
|
+
body: JSON.stringify({ event: 'first_call_no_jwt', version: CURRENT_VERSION, editor, tool: toolName }),
|
|
466
418
|
signal: AbortSignal.timeout(3000),
|
|
467
419
|
});
|
|
468
420
|
} catch { /* fire-and-forget, never block the user */ }
|
|
@@ -501,28 +453,18 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
501
453
|
const flow = await startDeviceFlow();
|
|
502
454
|
if (flow) {
|
|
503
455
|
_deviceFlow = flow;
|
|
504
|
-
sendFunnelEvent('device_flow_started', { tool: name });
|
|
505
|
-
// Try to open the browser automatically — fire-and-forget, never block
|
|
506
|
-
void (async () => {
|
|
507
|
-
try {
|
|
508
|
-
const { exec } = await import('node:child_process');
|
|
509
|
-
const url = flow.verifyUrl;
|
|
510
|
-
const cmd = process.platform === 'win32' ? `start "" "${url}"`
|
|
511
|
-
: process.platform === 'darwin' ? `open "${url}"`
|
|
512
|
-
: `xdg-open "${url}"`;
|
|
513
|
-
exec(cmd);
|
|
514
|
-
} catch { /* non-critical */ }
|
|
515
|
-
})();
|
|
516
456
|
return [
|
|
517
|
-
'🧠 **cachly AI Brain — sign in
|
|
457
|
+
'🧠 **cachly AI Brain — One-click sign in**',
|
|
518
458
|
'',
|
|
519
|
-
|
|
459
|
+
'1. Open this URL in your browser (it may open automatically):',
|
|
460
|
+
` **${flow.verifyUrl}**`,
|
|
520
461
|
'',
|
|
521
|
-
`
|
|
462
|
+
`2. Enter this code if prompted: **${flow.userCode}**`,
|
|
522
463
|
'',
|
|
523
|
-
'After sign-in
|
|
464
|
+
'3. After sign-in, call this tool again — it will proceed automatically.',
|
|
524
465
|
'',
|
|
525
|
-
'✨ Free
|
|
466
|
+
'✨ Free tier includes: 1 Brain instance, persistent memory, 63 MCP tools.',
|
|
467
|
+
' No credit card required.',
|
|
526
468
|
].join('\n');
|
|
527
469
|
}
|
|
528
470
|
|
|
@@ -549,23 +491,11 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
549
491
|
|
|
550
492
|
// Delegate brain tools (learn, recall, session, etc.)
|
|
551
493
|
const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
552
|
-
if (brainResult !== null)
|
|
553
|
-
if (!_firstCallSuccessSent && JWT) {
|
|
554
|
-
_firstCallSuccessSent = true;
|
|
555
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
556
|
-
}
|
|
557
|
-
return brainResult;
|
|
558
|
-
}
|
|
494
|
+
if (brainResult !== null) return brainResult;
|
|
559
495
|
|
|
560
496
|
// Delegate context tools (remember/recall/list/forget)
|
|
561
497
|
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
562
|
-
if (contextResult !== null)
|
|
563
|
-
if (!_firstCallSuccessSent && JWT) {
|
|
564
|
-
_firstCallSuccessSent = true;
|
|
565
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
566
|
-
}
|
|
567
|
-
return contextResult;
|
|
568
|
-
}
|
|
498
|
+
if (contextResult !== null) return contextResult;
|
|
569
499
|
|
|
570
500
|
// Delegate instance + cache tools
|
|
571
501
|
const instanceResult = await handleInstanceTool(name, args, getConnection, apiFetch);
|
|
@@ -705,7 +635,33 @@ const server = new Server(
|
|
|
705
635
|
{ capabilities: { tools: {} } }
|
|
706
636
|
);
|
|
707
637
|
|
|
708
|
-
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
638
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
639
|
+
if (!JWT) {
|
|
640
|
+
// No credentials — inject sign-up CTA into session_start description
|
|
641
|
+
const tools = TOOLS.map(t => {
|
|
642
|
+
if (t.name !== 'session_start') return t;
|
|
643
|
+
return {
|
|
644
|
+
...t,
|
|
645
|
+
description:
|
|
646
|
+
`⚡ FREE AI BRAIN — sign up once at https://cachly.dev/sign-up (no credit card) ` +
|
|
647
|
+
`then run: npx @cachly-dev/mcp-server@latest setup\n\n` + t.description,
|
|
648
|
+
};
|
|
649
|
+
});
|
|
650
|
+
return { tools };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ── Auto-start session on connection (fire and forget) ────────────────────
|
|
654
|
+
// When the AI reads the tools list, it has just connected. Proactively start
|
|
655
|
+
// the Brain session in the background so by the time the first tool call
|
|
656
|
+
// arrives, session context is already loaded and ready to inject.
|
|
657
|
+
if (!_autoSessionStarted && !_autoSessionStarting) {
|
|
658
|
+
void resolveDefaultInstanceId().then(instanceId => {
|
|
659
|
+
if (instanceId) void autoStartSession(instanceId).catch(() => undefined);
|
|
660
|
+
}).catch(() => undefined);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return { tools: TOOLS };
|
|
664
|
+
});
|
|
709
665
|
|
|
710
666
|
// ── Auto-session management ───────────────────────────────────────────────────
|
|
711
667
|
// Transparently starts a Brain session on the first tool call of a process
|
|
@@ -714,59 +670,91 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
|
714
670
|
|
|
715
671
|
let _autoSessionInstanceId: string | null = null;
|
|
716
672
|
let _autoSessionStarted = false;
|
|
717
|
-
let _autoSessionStarting: Promise<
|
|
673
|
+
let _autoSessionStarting: Promise<string | null> | null = null; // guard against double-start race
|
|
718
674
|
let _autoSessionToolCount = 0;
|
|
675
|
+
let _autoSessionContext: string | null = null; // result of session_start, injected into first tool response
|
|
676
|
+
let _autoSessionContextInjected = false; // only inject once
|
|
719
677
|
|
|
720
678
|
// ── brain_from_git concurrency limiter ────────────────────────────────────────
|
|
721
679
|
// Spawning many `git log` subprocesses in parallel can fork-bomb the host.
|
|
722
680
|
// This simple semaphore caps concurrent brain_from_git calls to 10 per process.
|
|
723
|
-
async function autoStartSession(instanceId: string): Promise<
|
|
724
|
-
if (_autoSessionStarted) return;
|
|
681
|
+
async function autoStartSession(instanceId: string): Promise<string | null> {
|
|
682
|
+
if (_autoSessionStarted) return null;
|
|
725
683
|
// If another call already started the session, await it instead of double-starting.
|
|
726
|
-
if (_autoSessionStarting) {
|
|
727
|
-
_autoSessionStarting = (async () => {
|
|
684
|
+
if (_autoSessionStarting) { return _autoSessionStarting; }
|
|
685
|
+
_autoSessionStarting = (async (): Promise<string | null> => {
|
|
728
686
|
_autoSessionStarted = true;
|
|
729
687
|
_autoSessionInstanceId = instanceId;
|
|
688
|
+
let sessionResult: string | null = null;
|
|
730
689
|
try {
|
|
731
|
-
|
|
690
|
+
// Auto-detect git context for a meaningful session focus
|
|
691
|
+
let focus = 'auto (MCP session)';
|
|
692
|
+
try {
|
|
693
|
+
const { execSync } = await import('node:child_process');
|
|
694
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD',
|
|
695
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }
|
|
696
|
+
).trim();
|
|
697
|
+
const lastMsg = execSync('git log -1 --pretty=%s',
|
|
698
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }
|
|
699
|
+
).trim();
|
|
700
|
+
if (branch && branch !== 'HEAD') {
|
|
701
|
+
focus = lastMsg ? `${branch}: ${lastMsg.slice(0, 80)}` : `branch: ${branch}`;
|
|
702
|
+
}
|
|
703
|
+
} catch { /* git not available in this workspace */ }
|
|
704
|
+
sessionResult = await handleTool('session_start', { instance_id: instanceId, focus });
|
|
705
|
+
_autoSessionContext = sessionResult;
|
|
732
706
|
} catch { /* non-fatal — session tracking is a best-effort feature */ } finally {
|
|
733
707
|
_autoSessionStarting = null;
|
|
734
708
|
}
|
|
735
|
-
})();
|
|
736
|
-
await _autoSessionStarting;
|
|
737
709
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
710
|
+
// Auto-index the project if it hasn't been indexed in the last 24h.
|
|
711
|
+
// This is the main lever for growing token savings: more indexed code →
|
|
712
|
+
// more semantic cache hits → fewer LLM calls for repeated questions.
|
|
713
|
+
if (process.env.CACHLY_AUTO_INDEX !== 'false') {
|
|
714
|
+
try {
|
|
715
|
+
const redis = await getConnection(instanceId);
|
|
716
|
+
const lastIndexed = await redis.get(`cachly:index:last_indexed:${instanceId}`);
|
|
717
|
+
const staleMs = 24 * 60 * 60 * 1000;
|
|
718
|
+
const isStale = !lastIndexed || (Date.now() - parseInt(lastIndexed, 10)) > staleMs;
|
|
719
|
+
if (isStale) {
|
|
720
|
+
// Mark as indexing now to prevent concurrent re-runs.
|
|
721
|
+
await redis.set(`cachly:index:last_indexed:${instanceId}`, String(Date.now()), 'EX', 90000);
|
|
722
|
+
// Run in background — don't block the first tool call.
|
|
723
|
+
handleTool('index_project', {
|
|
724
|
+
instance_id: instanceId,
|
|
725
|
+
dir: process.cwd(),
|
|
726
|
+
max_files: 150,
|
|
727
|
+
ttl: 86400 * 7, // cache for 7 days
|
|
728
|
+
namespace: 'cachly:sem:code',
|
|
729
|
+
}).catch(() => undefined);
|
|
730
|
+
}
|
|
731
|
+
} catch { /* never block the session on indexing errors */ }
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return sessionResult;
|
|
735
|
+
})();
|
|
736
|
+
return _autoSessionStarting;
|
|
761
737
|
}
|
|
762
738
|
|
|
763
739
|
async function autoEndSession(): Promise<void> {
|
|
764
740
|
if (!_autoSessionStarted || !_autoSessionInstanceId) return;
|
|
765
741
|
_autoSessionStarted = false;
|
|
766
742
|
try {
|
|
743
|
+
// Try to derive a meaningful summary from git
|
|
744
|
+
let summary = `Auto-ended after ${_autoSessionToolCount} tool call(s).`;
|
|
745
|
+
try {
|
|
746
|
+
const { execSync } = await import('node:child_process');
|
|
747
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD',
|
|
748
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }
|
|
749
|
+
).trim();
|
|
750
|
+
const lastMsg = execSync('git log -1 --pretty=%s',
|
|
751
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }
|
|
752
|
+
).trim();
|
|
753
|
+
if (lastMsg) summary = `${branch ? `[${branch}] ` : ''}${lastMsg} · ${_autoSessionToolCount} tool calls`;
|
|
754
|
+
} catch { /* git unavailable */ }
|
|
767
755
|
await handleTool('session_end', {
|
|
768
756
|
instance_id: _autoSessionInstanceId,
|
|
769
|
-
summary
|
|
757
|
+
summary,
|
|
770
758
|
files_changed: [],
|
|
771
759
|
lessons_learned: 0,
|
|
772
760
|
});
|
|
@@ -776,7 +764,7 @@ async function autoEndSession(): Promise<void> {
|
|
|
776
764
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
777
765
|
const { name, arguments: args } = request.params;
|
|
778
766
|
|
|
779
|
-
// Auto-start brain session
|
|
767
|
+
// Auto-start brain session if not yet started (fallback: ListTools already fired this).
|
|
780
768
|
// Skip session management tools to avoid recursion.
|
|
781
769
|
const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
|
|
782
770
|
if (!sessionTools.has(name) && !_autoSessionStarted && !_autoSessionStarting) {
|
|
@@ -794,6 +782,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
794
782
|
|
|
795
783
|
try {
|
|
796
784
|
const text = await handleTool(name, (args ?? {}) as Record<string, unknown>);
|
|
785
|
+
|
|
786
|
+
// ── Inject session context into the FIRST tool response ─────────────────
|
|
787
|
+
// This is the key: the AI sees the previous session summary, handoff tasks,
|
|
788
|
+
// and recent lessons automatically — without needing to call session_start.
|
|
789
|
+
// Only injected once per process (on the very first tool call).
|
|
790
|
+
if (!_autoSessionContextInjected && _autoSessionContext && !sessionTools.has(name)) {
|
|
791
|
+
_autoSessionContextInjected = true;
|
|
792
|
+
const ctx = _autoSessionContext;
|
|
793
|
+
_autoSessionContext = null; // free memory
|
|
794
|
+
return { content: [{ type: 'text', text: `${ctx}\n\n---\n\n${text}` }] };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
797
|
return { content: [{ type: 'text', text }] };
|
|
798
798
|
} catch (err) {
|
|
799
799
|
if (err instanceof McpError) throw err;
|
|
@@ -873,61 +873,122 @@ function buildMcpConfig(apiKey: string, instanceId: string, editor: string): str
|
|
|
873
873
|
}, null, 2);
|
|
874
874
|
}
|
|
875
875
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
editor
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
876
|
+
/**
|
|
877
|
+
* Build only the per-server entry that gets merged into an existing config.
|
|
878
|
+
* Returned shape varies by editor — see writeMergedMcpConfig for the merge
|
|
879
|
+
* locations.
|
|
880
|
+
*/
|
|
881
|
+
function buildCachlyServerEntry(apiKey: string, instanceId: string, editor: string): unknown {
|
|
882
|
+
if (editor === 'continue') {
|
|
883
|
+
return {
|
|
884
|
+
transport: { type: 'stdio', command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'] },
|
|
885
|
+
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
if (editor === 'zed') {
|
|
889
|
+
return {
|
|
890
|
+
command: {
|
|
891
|
+
path: 'npx',
|
|
892
|
+
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
893
|
+
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
894
|
+
},
|
|
895
|
+
settings: {},
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
886
899
|
command: 'npx',
|
|
887
900
|
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
888
901
|
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
889
902
|
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Merge the cachly server into an existing editor MCP config file — preserving
|
|
907
|
+
* every other MCP server the user has configured. Creates the file if missing.
|
|
908
|
+
* Writes a one-time .cachly-backup-<timestamp> next to the file before any
|
|
909
|
+
* mutation so the user can recover from corrupt/unexpected merges.
|
|
910
|
+
*
|
|
911
|
+
* Returns 'created' if the file did not exist, 'updated' if cachly was already
|
|
912
|
+
* present (re-pointed to a possibly new instance), or 'merged' if cachly was
|
|
913
|
+
* added alongside other servers.
|
|
914
|
+
*/
|
|
915
|
+
async function writeMergedMcpConfig(
|
|
916
|
+
configPath: string,
|
|
917
|
+
apiKey: string,
|
|
918
|
+
instanceId: string,
|
|
919
|
+
editor: string,
|
|
920
|
+
): Promise<'created' | 'updated' | 'merged'> {
|
|
921
|
+
const { writeFile, readFile, mkdir, copyFile } = await import('node:fs/promises');
|
|
922
|
+
const { existsSync } = await import('node:fs');
|
|
923
|
+
const { dirname } = await import('node:path');
|
|
924
|
+
|
|
925
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
890
926
|
|
|
891
|
-
|
|
927
|
+
// No existing file → write fresh full config.
|
|
928
|
+
if (!existsSync(configPath)) {
|
|
929
|
+
await writeFile(configPath, buildMcpConfig(apiKey, instanceId, editor), 'utf-8');
|
|
930
|
+
return 'created';
|
|
931
|
+
}
|
|
892
932
|
|
|
893
|
-
|
|
933
|
+
// Parse existing config. On parse failure, back it up and start fresh
|
|
934
|
+
// (never silently destroy user data — the .cachly-backup gives them a
|
|
935
|
+
// recovery path).
|
|
936
|
+
const raw = await readFile(configPath, 'utf-8');
|
|
937
|
+
let existing: Record<string, unknown>;
|
|
894
938
|
try {
|
|
895
|
-
existing = JSON.parse(
|
|
939
|
+
existing = JSON.parse(raw) as Record<string, unknown>;
|
|
940
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
941
|
+
throw new Error('config root is not an object');
|
|
942
|
+
}
|
|
896
943
|
} catch {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
process.stderr.write(`⚠️ ${configPath} was not valid JSON — backed up to ${configPath}.bak\n`);
|
|
902
|
-
} catch { /* backup failed — still safe to overwrite with valid config */ }
|
|
944
|
+
const backupPath = `${configPath}.cachly-backup-${Date.now()}`;
|
|
945
|
+
await copyFile(configPath, backupPath);
|
|
946
|
+
await writeFile(configPath, buildMcpConfig(apiKey, instanceId, editor), 'utf-8');
|
|
947
|
+
return 'created';
|
|
903
948
|
}
|
|
904
949
|
|
|
950
|
+
// Defensive backup of the valid config before mutation — first time only.
|
|
951
|
+
// The cachly-backup file is overwritten on each re-init so we don't pollute
|
|
952
|
+
// the project, but it gives the user one undo level.
|
|
953
|
+
const backupPath = `${configPath}.cachly-backup`;
|
|
954
|
+
try {
|
|
955
|
+
await copyFile(configPath, backupPath);
|
|
956
|
+
} catch { /* non-critical — backup is best-effort */ }
|
|
957
|
+
|
|
958
|
+
const entry = buildCachlyServerEntry(apiKey, instanceId, editor);
|
|
959
|
+
let hadCachly = false;
|
|
960
|
+
|
|
905
961
|
if (editor === 'continue') {
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
962
|
+
// experimental.modelContextProtocolServers is an Array — replace existing
|
|
963
|
+
// cachly entry (matched by command/args containing @cachly-dev/mcp-server)
|
|
964
|
+
// or append.
|
|
965
|
+
type ContinueServer = { transport?: { args?: string[] }; env?: Record<string, string> };
|
|
966
|
+
const exp = (existing.experimental as Record<string, unknown> | undefined) ?? {};
|
|
967
|
+
const servers = Array.isArray(exp.modelContextProtocolServers)
|
|
968
|
+
? (exp.modelContextProtocolServers as ContinueServer[])
|
|
969
|
+
: [];
|
|
970
|
+
const isCachly = (s: ContinueServer) =>
|
|
971
|
+
s?.transport?.args?.some((a) => typeof a === 'string' && a.includes('@cachly-dev/mcp-server')) ?? false;
|
|
909
972
|
const filtered = servers.filter((s) => {
|
|
910
|
-
|
|
911
|
-
return
|
|
973
|
+
if (isCachly(s)) { hadCachly = true; return false; }
|
|
974
|
+
return true;
|
|
912
975
|
});
|
|
913
|
-
filtered.push(
|
|
914
|
-
existing
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
976
|
+
filtered.push(entry as ContinueServer);
|
|
977
|
+
existing.experimental = { ...exp, modelContextProtocolServers: filtered };
|
|
978
|
+
} else if (editor === 'zed') {
|
|
979
|
+
// context_servers is a Record — set cachly key.
|
|
980
|
+
const cs = (existing.context_servers as Record<string, unknown> | undefined) ?? {};
|
|
981
|
+
hadCachly = 'cachly' in cs;
|
|
982
|
+
existing.context_servers = { ...cs, cachly: entry };
|
|
983
|
+
} else {
|
|
984
|
+
// Standard mcpServers map — Claude / Cursor / Windsurf / VS Code / Cline.
|
|
985
|
+
const mcp = (existing.mcpServers as Record<string, unknown> | undefined) ?? {};
|
|
986
|
+
hadCachly = 'cachly' in mcp;
|
|
987
|
+
existing.mcpServers = { ...mcp, cachly: entry };
|
|
924
988
|
}
|
|
925
989
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
servers['cachly'] = cachlyEntry;
|
|
929
|
-
existing['mcpServers'] = servers;
|
|
930
|
-
return JSON.stringify(existing, null, 2);
|
|
990
|
+
await writeFile(configPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
991
|
+
return hadCachly ? 'updated' : 'merged';
|
|
931
992
|
}
|
|
932
993
|
|
|
933
994
|
function buildClaudeMdBlock(instanceId: string): string {
|
|
@@ -1002,616 +1063,6 @@ async function writeClaudeMd(projectDir: string, instanceId: string): Promise<'w
|
|
|
1002
1063
|
return 'written';
|
|
1003
1064
|
}
|
|
1004
1065
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
// ── CLI: cachly digest ────────────────────────────────────────────────────────
|
|
1008
|
-
// Usage: npx @cachly-dev/mcp-server@latest digest
|
|
1009
|
-
// Weekly brain summary — what your AI learned this week. Shareable output.
|
|
1010
|
-
|
|
1011
|
-
if (process.argv[2] === 'digest') {
|
|
1012
|
-
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1013
|
-
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1014
|
-
|
|
1015
|
-
if (!apiKey || !instanceId) {
|
|
1016
|
-
console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1017
|
-
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
1018
|
-
process.exit(1);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
process.stdout.write('\n🧠 Fetching your weekly Brain digest...\n\n');
|
|
1022
|
-
|
|
1023
|
-
try {
|
|
1024
|
-
// Fetch current stats
|
|
1025
|
-
const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
1026
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1027
|
-
signal: AbortSignal.timeout(8000),
|
|
1028
|
-
});
|
|
1029
|
-
if (!statsRes.ok) throw new Error(`stats HTTP ${statsRes.status}`);
|
|
1030
|
-
const stats = await statsRes.json() as {
|
|
1031
|
-
lesson_count?: number; context_count?: number; quality_score?: number;
|
|
1032
|
-
total_recall_count?: number; top_lessons?: Array<{ topic: string; recall_count: number; what_worked: string }>;
|
|
1033
|
-
team_authors?: string[];
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
const lessons = stats.lesson_count ?? 0;
|
|
1037
|
-
const recalls = stats.total_recall_count ?? 0;
|
|
1038
|
-
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
1039
|
-
const topLessons = (stats.top_lessons ?? []).slice(0, 5);
|
|
1040
|
-
const teamAuthors = stats.team_authors ?? [];
|
|
1041
|
-
const tokensSaved = recalls * 1200;
|
|
1042
|
-
const costSaved = (tokensSaved * 0.000003).toFixed(2);
|
|
1043
|
-
|
|
1044
|
-
const level = lessons < 10 ? 'Junior Dev 🔧' : lessons < 30 ? 'Mid Dev ⚡' :
|
|
1045
|
-
lessons < 60 ? 'Senior Dev 🧠' : lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
1046
|
-
|
|
1047
|
-
const now = new Date();
|
|
1048
|
-
const weekStart = new Date(now); weekStart.setDate(now.getDate() - 7);
|
|
1049
|
-
const fmt = (d: Date) => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
1050
|
-
|
|
1051
|
-
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
1052
|
-
console.log(`│ \x1b[1m🧠 Brain Weekly Digest\x1b[0m \x1b[90m${fmt(weekStart)} – ${fmt(now)}\x1b[0m │`);
|
|
1053
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1054
|
-
console.log(`│ Total lessons : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level : \x1b[32m${level.padEnd(18)}\x1b[0m│`);
|
|
1055
|
-
console.log(`│ Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m │`);
|
|
1056
|
-
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│`);
|
|
1057
|
-
|
|
1058
|
-
if (teamAuthors.length > 0) {
|
|
1059
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1060
|
-
console.log(`│ \x1b[36m👥 Team contributors:\x1b[0m ${teamAuthors.slice(0, 5).join(', ').slice(0, 42).padEnd(42)} │`);
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (topLessons.length > 0) {
|
|
1064
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1065
|
-
console.log('│ \x1b[33m🔥 Most recalled lessons this week:\x1b[0m │');
|
|
1066
|
-
for (const l of topLessons) {
|
|
1067
|
-
const line = `${l.topic}: ${l.what_worked}`.slice(0, 57);
|
|
1068
|
-
console.log(`│ \x1b[90m• ${line.padEnd(58)}\x1b[0m│`);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1073
|
-
console.log('│ \x1b[32m📋 Share this digest:\x1b[0m │');
|
|
1074
|
-
console.log('│ \x1b[90m npx @cachly-dev/mcp-server@latest share\x1b[0m │');
|
|
1075
|
-
console.log('│ \x1b[90m Add more: npx @cachly-dev/mcp-server@latest invite\x1b[0m │');
|
|
1076
|
-
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1077
|
-
console.log('');
|
|
1078
|
-
|
|
1079
|
-
// Pro tip for cron
|
|
1080
|
-
console.log(' \x1b[2m💡 Automate: add to crontab for a weekly team email\x1b[0m');
|
|
1081
|
-
console.log(' \x1b[2m 0 9 * * 1 npx @cachly-dev/mcp-server@latest digest\x1b[0m');
|
|
1082
|
-
console.log('');
|
|
1083
|
-
} catch (e) {
|
|
1084
|
-
console.log(`\n❌ Could not fetch digest: ${(e as Error).message}\n`);
|
|
1085
|
-
process.exit(1);
|
|
1086
|
-
}
|
|
1087
|
-
process.exit(0);
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// ── CLI: cachly invite ────────────────────────────────────────────────────────
|
|
1091
|
-
// Usage: npx @cachly-dev/mcp-server@latest invite [email]
|
|
1092
|
-
// Invites a teammate to share your Brain. Fastest referral loop.
|
|
1093
|
-
|
|
1094
|
-
if (process.argv[2] === 'invite') {
|
|
1095
|
-
const { createInterface } = await import('node:readline');
|
|
1096
|
-
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1097
|
-
|
|
1098
|
-
if (!apiKey) {
|
|
1099
|
-
console.log('\n⚠️ CACHLY_JWT must be set.');
|
|
1100
|
-
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
1101
|
-
process.exit(1);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
let email = process.argv[3] ?? '';
|
|
1105
|
-
|
|
1106
|
-
if (!email) {
|
|
1107
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1108
|
-
email = await new Promise<string>((resolve) => {
|
|
1109
|
-
rl.question('\n 📬 Teammate email to invite: ', (ans) => { rl.close(); resolve(ans.trim()); });
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
if (!email || !email.includes('@')) {
|
|
1114
|
-
console.log('\n❌ Invalid email address.\n');
|
|
1115
|
-
process.exit(1);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
console.log(`\n ⏳ Inviting ${email}...`);
|
|
1119
|
-
|
|
1120
|
-
try {
|
|
1121
|
-
const res = await fetch(`${API_URL}/api/v1/team/invite`, {
|
|
1122
|
-
method: 'POST',
|
|
1123
|
-
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
1124
|
-
body: JSON.stringify({ email, role: 'member', source: 'cli-invite' }),
|
|
1125
|
-
signal: AbortSignal.timeout(8000),
|
|
1126
|
-
});
|
|
1127
|
-
|
|
1128
|
-
if (res.ok) {
|
|
1129
|
-
console.log(`\n ✅ Invite sent to \x1b[32m${email}\x1b[0m`);
|
|
1130
|
-
console.log(` They'll get a link to join your Brain — one click, 30 seconds.\n`);
|
|
1131
|
-
console.log(` 💡 Once they join, your AI assistants share lessons automatically.\n`);
|
|
1132
|
-
} else if (res.status === 409) {
|
|
1133
|
-
console.log(`\n ✓ ${email} is already a team member.\n`);
|
|
1134
|
-
} else {
|
|
1135
|
-
const body = await res.json().catch(() => ({})) as { error?: string };
|
|
1136
|
-
console.log(`\n ❌ Invite failed: ${body.error ?? `HTTP ${res.status}`}\n`);
|
|
1137
|
-
process.exit(1);
|
|
1138
|
-
}
|
|
1139
|
-
} catch (e) {
|
|
1140
|
-
console.log(`\n ❌ Network error: ${(e as Error).message}\n`);
|
|
1141
|
-
process.exit(1);
|
|
1142
|
-
}
|
|
1143
|
-
process.exit(0);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// ── CLI: cachly join <token> ──────────────────────────────────────────────────
|
|
1147
|
-
// Usage: npx @cachly-dev/mcp-server@latest join <token>
|
|
1148
|
-
// Accepts a Brain invite link — writes the shared instance ID into all
|
|
1149
|
-
// detected editor MCP configs and the global ~/.claude/mcp.json.
|
|
1150
|
-
// No account needed if CACHLY_JWT is already set; otherwise prompts for auth.
|
|
1151
|
-
|
|
1152
|
-
if (process.argv[2] === 'join') {
|
|
1153
|
-
const { writeFile: wfJ, readFile: rfJ, mkdir: mkJ } = await import('node:fs/promises');
|
|
1154
|
-
const { existsSync: exJ } = await import('node:fs');
|
|
1155
|
-
const { resolve: resJ, dirname: dirJ } = await import('node:path');
|
|
1156
|
-
const { homedir } = await import('node:os');
|
|
1157
|
-
const { createInterface: ciJ } = await import('node:readline');
|
|
1158
|
-
|
|
1159
|
-
const token = process.argv[3] ?? '';
|
|
1160
|
-
if (!token) {
|
|
1161
|
-
console.log('\n❌ Usage: npx @cachly-dev/mcp-server@latest join <token>\n');
|
|
1162
|
-
console.log(' Get a token from a teammate: npx @cachly-dev/mcp-server@latest invite\n');
|
|
1163
|
-
process.exit(1);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
console.log('\n⏳ Looking up invite...');
|
|
1167
|
-
|
|
1168
|
-
// ── Step 1: Fetch invite info (public, no auth) ───────────────────────────
|
|
1169
|
-
let inviteInfo: { instance_id: string; instance_name: string; tier: string; expires_at: string; label?: string };
|
|
1170
|
-
try {
|
|
1171
|
-
const res = await fetch(`${API_URL}/api/invite/${token}`, { signal: AbortSignal.timeout(8000) });
|
|
1172
|
-
if (res.status === 404) { console.log('\n❌ Invite not found — the link may be invalid.\n'); process.exit(1); }
|
|
1173
|
-
if (res.status === 410) { console.log('\n❌ This invite link has expired (7-day limit).\n'); process.exit(1); }
|
|
1174
|
-
if (!res.ok) { console.log(`\n❌ Could not fetch invite: HTTP ${res.status}\n`); process.exit(1); }
|
|
1175
|
-
inviteInfo = await res.json() as typeof inviteInfo;
|
|
1176
|
-
} catch (e) {
|
|
1177
|
-
console.log(`\n❌ Network error: ${(e as Error).message}\n`);
|
|
1178
|
-
process.exit(1);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const { instance_id, instance_name, tier, label } = inviteInfo;
|
|
1182
|
-
const expires = new Date(inviteInfo.expires_at).toLocaleDateString();
|
|
1183
|
-
|
|
1184
|
-
console.log('');
|
|
1185
|
-
console.log(' \x1b[35m╔══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1186
|
-
console.log(' \x1b[35m║\x1b[0m \x1b[1m🧠 Brain Invite\x1b[0m \x1b[35m║\x1b[0m');
|
|
1187
|
-
console.log(' \x1b[35m╚══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1188
|
-
console.log('');
|
|
1189
|
-
console.log(` Brain : \x1b[1m${instance_name}\x1b[0m`);
|
|
1190
|
-
console.log(` Tier : ${tier}`);
|
|
1191
|
-
if (label) console.log(` Note : ${label}`);
|
|
1192
|
-
console.log(` Expires : ${expires}`);
|
|
1193
|
-
console.log('');
|
|
1194
|
-
|
|
1195
|
-
// ── Step 2: Confirm ───────────────────────────────────────────────────────
|
|
1196
|
-
const rlJ = ciJ({ input: process.stdin, output: process.stdout });
|
|
1197
|
-
const confirmJ = await new Promise<string>((resolve) => {
|
|
1198
|
-
rlJ.question(' Join this Brain? [Y/n] ', (a) => { rlJ.close(); resolve(a.trim().toLowerCase() || 'y'); });
|
|
1199
|
-
});
|
|
1200
|
-
if (confirmJ !== 'y' && confirmJ !== 'yes') {
|
|
1201
|
-
console.log('\n Cancelled.\n');
|
|
1202
|
-
process.exit(0);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// ── Step 3: Ensure JWT ────────────────────────────────────────────────────
|
|
1206
|
-
let joinJwt = JWT;
|
|
1207
|
-
if (!joinJwt) {
|
|
1208
|
-
console.log('\n No API key found — starting sign-in (10 seconds)...\n');
|
|
1209
|
-
try {
|
|
1210
|
-
const dcRes = await fetch(`${API_URL}/api/v1/auth/device/code`, {
|
|
1211
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1212
|
-
body: JSON.stringify({ client_id: 'cachly-mcp-cli' }),
|
|
1213
|
-
signal: AbortSignal.timeout(10000),
|
|
1214
|
-
});
|
|
1215
|
-
if (dcRes.ok) {
|
|
1216
|
-
const dc = await dcRes.json() as { verification_uri: string; user_code: string; device_code: string; interval: number };
|
|
1217
|
-
console.log(` 👉 Open: \x1b[36m${dc.verification_uri}\x1b[0m`);
|
|
1218
|
-
console.log(` Code: \x1b[1m${dc.user_code}\x1b[0m\n`);
|
|
1219
|
-
// Poll for token
|
|
1220
|
-
const pollInterval = (dc.interval ?? 5) * 1000;
|
|
1221
|
-
for (let i = 0; i < 24; i++) {
|
|
1222
|
-
await new Promise(r => setTimeout(r, pollInterval));
|
|
1223
|
-
try {
|
|
1224
|
-
const tokRes = await fetch(`${API_URL}/api/v1/auth/device/token`, {
|
|
1225
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1226
|
-
body: JSON.stringify({ device_code: dc.device_code, client_id: 'cachly-mcp-cli' }),
|
|
1227
|
-
signal: AbortSignal.timeout(8000),
|
|
1228
|
-
});
|
|
1229
|
-
if (tokRes.ok) {
|
|
1230
|
-
const tok = await tokRes.json() as { access_token?: string };
|
|
1231
|
-
if (tok.access_token) { joinJwt = tok.access_token; break; }
|
|
1232
|
-
}
|
|
1233
|
-
} catch { /* polling — keep going */ }
|
|
1234
|
-
}
|
|
1235
|
-
if (!joinJwt) { console.log('\n❌ Sign-in timed out. Run the command again.\n'); process.exit(1); }
|
|
1236
|
-
console.log(' ✅ Signed in!\n');
|
|
1237
|
-
}
|
|
1238
|
-
} catch (e) {
|
|
1239
|
-
console.log(`\n ⚠️ Could not start sign-in: ${(e as Error).message}`);
|
|
1240
|
-
console.log(` Set CACHLY_JWT manually from https://cachly.dev/setup-ai\n`);
|
|
1241
|
-
process.exit(1);
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// ── Step 4: Write instance ID into all editor MCP configs ────────────────
|
|
1246
|
-
const cwd = process.cwd();
|
|
1247
|
-
const home = homedir();
|
|
1248
|
-
|
|
1249
|
-
const editorFiles: Array<{ label: string; path: string; global?: boolean }> = [
|
|
1250
|
-
{ label: '.mcp.json (Claude Code project)', path: resJ(cwd, '.mcp.json') },
|
|
1251
|
-
{ label: '.cursor/mcp.json', path: resJ(cwd, '.cursor', 'mcp.json') },
|
|
1252
|
-
{ label: '.windsurf/mcp.json', path: resJ(cwd, '.windsurf', 'mcp.json') },
|
|
1253
|
-
{ label: '.vscode/mcp.json (Copilot/Cline)', path: resJ(cwd, '.vscode', 'mcp.json') },
|
|
1254
|
-
{ label: '~/.claude/mcp.json (global)', path: resJ(home, '.claude', 'mcp.json'), global: true },
|
|
1255
|
-
];
|
|
1256
|
-
|
|
1257
|
-
let written = 0;
|
|
1258
|
-
for (const { label: eLabel, path: ePath, global: isGlobal } of editorFiles) {
|
|
1259
|
-
// Only write project configs if the directory exists (or global)
|
|
1260
|
-
const dir = dirJ(ePath);
|
|
1261
|
-
if (!isGlobal && !exJ(dir) && !exJ(ePath)) continue;
|
|
1262
|
-
try {
|
|
1263
|
-
await mkJ(dir, { recursive: true });
|
|
1264
|
-
let cfg: Record<string, unknown> = {};
|
|
1265
|
-
if (exJ(ePath)) {
|
|
1266
|
-
try {
|
|
1267
|
-
cfg = JSON.parse(await rfJ(ePath, 'utf-8')) as Record<string, unknown>;
|
|
1268
|
-
} catch {
|
|
1269
|
-
try { await (await import('node:fs/promises')).rename(ePath, ePath + '.bak'); } catch { /* backup failed */ }
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
// Update or create the cachly entry with the shared instance ID
|
|
1273
|
-
const servers = (cfg['mcpServers'] ?? {}) as Record<string, unknown>;
|
|
1274
|
-
const existing = (servers['cachly'] ?? {}) as Record<string, unknown>;
|
|
1275
|
-
const env = (existing['env'] ?? {}) as Record<string, string>;
|
|
1276
|
-
env['CACHLY_BRAIN_INSTANCE_ID'] = instance_id;
|
|
1277
|
-
if (joinJwt && !env['CACHLY_JWT']) env['CACHLY_JWT'] = joinJwt;
|
|
1278
|
-
env['CACHLY_API_URL'] ??= 'https://api.cachly.dev';
|
|
1279
|
-
servers['cachly'] = { ...existing, command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env };
|
|
1280
|
-
cfg['mcpServers'] = servers;
|
|
1281
|
-
await wfJ(ePath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
1282
|
-
console.log(` ✅ ${eLabel}`);
|
|
1283
|
-
written++;
|
|
1284
|
-
} catch (joinWriteErr) {
|
|
1285
|
-
// Only surface unexpected errors — missing editor dirs are expected and silently skipped above
|
|
1286
|
-
if ((joinWriteErr as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
1287
|
-
console.log(` ⚠️ ${eLabel}: ${(joinWriteErr as Error).message}`);
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
console.log('');
|
|
1293
|
-
if (written === 0) {
|
|
1294
|
-
console.log(' ⚠️ No editor configs found — run `cachly setup` first.\n');
|
|
1295
|
-
} else {
|
|
1296
|
-
console.log(` 🧠 You're now sharing Brain: \x1b[1m${instance_name}\x1b[0m`);
|
|
1297
|
-
console.log(' Restart your editor — your AI arrives pre-briefed from the team Brain.\n');
|
|
1298
|
-
console.log(' 📛 Add the badge to your README:');
|
|
1299
|
-
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
1300
|
-
}
|
|
1301
|
-
process.exit(0);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// ── CLI: cachly upgrade ───────────────────────────────────────────────────────
|
|
1305
|
-
// Usage: npx @cachly-dev/mcp-server@latest upgrade
|
|
1306
|
-
// Checks npm for the latest version and prints upgrade instructions if outdated.
|
|
1307
|
-
|
|
1308
|
-
if (process.argv[2] === 'upgrade') {
|
|
1309
|
-
process.stdout.write('\n⏳ Checking for updates...\n');
|
|
1310
|
-
try {
|
|
1311
|
-
const res = await fetch('https://registry.npmjs.org/@cachly-dev/mcp-server/latest', {
|
|
1312
|
-
signal: AbortSignal.timeout(8000),
|
|
1313
|
-
});
|
|
1314
|
-
if (!res.ok) throw new Error(`registry HTTP ${res.status}`);
|
|
1315
|
-
const pkg = await res.json() as { version?: string };
|
|
1316
|
-
const latest = pkg.version;
|
|
1317
|
-
if (!latest || !/^\d+\.\d+\.\d+/.test(latest)) throw new Error('npm registry returned unexpected response');
|
|
1318
|
-
|
|
1319
|
-
if (latest === CURRENT_VERSION) {
|
|
1320
|
-
console.log(`\n✅ Already on the latest version: \x1b[32mv${CURRENT_VERSION}\x1b[0m\n`);
|
|
1321
|
-
} else {
|
|
1322
|
-
console.log('');
|
|
1323
|
-
console.log('\x1b[35m ╔══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1324
|
-
console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly update available\x1b[0m \x1b[35m║\x1b[0m');
|
|
1325
|
-
console.log('\x1b[35m ╚══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1326
|
-
console.log('');
|
|
1327
|
-
console.log(` Current : \x1b[31mv${CURRENT_VERSION}\x1b[0m`);
|
|
1328
|
-
console.log(` Latest : \x1b[32mv${latest}\x1b[0m`);
|
|
1329
|
-
console.log('');
|
|
1330
|
-
console.log(' Update your editor configs to pick up the new version:');
|
|
1331
|
-
console.log('');
|
|
1332
|
-
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m');
|
|
1333
|
-
console.log('');
|
|
1334
|
-
console.log(' \x1b[2m(npx always fetches the latest when @latest is specified)\x1b[0m');
|
|
1335
|
-
console.log('');
|
|
1336
|
-
}
|
|
1337
|
-
} catch (e) {
|
|
1338
|
-
console.log(`\n❌ Could not check for updates: ${(e as Error).message}\n`);
|
|
1339
|
-
process.exit(1);
|
|
1340
|
-
}
|
|
1341
|
-
process.exit(0);
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
// ── CLI: cachly demo ──────────────────────────────────────────────────────────
|
|
1345
|
-
// Usage: npx @cachly-dev/mcp-server@latest demo
|
|
1346
|
-
// Zero-signup. Reads local git history and shows what the AI Brain would know.
|
|
1347
|
-
// The fastest path from "what is this?" to "I need this".
|
|
1348
|
-
|
|
1349
|
-
if (process.argv[2] === 'demo') {
|
|
1350
|
-
const { execSync } = await import('node:child_process');
|
|
1351
|
-
const { existsSync } = await import('node:fs');
|
|
1352
|
-
const { resolve } = await import('node:path');
|
|
1353
|
-
const cwd = process.cwd();
|
|
1354
|
-
|
|
1355
|
-
console.log('\n\x1b[35m🧠 cachly Brain — Live Demo\x1b[0m');
|
|
1356
|
-
console.log('\x1b[90mNo account needed. Reading your git history...\x1b[0m\n');
|
|
1357
|
-
|
|
1358
|
-
// Verify git repo
|
|
1359
|
-
if (!existsSync(resolve(cwd, '.git'))) {
|
|
1360
|
-
console.log('⚠️ No git repository found here.');
|
|
1361
|
-
console.log(' Run this command inside a project directory with git history.\n');
|
|
1362
|
-
console.log(' Example: cd ~/my-project && npx @cachly-dev/mcp-server@latest demo\n');
|
|
1363
|
-
process.exit(0);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
// Read git log
|
|
1367
|
-
let logOutput = '';
|
|
1368
|
-
try {
|
|
1369
|
-
logOutput = execSync(
|
|
1370
|
-
'git log HEAD --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n 200',
|
|
1371
|
-
{ cwd, encoding: 'utf-8', stdio: 'pipe' }
|
|
1372
|
-
);
|
|
1373
|
-
} catch {
|
|
1374
|
-
console.log('⚠️ Could not read git log. Make sure you have commits.\n');
|
|
1375
|
-
process.exit(0);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
1379
|
-
const [sha, subject, date, author] = line.split('|||');
|
|
1380
|
-
return { sha: (sha ?? '').trim().slice(0, 8), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
|
-
if (commits.length === 0) {
|
|
1384
|
-
console.log('⚠️ No commits found.\n');
|
|
1385
|
-
process.exit(0);
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
// Classify commits (same logic as brain_from_git)
|
|
1389
|
-
const classify = (s: string) => {
|
|
1390
|
-
const t = s.toLowerCase();
|
|
1391
|
-
if (/\b(fix|fixed|bug|hotfix|patch|revert|resolve)\b/.test(t)) return 'fix';
|
|
1392
|
-
if (/\b(feat|feature|add|implement|new|introduce)\b/.test(t)) return 'feat';
|
|
1393
|
-
if (/\b(perf|optim|speed|cache|latency)\b/.test(t)) return 'perf';
|
|
1394
|
-
if (/\b(security|cve|auth|csrf|xss|inject|sanitize)\b/.test(t)) return 'security';
|
|
1395
|
-
if (/\b(deploy|ci|cd|docker|k8s|infra)\b/.test(t)) return 'deploy';
|
|
1396
|
-
if (/\b(refactor|clean|simplify|extract)\b/.test(t)) return 'refactor';
|
|
1397
|
-
return 'chore';
|
|
1398
|
-
};
|
|
1399
|
-
|
|
1400
|
-
// Count categories
|
|
1401
|
-
const cats = new Map<string, number>();
|
|
1402
|
-
const fixes: string[] = [];
|
|
1403
|
-
const feats: string[] = [];
|
|
1404
|
-
const security: string[] = [];
|
|
1405
|
-
const authors = new Set<string>();
|
|
1406
|
-
|
|
1407
|
-
for (const c of commits) {
|
|
1408
|
-
const cat = classify(c.subject);
|
|
1409
|
-
cats.set(cat, (cats.get(cat) ?? 0) + 1);
|
|
1410
|
-
if (cat === 'fix' && fixes.length < 5) fixes.push(c.subject.slice(0, 72));
|
|
1411
|
-
if (cat === 'feat' && feats.length < 5) feats.push(c.subject.slice(0, 72));
|
|
1412
|
-
if (cat === 'security' && security.length < 3) security.push(c.subject.slice(0, 72));
|
|
1413
|
-
if (c.author) authors.add(c.author.split(' ')[0]!);
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
const totalLessons = commits.length - (cats.get('chore') ?? 0);
|
|
1417
|
-
const dateRange = `${commits[commits.length - 1]?.date ?? '?'} → ${commits[0]?.date ?? '?'}`;
|
|
1418
|
-
|
|
1419
|
-
// Display the demo
|
|
1420
|
-
process.stdout.write('\x1b[2K\r');
|
|
1421
|
-
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
1422
|
-
console.log('│ \x1b[1m🧠 Brain Preview — What your AI would know\x1b[0m │');
|
|
1423
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1424
|
-
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 │`);
|
|
1425
|
-
console.log(`│ Lessons extracted: \x1b[32m${String(totalLessons).padEnd(6)}\x1b[0m Contributors: \x1b[36m${String(authors.size).padEnd(20)}\x1b[0m│`);
|
|
1426
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1427
|
-
|
|
1428
|
-
// Category breakdown
|
|
1429
|
-
const sorted = [...cats.entries()].filter(([k]) => k !== 'chore').sort((a, b) => b[1] - a[1]);
|
|
1430
|
-
const bar = (n: number, max: number) => '█'.repeat(Math.round((n / max) * 20)).padEnd(20);
|
|
1431
|
-
const maxVal = Math.max(...sorted.map(([, v]) => v), 1);
|
|
1432
|
-
console.log('│ \x1b[90mCategory breakdown:\x1b[0m │');
|
|
1433
|
-
for (const [cat, count] of sorted) {
|
|
1434
|
-
const emoji = { fix: '🔧', feat: '✨', perf: '⚡', security: '🔒', deploy: '🚀', refactor: '🔄' }[cat] ?? '•';
|
|
1435
|
-
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 │`);
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
if (security.length > 0) {
|
|
1439
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1440
|
-
console.log('│ \x1b[31m🔒 Security fixes your AI would know:\x1b[0m │');
|
|
1441
|
-
for (const s of security) console.log(`│ \x1b[90m• ${s.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
if (fixes.length > 0) {
|
|
1445
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1446
|
-
console.log('│ \x1b[33m🔧 Bug fixes your AI would remember:\x1b[0m │');
|
|
1447
|
-
for (const f of fixes.slice(0, 4)) console.log(`│ \x1b[90m• ${f.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
if (feats.length > 0) {
|
|
1451
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1452
|
-
console.log('│ \x1b[32m✨ Features & decisions it would recall:\x1b[0m │');
|
|
1453
|
-
for (const f of feats.slice(0, 3)) console.log(`│ \x1b[90m• ${f.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1457
|
-
console.log('│ \x1b[32m💡 With cachly, your AI arrives pre-briefed every session.\x1b[0m │');
|
|
1458
|
-
console.log('│ \x1b[32m No more re-explaining. No more repeated mistakes.\x1b[0m │');
|
|
1459
|
-
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1460
|
-
console.log('');
|
|
1461
|
-
console.log(' \x1b[1mMake this permanent (free, 30 seconds):\x1b[0m');
|
|
1462
|
-
console.log(' \x1b[32m$ npx @cachly-dev/mcp-server@latest setup\x1b[0m');
|
|
1463
|
-
console.log('');
|
|
1464
|
-
console.log(' Works with: Claude Code · Cursor · Windsurf · Copilot · Cline · Zed');
|
|
1465
|
-
console.log(' Free forever · GDPR · German servers · No credit card');
|
|
1466
|
-
console.log('');
|
|
1467
|
-
process.exit(0);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// ── CLI: cachly share ─────────────────────────────────────────────────────────
|
|
1471
|
-
// Usage: npx @cachly-dev/mcp-server@latest share
|
|
1472
|
-
// Generates a shareable ASCII card + tweet text with your Brain stats.
|
|
1473
|
-
|
|
1474
|
-
if (process.argv[2] === 'share') {
|
|
1475
|
-
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1476
|
-
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1477
|
-
|
|
1478
|
-
if (!apiKey || !instanceId) {
|
|
1479
|
-
console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1480
|
-
console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 30 seconds)\n');
|
|
1481
|
-
process.exit(1);
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
process.stdout.write('\n🧠 Fetching your Brain stats...\n');
|
|
1485
|
-
|
|
1486
|
-
try {
|
|
1487
|
-
const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
1488
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1489
|
-
signal: AbortSignal.timeout(8000),
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
if (!statsRes.ok) throw new Error(`HTTP ${statsRes.status}`);
|
|
1493
|
-
const stats = await statsRes.json() as {
|
|
1494
|
-
lesson_count?: number; context_count?: number;
|
|
1495
|
-
quality_score?: number; total_recall_count?: number;
|
|
1496
|
-
};
|
|
1497
|
-
|
|
1498
|
-
const lessons = stats.lesson_count ?? 0;
|
|
1499
|
-
const recalls = stats.total_recall_count ?? 0;
|
|
1500
|
-
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
1501
|
-
const tokensSaved = recalls * 1200;
|
|
1502
|
-
const costSaved = (tokensSaved * 0.000003).toFixed(2);
|
|
1503
|
-
|
|
1504
|
-
const level = lessons === 0 ? 'Intern 🌱' :
|
|
1505
|
-
lessons < 10 ? 'Junior Dev 🔧' :
|
|
1506
|
-
lessons < 30 ? 'Mid Dev ⚡' :
|
|
1507
|
-
lessons < 60 ? 'Senior Dev 🧠' :
|
|
1508
|
-
lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
1509
|
-
|
|
1510
|
-
console.log('');
|
|
1511
|
-
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
1512
|
-
console.log('│ 🧠 My AI Brain · powered by \x1b[35mcachly.dev\x1b[0m │');
|
|
1513
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1514
|
-
console.log(`│ Lessons stored : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level: \x1b[32m${level.padEnd(18)}\x1b[0m│`);
|
|
1515
|
-
console.log(`│ Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m │`);
|
|
1516
|
-
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│`);
|
|
1517
|
-
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1518
|
-
console.log('│ Works with: Claude Code · Cursor · Windsurf · Copilot │');
|
|
1519
|
-
console.log('│ Free · GDPR · German servers │');
|
|
1520
|
-
console.log('│ \x1b[35mnpx @cachly-dev/mcp-server@latest setup\x1b[0m │');
|
|
1521
|
-
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1522
|
-
console.log('');
|
|
1523
|
-
|
|
1524
|
-
// Tweet text
|
|
1525
|
-
const shareUrl = 'https://cachly.dev?ref=share&utm_source=x&utm_medium=social&utm_campaign=cli-share';
|
|
1526
|
-
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\n${shareUrl}\n\n#AIMemory #ClaudeCode #Cursor #DeveloperTools`;
|
|
1527
|
-
|
|
1528
|
-
console.log(' \x1b[1m📋 Share on Twitter/X (copy this):\x1b[0m');
|
|
1529
|
-
console.log(' ─────────────────────────────────────');
|
|
1530
|
-
console.log(tweet.split('\n').map(l => ` ${l}`).join('\n'));
|
|
1531
|
-
console.log(' ─────────────────────────────────────');
|
|
1532
|
-
console.log('');
|
|
1533
|
-
console.log(' \x1b[2mPro tip: add a screenshot of your Brain panel for 3x more engagement\x1b[0m');
|
|
1534
|
-
console.log(' \x1b[2mAlso add a live badge to your README:\x1b[0m');
|
|
1535
|
-
console.log(` \x1b[90m npx @cachly-dev/mcp-server@latest badge\x1b[0m`);
|
|
1536
|
-
console.log('');
|
|
1537
|
-
} catch (e) {
|
|
1538
|
-
console.log(`\n❌ Could not fetch stats: ${(e as Error).message}\n`);
|
|
1539
|
-
process.exit(1);
|
|
1540
|
-
}
|
|
1541
|
-
process.exit(0);
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
// ── CLI: cachly badge ─────────────────────────────────────────────────────────
|
|
1545
|
-
// Outputs the Markdown + HTML snippet for embedding a live Brain lesson-count
|
|
1546
|
-
// badge in any README or website. Badge SVG served by cachly API (public, no auth).
|
|
1547
|
-
|
|
1548
|
-
if (process.argv[2] === 'badge') {
|
|
1549
|
-
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1550
|
-
|
|
1551
|
-
if (!instanceId) {
|
|
1552
|
-
console.log('\n⚠️ CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1553
|
-
console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 30 seconds)\n');
|
|
1554
|
-
process.exit(1);
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
const badgeUrl = `${API_URL}/api/v1/badge/${instanceId}`;
|
|
1558
|
-
const targetUrl = 'https://cachly.dev';
|
|
1559
|
-
const markdown = `[](${targetUrl})`;
|
|
1560
|
-
const htmlBadge = `<a href="${targetUrl}"><img src="${badgeUrl}" alt="cachly Brain" /></a>`;
|
|
1561
|
-
|
|
1562
|
-
console.log('');
|
|
1563
|
-
console.log('\x1b[35m ╔═══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1564
|
-
console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly Brain Badge\x1b[0m \x1b[35m║\x1b[0m');
|
|
1565
|
-
console.log('\x1b[35m ╚═══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1566
|
-
console.log('');
|
|
1567
|
-
console.log(' Paste into your README.md:');
|
|
1568
|
-
console.log('');
|
|
1569
|
-
console.log(` \x1b[32m${markdown}\x1b[0m`);
|
|
1570
|
-
console.log('');
|
|
1571
|
-
console.log(' ─────────────────────────────────────────────────────');
|
|
1572
|
-
console.log(' Or paste into HTML:');
|
|
1573
|
-
console.log('');
|
|
1574
|
-
console.log(` \x1b[36m${htmlBadge}\x1b[0m`);
|
|
1575
|
-
console.log('');
|
|
1576
|
-
console.log(' ─────────────────────────────────────────────────────');
|
|
1577
|
-
console.log(' Badge URL (live, updates every hour):');
|
|
1578
|
-
console.log(` \x1b[90m${badgeUrl}\x1b[0m`);
|
|
1579
|
-
console.log('');
|
|
1580
|
-
console.log(' \x1b[2mThe badge shows your current lesson count — no auth required,');
|
|
1581
|
-
console.log(' safe to embed in public repos. Updates hourly.\x1b[0m');
|
|
1582
|
-
console.log('');
|
|
1583
|
-
process.exit(0);
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
// ── CLI: no-args splash screen ────────────────────────────────────────────────
|
|
1587
|
-
// When run with no recognized subcommand, show a compelling pitch + help.
|
|
1588
|
-
|
|
1589
|
-
if (!process.argv[2] && process.stdout.isTTY) {
|
|
1590
|
-
console.log('');
|
|
1591
|
-
console.log('\x1b[35m ╔═══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1592
|
-
console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly — Persistent AI Memory for Developers\x1b[0m \x1b[35m║\x1b[0m');
|
|
1593
|
-
console.log('\x1b[35m ╚═══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1594
|
-
console.log('');
|
|
1595
|
-
console.log(' Your AI forgets everything when a session ends.');
|
|
1596
|
-
console.log(' cachly gives it a brain that survives forever.');
|
|
1597
|
-
console.log('');
|
|
1598
|
-
console.log(' \x1b[1mCommands:\x1b[0m');
|
|
1599
|
-
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest demo\x1b[0m ← Start here (no account)');
|
|
1600
|
-
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m ← Wire up your AI editors');
|
|
1601
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest health\x1b[0m ← Check everything works');
|
|
1602
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest digest\x1b[0m ← Weekly Brain summary');
|
|
1603
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest share\x1b[0m ← Share your Brain stats');
|
|
1604
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest badge\x1b[0m ← README badge for your Brain');
|
|
1605
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest invite\x1b[0m ← Invite a teammate');
|
|
1606
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest join <token>\x1b[0m ← Accept a Brain invite');
|
|
1607
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
|
|
1608
|
-
console.log('');
|
|
1609
|
-
console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
|
|
1610
|
-
console.log(' \x1b[90mFree forever · GDPR · German servers · 89 MCP tools\x1b[0m');
|
|
1611
|
-
console.log('');
|
|
1612
|
-
process.exit(0);
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
1066
|
// ── CLI: cachly init ──────────────────────────────────────────────────────────
|
|
1616
1067
|
// Usage: npx @cachly-dev/mcp-server init --instance-id <id> --api-key <key> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]
|
|
1617
1068
|
|
|
@@ -1636,11 +1087,11 @@ if (process.argv[2] === 'init') {
|
|
|
1636
1087
|
|
|
1637
1088
|
const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
|
|
1638
1089
|
const configPath = resolve(projectDir, configFile);
|
|
1639
|
-
await
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
console.log(`\n
|
|
1090
|
+
const mergeResult = await writeMergedMcpConfig(configPath, apiKey, instanceId, editor);
|
|
1091
|
+
const verb = mergeResult === 'created' ? '✅ Written'
|
|
1092
|
+
: mergeResult === 'updated' ? '✅ Updated (kept existing servers)'
|
|
1093
|
+
: '✅ Merged into existing config (other servers untouched)';
|
|
1094
|
+
console.log(`\n${verb}: ${configFile}`);
|
|
1644
1095
|
|
|
1645
1096
|
// Always write CLAUDE.md (idempotent — safe to run multiple times)
|
|
1646
1097
|
const result = await writeClaudeMd(projectDir, instanceId);
|
|
@@ -1683,198 +1134,7 @@ if (process.argv[2] === 'init') {
|
|
|
1683
1134
|
} catch { /* non-critical — git hook is a best-effort feature */ }
|
|
1684
1135
|
|
|
1685
1136
|
console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}!`);
|
|
1686
|
-
console.log(` Restart your editor — the \`cachly\` MCP tools will appear
|
|
1687
|
-
console.log(`\n 📛 Add a live badge to your README:`);
|
|
1688
|
-
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
1689
|
-
process.exit(0);
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// ── CLI: cachly health ────────────────────────────────────────────────────────
|
|
1693
|
-
// Usage: npx @cachly-dev/mcp-server@latest health
|
|
1694
|
-
// Checks: JWT valid, Brain API reachable, editor configs found, git hook present.
|
|
1695
|
-
|
|
1696
|
-
if (process.argv[2] === 'health') {
|
|
1697
|
-
const { existsSync } = await import('node:fs');
|
|
1698
|
-
const { readFile } = await import('node:fs/promises');
|
|
1699
|
-
const { resolve, join: pJoin } = await import('node:path');
|
|
1700
|
-
|
|
1701
|
-
const cwd = process.cwd();
|
|
1702
|
-
let passed = 0;
|
|
1703
|
-
let failed = 0;
|
|
1704
|
-
|
|
1705
|
-
const ok = (msg: string) => { console.log(` ✅ ${msg}`); passed++; };
|
|
1706
|
-
const warn = (msg: string) => { console.log(` ⚠️ ${msg}`); };
|
|
1707
|
-
const fail = (msg: string) => { console.log(` ❌ ${msg}`); failed++; };
|
|
1708
|
-
|
|
1709
|
-
console.log('\n🧠 cachly health check\n');
|
|
1710
|
-
|
|
1711
|
-
// ── 1. JWT token ────────────────────────────────────────────────────────────
|
|
1712
|
-
console.log('🔑 Auth token');
|
|
1713
|
-
const jwt = process.env.CACHLY_JWT ?? '';
|
|
1714
|
-
if (!jwt) {
|
|
1715
|
-
fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
|
|
1716
|
-
} else {
|
|
1717
|
-
try {
|
|
1718
|
-
const parts = jwt.split('.');
|
|
1719
|
-
if (parts.length !== 3) throw new Error('not a JWT');
|
|
1720
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
|
|
1721
|
-
sub?: string; exp?: number;
|
|
1722
|
-
};
|
|
1723
|
-
const expMs = payload.exp ? payload.exp * 1000 : null;
|
|
1724
|
-
const minsLeft = expMs ? Math.floor((expMs - Date.now()) / 60_000) : null;
|
|
1725
|
-
if (expMs && expMs < Date.now()) {
|
|
1726
|
-
fail(`JWT expired ${Math.abs(minsLeft!)} minute(s) ago — get a new one: https://cachly.dev/setup-ai`);
|
|
1727
|
-
} else if (minsLeft !== null && minsLeft < 30) {
|
|
1728
|
-
warn(`JWT expires in ${minsLeft} minute(s) — refresh soon at https://cachly.dev/setup-ai`);
|
|
1729
|
-
} else {
|
|
1730
|
-
ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
|
|
1731
|
-
}
|
|
1732
|
-
} catch {
|
|
1733
|
-
fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
// ── 2. Brain API reachability ───────────────────────────────────────────────
|
|
1738
|
-
console.log('\n🌐 Brain API');
|
|
1739
|
-
const apiUrl = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
1740
|
-
try {
|
|
1741
|
-
const res = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(5000) });
|
|
1742
|
-
if (res.ok) {
|
|
1743
|
-
const body = await res.json() as { status?: string };
|
|
1744
|
-
ok(`${apiUrl} reachable (status: ${body.status ?? 'ok'})`);
|
|
1745
|
-
} else if (res.status === 401 || res.status === 403) {
|
|
1746
|
-
ok(`${apiUrl} reachable (auth required — expected)`);
|
|
1747
|
-
} else {
|
|
1748
|
-
fail(`${apiUrl}/health returned HTTP ${res.status}`);
|
|
1749
|
-
}
|
|
1750
|
-
} catch (e) {
|
|
1751
|
-
fail(`${apiUrl} unreachable — ${(e as Error).message}`);
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
// ── 3. Brain instance ───────────────────────────────────────────────────────
|
|
1755
|
-
console.log('\n🧠 Brain instance');
|
|
1756
|
-
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1757
|
-
if (!instanceId) {
|
|
1758
|
-
warn('CACHLY_BRAIN_INSTANCE_ID not set — will auto-resolve on first tool call');
|
|
1759
|
-
} else if (jwt) {
|
|
1760
|
-
try {
|
|
1761
|
-
const instRes = await fetch(`${apiUrl}/api/v1/instances/${instanceId}`, {
|
|
1762
|
-
headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' },
|
|
1763
|
-
signal: AbortSignal.timeout(5000),
|
|
1764
|
-
});
|
|
1765
|
-
if (instRes.ok) {
|
|
1766
|
-
const inst = await instRes.json() as { name?: string; status?: string; host?: string; port?: number; tls_enabled?: boolean };
|
|
1767
|
-
if (inst.status === 'running') {
|
|
1768
|
-
ok(`Instance "${inst.name}" (${instanceId.slice(0, 8)}…) is running`);
|
|
1769
|
-
// Attempt actual Redis PING to verify connectivity end-to-end
|
|
1770
|
-
console.log('\n🔌 Redis connectivity');
|
|
1771
|
-
if (inst.host && inst.port) {
|
|
1772
|
-
try {
|
|
1773
|
-
let pingPassword: string | undefined;
|
|
1774
|
-
let pingTls = inst.tls_enabled !== false;
|
|
1775
|
-
try {
|
|
1776
|
-
const connRes = await fetch(`${apiUrl}/api/v1/instances/${instanceId}/connection`, {
|
|
1777
|
-
headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' },
|
|
1778
|
-
signal: AbortSignal.timeout(5000),
|
|
1779
|
-
});
|
|
1780
|
-
if (connRes.ok) {
|
|
1781
|
-
const conn = await connRes.json() as { password?: string; tls_enabled?: boolean };
|
|
1782
|
-
pingPassword = conn.password;
|
|
1783
|
-
pingTls = conn.tls_enabled !== false;
|
|
1784
|
-
}
|
|
1785
|
-
} catch { /* proceed without password */ }
|
|
1786
|
-
const pingClient = new Redis({
|
|
1787
|
-
host: inst.host, port: inst.port,
|
|
1788
|
-
password: pingPassword,
|
|
1789
|
-
...(pingTls ? { tls: {} } : {}),
|
|
1790
|
-
lazyConnect: true, connectTimeout: 5000, retryStrategy: () => null,
|
|
1791
|
-
});
|
|
1792
|
-
await pingClient.connect();
|
|
1793
|
-
const pong = await pingClient.ping();
|
|
1794
|
-
pingClient.disconnect();
|
|
1795
|
-
if (pong === 'PONG') {
|
|
1796
|
-
ok(`Redis PING → PONG (${inst.host}:${inst.port})`);
|
|
1797
|
-
} else {
|
|
1798
|
-
warn(`Redis responded but not PONG: ${pong}`);
|
|
1799
|
-
}
|
|
1800
|
-
} catch (pingErr) {
|
|
1801
|
-
fail(`Redis unreachable — ${(pingErr as Error).message}`);
|
|
1802
|
-
}
|
|
1803
|
-
} else {
|
|
1804
|
-
warn('Instance has no host/port yet');
|
|
1805
|
-
}
|
|
1806
|
-
} else {
|
|
1807
|
-
warn(`Instance "${inst.name}" status: ${inst.status}`);
|
|
1808
|
-
}
|
|
1809
|
-
} else {
|
|
1810
|
-
fail(`Instance ${instanceId.slice(0, 8)}… not found (HTTP ${instRes.status})`);
|
|
1811
|
-
}
|
|
1812
|
-
} catch (e) {
|
|
1813
|
-
fail(`Could not check instance — ${(e as Error).message}`);
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
// ── 4. Editor configs ───────────────────────────────────────────────────────
|
|
1818
|
-
console.log('\n🛠 Editor configs (project dir)');
|
|
1819
|
-
const editorConfigs: Array<{ editor: string; path: string }> = [
|
|
1820
|
-
{ editor: 'Claude Code', path: '.mcp.json' },
|
|
1821
|
-
{ editor: 'Cursor', path: '.cursor/mcp.json' },
|
|
1822
|
-
{ editor: 'Windsurf', path: '.windsurf/mcp.json' },
|
|
1823
|
-
{ editor: 'Copilot / Cline', path: '.vscode/mcp.json' },
|
|
1824
|
-
{ editor: 'Zed', path: '.zed/settings.json' },
|
|
1825
|
-
{ editor: 'Continue.dev', path: '.continue/config.json' },
|
|
1826
|
-
];
|
|
1827
|
-
let configsFound = 0;
|
|
1828
|
-
for (const { editor, path } of editorConfigs) {
|
|
1829
|
-
const fullPath = resolve(cwd, path);
|
|
1830
|
-
if (existsSync(fullPath)) {
|
|
1831
|
-
try {
|
|
1832
|
-
const content = await readFile(fullPath, 'utf-8');
|
|
1833
|
-
const hasCachly = content.includes('@cachly-dev/mcp-server') || content.includes('cachly');
|
|
1834
|
-
if (hasCachly) {
|
|
1835
|
-
ok(`${editor}: ${path}`);
|
|
1836
|
-
configsFound++;
|
|
1837
|
-
} else {
|
|
1838
|
-
warn(`${editor}: ${path} exists but cachly not found inside`);
|
|
1839
|
-
}
|
|
1840
|
-
} catch {
|
|
1841
|
-
warn(`${editor}: ${path} exists but could not read`);
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
if (configsFound === 0) {
|
|
1846
|
-
fail('No editor MCP configs found — run: npx @cachly-dev/mcp-server@latest setup');
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
// ── 5. Git hook ─────────────────────────────────────────────────────────────
|
|
1850
|
-
console.log('\n🪝 Git hook (ambient learning)');
|
|
1851
|
-
const hookPath = resolve(cwd, '.git', 'hooks', 'post-commit');
|
|
1852
|
-
if (!existsSync(resolve(cwd, '.git'))) {
|
|
1853
|
-
warn('Not in a git repository — skip');
|
|
1854
|
-
} else if (existsSync(hookPath)) {
|
|
1855
|
-
try {
|
|
1856
|
-
const hook = await readFile(hookPath, 'utf-8');
|
|
1857
|
-
if (hook.includes('cachly')) {
|
|
1858
|
-
ok('.git/hooks/post-commit (cachly CLS hook present)');
|
|
1859
|
-
} else {
|
|
1860
|
-
warn('.git/hooks/post-commit exists but cachly hook not found — run setup to add it');
|
|
1861
|
-
}
|
|
1862
|
-
} catch {
|
|
1863
|
-
warn('.git/hooks/post-commit exists but could not read');
|
|
1864
|
-
}
|
|
1865
|
-
} else {
|
|
1866
|
-
warn('.git/hooks/post-commit not found — run: npx @cachly-dev/mcp-server@latest setup');
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
1870
|
-
console.log(`\n${'─'.repeat(40)}`);
|
|
1871
|
-
if (failed === 0) {
|
|
1872
|
-
console.log(`✅ All checks passed (${passed} ok)\n`);
|
|
1873
|
-
} else {
|
|
1874
|
-
console.log(`❌ ${failed} check(s) failed, ${passed} passed\n`);
|
|
1875
|
-
console.log(`💡 Fix issues with: npx @cachly-dev/mcp-server@latest setup\n`);
|
|
1876
|
-
process.exit(1);
|
|
1877
|
-
}
|
|
1137
|
+
console.log(` Restart your editor — the \`cachly\` MCP tools will appear.\n`);
|
|
1878
1138
|
process.exit(0);
|
|
1879
1139
|
}
|
|
1880
1140
|
|
|
@@ -1887,8 +1147,6 @@ if (process.argv[2] === 'setup') {
|
|
|
1887
1147
|
const { resolve, dirname } = await import('node:path');
|
|
1888
1148
|
const { createInterface } = await import('node:readline');
|
|
1889
1149
|
|
|
1890
|
-
sendFunnelEvent('setup_started');
|
|
1891
|
-
|
|
1892
1150
|
// --yes / -y → non-interactive mode (skips all prompts, picks defaults)
|
|
1893
1151
|
const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
1894
1152
|
|
|
@@ -2114,22 +1372,18 @@ if (process.argv[2] === 'setup') {
|
|
|
2114
1372
|
console.log(`Step 3: Configuring for: ${editorsToSetup.map(editorLabel).join(', ')}\n`);
|
|
2115
1373
|
|
|
2116
1374
|
// ── Step 4: Write editor configs (project-level) ─────────────────────────
|
|
2117
|
-
//
|
|
2118
|
-
//
|
|
2119
|
-
|
|
1375
|
+
// Critical: NEVER overwrite existing configs blindly — merge cachly into the
|
|
1376
|
+
// existing mcpServers map so the user's other MCP servers (filesystem,
|
|
1377
|
+
// github, postgres, …) are preserved. writeMergedMcpConfig also drops a
|
|
1378
|
+
// .cachly-backup file next to the original for one-step recovery.
|
|
2120
1379
|
for (const editor of editorsToSetup) {
|
|
2121
1380
|
const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
|
|
2122
1381
|
const configPath = resolve(cwd, configFile);
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
console.log(`✅ ${wasExisting ? 'Updated' : 'Written'}: ${configFile}`);
|
|
2129
|
-
} catch (writeErr) {
|
|
2130
|
-
console.log(`⚠️ Could not write ${configFile}: ${(writeErr as Error).message}`);
|
|
2131
|
-
console.log(` Fix permissions or run with sudo, then re-run setup.`);
|
|
2132
|
-
}
|
|
1382
|
+
const result = await writeMergedMcpConfig(configPath, token, instance.id, editor);
|
|
1383
|
+
const verb = result === 'created' ? '✅ Written'
|
|
1384
|
+
: result === 'updated' ? '✅ Updated (kept existing servers)'
|
|
1385
|
+
: '✅ Merged (other servers untouched)';
|
|
1386
|
+
console.log(`${verb}: ${configFile}`);
|
|
2133
1387
|
}
|
|
2134
1388
|
|
|
2135
1389
|
// ── Step 4b: Write global Claude Code config (~/.claude/mcp.json) ─────────
|
|
@@ -2250,9 +1504,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2250
1504
|
|
|
2251
1505
|
console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
|
|
2252
1506
|
console.log(` Your AI now has persistent memory across every session.\n`);
|
|
2253
|
-
console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
|
|
2254
|
-
console.log(`\n 📛 Add a live badge to your README:`);
|
|
2255
|
-
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
1507
|
+
console.log(` Dashboard: https://cachly.dev/instances/${instance.id}\n`);
|
|
2256
1508
|
|
|
2257
1509
|
// ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
|
|
2258
1510
|
if (!nonInteractive) {
|
|
@@ -2352,9 +1604,7 @@ if (process.argv[2] === 'index') {
|
|
|
2352
1604
|
|
|
2353
1605
|
// Warn on stderr when credentials are missing so the user sees a clear
|
|
2354
1606
|
// actionable message in their editor's MCP log instead of silent failures.
|
|
2355
|
-
|
|
2356
|
-
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
|
|
2357
|
-
if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
1607
|
+
if (!JWT) {
|
|
2358
1608
|
process.stderr.write(
|
|
2359
1609
|
'\n' +
|
|
2360
1610
|
'╔══════════════════════════════════════════════════════════════════╗\n' +
|