@cachly-dev/mcp-server 0.10.26 → 0.10.40
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/.env.test.example +23 -0
- 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 +252 -0
- package/LAUNCH_TEXTS.md +182 -0
- package/LICENSE +202 -0
- package/README.md +142 -112
- 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/__tests__/e2e/api-auth.test.d.ts +7 -0
- package/dist/__tests__/e2e/api-auth.test.js +162 -0
- package/dist/__tests__/e2e/api-auth.test.js.map +1 -0
- package/dist/__tests__/e2e/api-health.test.d.ts +7 -0
- package/dist/__tests__/e2e/api-health.test.js +95 -0
- package/dist/__tests__/e2e/api-health.test.js.map +1 -0
- package/dist/__tests__/e2e/cli.test.d.ts +10 -0
- package/dist/__tests__/e2e/cli.test.js +204 -0
- package/dist/__tests__/e2e/cli.test.js.map +1 -0
- package/dist/__tests__/e2e/config.d.ts +35 -0
- package/dist/__tests__/e2e/config.js +53 -0
- package/dist/__tests__/e2e/config.js.map +1 -0
- package/dist/__tests__/e2e/funnel.test.d.ts +13 -0
- package/dist/__tests__/e2e/funnel.test.js +90 -0
- package/dist/__tests__/e2e/funnel.test.js.map +1 -0
- package/dist/__tests__/e2e/mcp-tools.test.d.ts +7 -0
- package/dist/__tests__/e2e/mcp-tools.test.js +129 -0
- package/dist/__tests__/e2e/mcp-tools.test.js.map +1 -0
- package/dist/__tests__/e2e/provisioning.test.d.ts +15 -0
- package/dist/__tests__/e2e/provisioning.test.js +123 -0
- package/dist/__tests__/e2e/provisioning.test.js.map +1 -0
- package/dist/handlers/brain.js +119 -82
- 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 +1264 -234
- 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/eslint.config.js +16 -0
- package/glama.json +36 -0
- package/llms.txt +155 -0
- package/package.json +38 -14
- package/scripts/e2e-run.sh +160 -0
- package/scripts/postinstall.js +12 -12
- package/server.json +3 -9
- package/smithery.yaml +15 -8
- package/src/__tests__/auth.test.ts +147 -0
- package/src/__tests__/e2e/api-auth.test.ts +182 -0
- package/src/__tests__/e2e/api-health.test.ts +103 -0
- package/src/__tests__/e2e/cli.test.ts +228 -0
- package/src/__tests__/e2e/config.ts +53 -0
- package/src/__tests__/e2e/funnel.test.ts +99 -0
- package/src/__tests__/e2e/mcp-tools.test.ts +191 -0
- package/src/__tests__/e2e/provisioning.test.ts +128 -0
- package/src/handlers/brain.ts +118 -78
- 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 +1312 -257
- package/src/tools.ts +8 -66
- package/vitest.config.ts +12 -0
- 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/src/index.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { jwtExpiryMs, checkJwt, handleApiError } from './auth.js';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { execSync } from 'node:child_process';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
// True only when this file is the entry point (not imported by tests or other modules).
|
|
9
|
+
const _isMain = process.argv[1] != null &&
|
|
10
|
+
(process.argv[1] === fileURLToPath(import.meta.url) ||
|
|
11
|
+
process.argv[1].endsWith('/dist/index.js') ||
|
|
12
|
+
process.argv[1].endsWith('/src/index.ts'));
|
|
6
13
|
/**
|
|
7
14
|
* cachly MCP Server v0.4.0
|
|
8
15
|
*
|
|
@@ -53,7 +60,7 @@ import { Redis } from 'ioredis';
|
|
|
53
60
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
54
61
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
55
62
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
56
|
-
const CURRENT_VERSION = '0.10.
|
|
63
|
+
const CURRENT_VERSION = '0.10.40';
|
|
57
64
|
|
|
58
65
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
59
66
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
@@ -159,6 +166,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
159
166
|
JWT = apiKey;
|
|
160
167
|
setEmbedJwt(apiKey); // keep embeddings.ts in sync
|
|
161
168
|
_deviceFlow = null;
|
|
169
|
+
sendFunnelEvent('device_flow_completed');
|
|
162
170
|
// Auto-provision: find or create the user's brain instance.
|
|
163
171
|
_defaultInstanceLastAttempt = 0;
|
|
164
172
|
await resolveDefaultInstanceId();
|
|
@@ -177,8 +185,16 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
177
185
|
};
|
|
178
186
|
const id = body.instance?.id ?? body.instance_id;
|
|
179
187
|
if (id) _defaultInstanceId = id;
|
|
188
|
+
} else {
|
|
189
|
+
// Log so telemetry can detect free-tier auto-provision failures
|
|
190
|
+
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
193
|
+
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
194
|
+
signal: AbortSignal.timeout(3000),
|
|
195
|
+
}).catch(() => {});
|
|
180
196
|
}
|
|
181
|
-
} catch { /* non-fatal */ }
|
|
197
|
+
} catch { /* non-fatal — server unreachable, getConnection will surface the error */ }
|
|
182
198
|
}
|
|
183
199
|
// Give a provisioning instance a head-start so getConnection's wait is shorter.
|
|
184
200
|
// The instance is almost always ready by the time the user's tool re-enters.
|
|
@@ -289,11 +305,24 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
289
305
|
body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
|
|
290
306
|
signal: AbortSignal.timeout(3000),
|
|
291
307
|
}).catch(() => {});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
308
|
+
let hint: string;
|
|
309
|
+
if (inst.status === 'provisioning') {
|
|
310
|
+
hint = `⏳ Brain instance "${inst.name}" is still starting up.\n\nFirst-time provisioning typically takes 1–3 minutes. Please retry in a moment — the instance will be ready soon.`;
|
|
311
|
+
} else if (inst.status === 'failed') {
|
|
312
|
+
hint = `❌ Brain instance "${inst.name}" failed to start.\n\n` +
|
|
313
|
+
`Our system will retry automatically. Check status at: https://cachly.dev/instances\n` +
|
|
314
|
+
`If this persists, contact support@cachly.dev.`;
|
|
315
|
+
} else if (inst.status === 'suspended') {
|
|
316
|
+
hint = `⏸ Brain instance "${inst.name}" is suspended (billing issue).\n\n` +
|
|
317
|
+
`Update your payment method at: https://cachly.dev/billing`;
|
|
318
|
+
} else if (inst.status === 'pending_payment') {
|
|
319
|
+
hint = `💳 Brain instance "${inst.name}" is waiting for payment.\n\n` +
|
|
320
|
+
`Complete your checkout at: https://cachly.dev/instances`;
|
|
321
|
+
} else {
|
|
322
|
+
hint = `Brain instance "${inst.name}" is not reachable (status: ${inst.status}).\n\n` +
|
|
295
323
|
`• View your instance at: https://cachly.dev/instances\n` +
|
|
296
324
|
`• Run \`get_api_status\` for a full diagnostic.`;
|
|
325
|
+
}
|
|
297
326
|
throw new McpError(ErrorCode.InvalidRequest, hint);
|
|
298
327
|
}
|
|
299
328
|
if (!inst.host || !inst.port) {
|
|
@@ -310,8 +339,15 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
310
339
|
);
|
|
311
340
|
password = conn.password ?? undefined;
|
|
312
341
|
tlsEnabled = conn.tls_enabled !== false;
|
|
313
|
-
} catch {
|
|
314
|
-
//
|
|
342
|
+
} catch (connErr) {
|
|
343
|
+
// If the connection endpoint fails and the instance has a password, we cannot connect.
|
|
344
|
+
// Surface the error so the user knows why Redis auth will fail rather than getting a cryptic NOAUTH.
|
|
345
|
+
const msg = connErr instanceof Error ? connErr.message : String(connErr);
|
|
346
|
+
if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
|
|
347
|
+
throw new McpError(ErrorCode.InvalidRequest,
|
|
348
|
+
`API key rejected — run \`cachly setup\` to refresh your credentials.`);
|
|
349
|
+
}
|
|
350
|
+
// Other failures (timeout, 500): proceed without password, Redis will surface NOAUTH if needed
|
|
315
351
|
}
|
|
316
352
|
|
|
317
353
|
const client = new Redis({
|
|
@@ -400,21 +436,40 @@ import { TOOLS } from './tools.js';
|
|
|
400
436
|
|
|
401
437
|
// Fires once per process when no JWT is set (anonymous, opt-out via CACHLY_NO_TELEMETRY=1)
|
|
402
438
|
let _telemetryPingSent = false;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const editor = process.env.CURSOR_TRACE_ID ? 'cursor'
|
|
439
|
+
// Fires once per process on the first successful Brain tool call — key activation metric
|
|
440
|
+
let _firstCallSuccessSent = false;
|
|
441
|
+
|
|
442
|
+
function detectEditor(): string {
|
|
443
|
+
return process.env.CURSOR_TRACE_ID ? 'cursor'
|
|
409
444
|
: process.env.WINDSURF_SESSION_ID ? 'windsurf'
|
|
410
445
|
: process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
|
|
411
446
|
: process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
|
|
412
447
|
: 'unknown';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function sendFunnelEvent(event: string, extra?: Record<string, unknown>): void {
|
|
451
|
+
if (process.env.CACHLY_NO_TELEMETRY === '1') return;
|
|
452
|
+
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
453
|
+
method: 'POST',
|
|
454
|
+
headers: { 'Content-Type': 'application/json' },
|
|
455
|
+
body: JSON.stringify({
|
|
456
|
+
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
457
|
+
...(JWT ? { jwt: JWT } : {}),
|
|
458
|
+
...extra,
|
|
459
|
+
}),
|
|
460
|
+
signal: AbortSignal.timeout(3000),
|
|
461
|
+
}).catch(() => {/* fire-and-forget */});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function sendAnonymousTelemetry(toolName: string): Promise<void> {
|
|
465
|
+
if (_telemetryPingSent) return;
|
|
466
|
+
if (process.env.CACHLY_NO_TELEMETRY === '1') return;
|
|
467
|
+
_telemetryPingSent = true;
|
|
413
468
|
try {
|
|
414
469
|
await fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
415
470
|
method: 'POST',
|
|
416
471
|
headers: { 'Content-Type': 'application/json' },
|
|
417
|
-
body: JSON.stringify({ event: 'first_call_no_jwt', version: CURRENT_VERSION, editor, tool: toolName }),
|
|
472
|
+
body: JSON.stringify({ event: 'first_call_no_jwt', version: CURRENT_VERSION, editor: detectEditor(), tool: toolName }),
|
|
418
473
|
signal: AbortSignal.timeout(3000),
|
|
419
474
|
});
|
|
420
475
|
} catch { /* fire-and-forget, never block the user */ }
|
|
@@ -453,18 +508,28 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
453
508
|
const flow = await startDeviceFlow();
|
|
454
509
|
if (flow) {
|
|
455
510
|
_deviceFlow = flow;
|
|
511
|
+
sendFunnelEvent('device_flow_started', { tool: name });
|
|
512
|
+
// Try to open the browser automatically — fire-and-forget, never block
|
|
513
|
+
void (async () => {
|
|
514
|
+
try {
|
|
515
|
+
const { exec } = await import('node:child_process');
|
|
516
|
+
const url = flow.verifyUrl;
|
|
517
|
+
const cmd = process.platform === 'win32' ? `start "" "${url}"`
|
|
518
|
+
: process.platform === 'darwin' ? `open "${url}"`
|
|
519
|
+
: `xdg-open "${url}"`;
|
|
520
|
+
exec(cmd);
|
|
521
|
+
} catch { /* non-critical */ }
|
|
522
|
+
})();
|
|
456
523
|
return [
|
|
457
|
-
'🧠 **cachly AI Brain —
|
|
524
|
+
'🧠 **cachly AI Brain — sign in to activate** (browser opening...)',
|
|
458
525
|
'',
|
|
459
|
-
|
|
460
|
-
` **${flow.verifyUrl}**`,
|
|
526
|
+
`👉 **${flow.verifyUrl}**`,
|
|
461
527
|
'',
|
|
462
|
-
`
|
|
528
|
+
`Code: **${flow.userCode}** (pre-filled if browser opened automatically)`,
|
|
463
529
|
'',
|
|
464
|
-
'
|
|
530
|
+
'After sign-in: call **any tool again** — your Brain activates instantly.',
|
|
465
531
|
'',
|
|
466
|
-
'✨ Free
|
|
467
|
-
' No credit card required.',
|
|
532
|
+
'✨ Free forever · No credit card · 89 MCP tools · GDPR · EU servers',
|
|
468
533
|
].join('\n');
|
|
469
534
|
}
|
|
470
535
|
|
|
@@ -491,11 +556,81 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
491
556
|
|
|
492
557
|
// Delegate brain tools (learn, recall, session, etc.)
|
|
493
558
|
const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
494
|
-
if (brainResult !== null)
|
|
559
|
+
if (brainResult !== null) {
|
|
560
|
+
if (!_firstCallSuccessSent && JWT) {
|
|
561
|
+
_firstCallSuccessSent = true;
|
|
562
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
563
|
+
}
|
|
564
|
+
// Per-tool telemetry — fires after every successful brain tool call.
|
|
565
|
+
// api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
|
|
566
|
+
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
567
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
568
|
+
if (name === 'recall_best_solution') {
|
|
569
|
+
// recall_best_solution → increments BrainRecallCount + triggers level-up logic
|
|
570
|
+
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
571
|
+
} else if (name === 'learn_from_attempts') {
|
|
572
|
+
sendFunnelEvent('learn_from_attempts', telemetryExtra);
|
|
573
|
+
} else if (name === 'session_start') {
|
|
574
|
+
sendFunnelEvent('session_start', telemetryExtra);
|
|
575
|
+
const resultText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
576
|
+
? JSON.stringify(brainResult)
|
|
577
|
+
: String(brainResult ?? '');
|
|
578
|
+
const isFirstSession = resultText.includes('Welcome! Your AI Brain is live.');
|
|
579
|
+
|
|
580
|
+
if (!isFirstSession && resultText.length > 100) {
|
|
581
|
+
// Existing brain — session_start acts as implicit recall → increment counter.
|
|
582
|
+
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
583
|
+
} else if (isFirstSession && args.workspace_path) {
|
|
584
|
+
// First session with a known workspace: auto-bootstrap from git history.
|
|
585
|
+
// Runs synchronously so the user sees the result in the same response.
|
|
586
|
+
try {
|
|
587
|
+
const gitBootstrap = await handleFedbrainTool('brain_from_git', {
|
|
588
|
+
instance_id: instanceId,
|
|
589
|
+
repo_path: args.workspace_path as string,
|
|
590
|
+
limit: 50,
|
|
591
|
+
}, getConnection, apiFetch);
|
|
592
|
+
if (gitBootstrap !== null) {
|
|
593
|
+
sendFunnelEvent('brain_from_git', telemetryExtra);
|
|
594
|
+
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
595
|
+
// Append bootstrap summary to the session_start briefing.
|
|
596
|
+
const bootstrapText = typeof gitBootstrap === 'object' && gitBootstrap !== null && 'content' in gitBootstrap
|
|
597
|
+
? (gitBootstrap as { content: { text?: string }[] }).content.map((c) => c.text ?? '').join('\n')
|
|
598
|
+
: String(gitBootstrap);
|
|
599
|
+
if (typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult) {
|
|
600
|
+
(brainResult as { content: { type: string; text: string }[] }).content.push({
|
|
601
|
+
type: 'text',
|
|
602
|
+
text: '\n---\n' + bootstrapText,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} catch { /* git bootstrap errors must never break session_start */ }
|
|
607
|
+
}
|
|
608
|
+
} else if (name === 'session_end') {
|
|
609
|
+
sendFunnelEvent('session_end', telemetryExtra);
|
|
610
|
+
} else if (name === 'smart_recall') {
|
|
611
|
+
sendFunnelEvent('smart_recall', telemetryExtra);
|
|
612
|
+
// smart_recall is the primary recall tool in CLAUDE.md — it retrieves brain lessons
|
|
613
|
+
// just like recall_best_solution. Count it toward BrainRecallCount so the dashboard
|
|
614
|
+
// nudge clears and the first-recall email fires for users following CLAUDE.md.
|
|
615
|
+
const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
616
|
+
? JSON.stringify(brainResult)
|
|
617
|
+
: String(brainResult ?? '');
|
|
618
|
+
if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons')) {
|
|
619
|
+
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return brainResult;
|
|
623
|
+
}
|
|
495
624
|
|
|
496
625
|
// Delegate context tools (remember/recall/list/forget)
|
|
497
626
|
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
498
|
-
if (contextResult !== null)
|
|
627
|
+
if (contextResult !== null) {
|
|
628
|
+
if (!_firstCallSuccessSent && JWT) {
|
|
629
|
+
_firstCallSuccessSent = true;
|
|
630
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
631
|
+
}
|
|
632
|
+
return contextResult;
|
|
633
|
+
}
|
|
499
634
|
|
|
500
635
|
// Delegate instance + cache tools
|
|
501
636
|
const instanceResult = await handleInstanceTool(name, args, getConnection, apiFetch);
|
|
@@ -515,10 +650,28 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
515
650
|
if (advancedResult !== null) return advancedResult;
|
|
516
651
|
|
|
517
652
|
const syndicateResult = await handleSyndicateTool(name, args, getConnection, apiFetch);
|
|
518
|
-
if (syndicateResult !== null)
|
|
653
|
+
if (syndicateResult !== null) {
|
|
654
|
+
// brain_predict is in syndicate handler — track telemetry here.
|
|
655
|
+
if (name === 'brain_predict') {
|
|
656
|
+
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
657
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
658
|
+
sendFunnelEvent('brain_predict', telemetryExtra);
|
|
659
|
+
}
|
|
660
|
+
return syndicateResult;
|
|
661
|
+
}
|
|
519
662
|
|
|
520
663
|
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
521
|
-
if (fedbrainResult !== null)
|
|
664
|
+
if (fedbrainResult !== null) {
|
|
665
|
+
// brain_from_git and brain_predict_failures are in fedbrain handler.
|
|
666
|
+
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
667
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
668
|
+
if (name === 'brain_from_git') {
|
|
669
|
+
sendFunnelEvent('brain_from_git', telemetryExtra);
|
|
670
|
+
} else if (name === 'brain_predict_failures') {
|
|
671
|
+
sendFunnelEvent('brain_predict_failures', telemetryExtra);
|
|
672
|
+
}
|
|
673
|
+
return fedbrainResult;
|
|
674
|
+
}
|
|
522
675
|
|
|
523
676
|
switch (name) {
|
|
524
677
|
// ── Instance management ──────────────────────────────────────────────
|
|
@@ -635,33 +788,7 @@ const server = new Server(
|
|
|
635
788
|
{ capabilities: { tools: {} } }
|
|
636
789
|
);
|
|
637
790
|
|
|
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
|
-
});
|
|
791
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
665
792
|
|
|
666
793
|
// ── Auto-session management ───────────────────────────────────────────────────
|
|
667
794
|
// Transparently starts a Brain session on the first tool call of a process
|
|
@@ -670,91 +797,59 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
670
797
|
|
|
671
798
|
let _autoSessionInstanceId: string | null = null;
|
|
672
799
|
let _autoSessionStarted = false;
|
|
673
|
-
let _autoSessionStarting: Promise<
|
|
800
|
+
let _autoSessionStarting: Promise<void> | null = null; // guard against double-start race
|
|
674
801
|
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
|
|
677
802
|
|
|
678
803
|
// ── brain_from_git concurrency limiter ────────────────────────────────────────
|
|
679
804
|
// Spawning many `git log` subprocesses in parallel can fork-bomb the host.
|
|
680
805
|
// This simple semaphore caps concurrent brain_from_git calls to 10 per process.
|
|
681
|
-
async function autoStartSession(instanceId: string): Promise<
|
|
682
|
-
if (_autoSessionStarted) return
|
|
806
|
+
async function autoStartSession(instanceId: string): Promise<void> {
|
|
807
|
+
if (_autoSessionStarted) return;
|
|
683
808
|
// If another call already started the session, await it instead of double-starting.
|
|
684
|
-
if (_autoSessionStarting) {
|
|
685
|
-
_autoSessionStarting = (async ()
|
|
809
|
+
if (_autoSessionStarting) { await _autoSessionStarting; return; }
|
|
810
|
+
_autoSessionStarting = (async () => {
|
|
686
811
|
_autoSessionStarted = true;
|
|
687
812
|
_autoSessionInstanceId = instanceId;
|
|
688
|
-
let sessionResult: string | null = null;
|
|
689
813
|
try {
|
|
690
|
-
|
|
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;
|
|
814
|
+
await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)', workspace_path: process.cwd() });
|
|
706
815
|
} catch { /* non-fatal — session tracking is a best-effort feature */ } finally {
|
|
707
816
|
_autoSessionStarting = null;
|
|
708
817
|
}
|
|
709
|
-
|
|
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
818
|
})();
|
|
736
|
-
|
|
819
|
+
await _autoSessionStarting;
|
|
820
|
+
|
|
821
|
+
// Auto-index the project if it hasn't been indexed in the last 24h.
|
|
822
|
+
// This is the main lever for growing token savings: more indexed code →
|
|
823
|
+
// more semantic cache hits → fewer LLM calls for repeated questions.
|
|
824
|
+
if (process.env.CACHLY_AUTO_INDEX !== 'false') {
|
|
825
|
+
try {
|
|
826
|
+
const redis = await getConnection(instanceId);
|
|
827
|
+
const lastIndexed = await redis.get(`cachly:index:last_indexed:${instanceId}`);
|
|
828
|
+
const staleMs = 24 * 60 * 60 * 1000;
|
|
829
|
+
const isStale = !lastIndexed || (Date.now() - parseInt(lastIndexed, 10)) > staleMs;
|
|
830
|
+
if (isStale) {
|
|
831
|
+
// Mark as indexing now to prevent concurrent re-runs.
|
|
832
|
+
await redis.set(`cachly:index:last_indexed:${instanceId}`, String(Date.now()), 'EX', 90000);
|
|
833
|
+
// Run in background — don't block the first tool call.
|
|
834
|
+
handleTool('index_project', {
|
|
835
|
+
instance_id: instanceId,
|
|
836
|
+
dir: process.cwd(),
|
|
837
|
+
max_files: 150,
|
|
838
|
+
ttl: 86400 * 7, // cache for 7 days
|
|
839
|
+
namespace: 'cachly:sem:code',
|
|
840
|
+
}).catch(() => undefined);
|
|
841
|
+
}
|
|
842
|
+
} catch { /* never block the session on indexing errors */ }
|
|
843
|
+
}
|
|
737
844
|
}
|
|
738
845
|
|
|
739
846
|
async function autoEndSession(): Promise<void> {
|
|
740
847
|
if (!_autoSessionStarted || !_autoSessionInstanceId) return;
|
|
741
848
|
_autoSessionStarted = false;
|
|
742
849
|
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 */ }
|
|
755
850
|
await handleTool('session_end', {
|
|
756
851
|
instance_id: _autoSessionInstanceId,
|
|
757
|
-
summary
|
|
852
|
+
summary: `Auto-ended after ${_autoSessionToolCount} tool call(s). Session was started automatically.`,
|
|
758
853
|
files_changed: [],
|
|
759
854
|
lessons_learned: 0,
|
|
760
855
|
});
|
|
@@ -764,7 +859,7 @@ async function autoEndSession(): Promise<void> {
|
|
|
764
859
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
765
860
|
const { name, arguments: args } = request.params;
|
|
766
861
|
|
|
767
|
-
// Auto-start brain session
|
|
862
|
+
// Auto-start brain session on first tool call that has an instance_id.
|
|
768
863
|
// Skip session management tools to avoid recursion.
|
|
769
864
|
const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
|
|
770
865
|
if (!sessionTools.has(name) && !_autoSessionStarted && !_autoSessionStarting) {
|
|
@@ -782,18 +877,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
782
877
|
|
|
783
878
|
try {
|
|
784
879
|
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
880
|
return { content: [{ type: 'text', text }] };
|
|
798
881
|
} catch (err) {
|
|
799
882
|
if (err instanceof McpError) throw err;
|
|
@@ -873,122 +956,61 @@ function buildMcpConfig(apiKey: string, instanceId: string, editor: string): str
|
|
|
873
956
|
}, null, 2);
|
|
874
957
|
}
|
|
875
958
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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 {
|
|
899
|
-
command: 'npx',
|
|
900
|
-
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
901
|
-
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
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(
|
|
959
|
+
// mergeMcpConfig reads an existing config file (if any), merges the cachly entry,
|
|
960
|
+
// and returns the updated JSON string — preserving all other MCP servers and settings.
|
|
961
|
+
async function mergeMcpConfig(
|
|
916
962
|
configPath: string,
|
|
917
963
|
apiKey: string,
|
|
918
964
|
instanceId: string,
|
|
919
965
|
editor: string,
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
966
|
+
fsOps: { readFile: (p: string, enc: BufferEncoding) => Promise<string>; existsSync: (p: string) => boolean },
|
|
967
|
+
): Promise<string> {
|
|
968
|
+
const cachlyEntry = {
|
|
969
|
+
command: 'npx',
|
|
970
|
+
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
971
|
+
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
972
|
+
};
|
|
926
973
|
|
|
927
|
-
|
|
928
|
-
if (!existsSync(configPath)) {
|
|
929
|
-
await writeFile(configPath, buildMcpConfig(apiKey, instanceId, editor), 'utf-8');
|
|
930
|
-
return 'created';
|
|
931
|
-
}
|
|
974
|
+
if (!fsOps.existsSync(configPath)) return buildMcpConfig(apiKey, instanceId, editor);
|
|
932
975
|
|
|
933
|
-
|
|
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>;
|
|
976
|
+
let existing: Record<string, unknown> = {};
|
|
938
977
|
try {
|
|
939
|
-
existing = JSON.parse(
|
|
940
|
-
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
941
|
-
throw new Error('config root is not an object');
|
|
942
|
-
}
|
|
978
|
+
existing = JSON.parse(await fsOps.readFile(configPath, 'utf-8')) as Record<string, unknown>;
|
|
943
979
|
} catch {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
980
|
+
// Corrupt or unreadable JSON — back it up by appending .bak, then start fresh
|
|
981
|
+
try {
|
|
982
|
+
const { rename } = await import('node:fs/promises');
|
|
983
|
+
await rename(configPath, configPath + '.bak');
|
|
984
|
+
process.stderr.write(`⚠️ ${configPath} was not valid JSON — backed up to ${configPath}.bak\n`);
|
|
985
|
+
} catch { /* backup failed — still safe to overwrite with valid config */ }
|
|
948
986
|
}
|
|
949
987
|
|
|
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
|
-
|
|
961
988
|
if (editor === 'continue') {
|
|
962
|
-
// experimental.modelContextProtocolServers
|
|
963
|
-
|
|
964
|
-
|
|
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;
|
|
989
|
+
// Merge into experimental.modelContextProtocolServers array — replace cachly entry if present
|
|
990
|
+
const exp = (existing['experimental'] ?? {}) as Record<string, unknown>;
|
|
991
|
+
const servers = (exp['modelContextProtocolServers'] ?? []) as Array<Record<string, unknown>>;
|
|
972
992
|
const filtered = servers.filter((s) => {
|
|
973
|
-
|
|
974
|
-
return
|
|
993
|
+
const env = (s['env'] ?? {}) as Record<string, string>;
|
|
994
|
+
return !env['CACHLY_JWT'] && !env['CACHLY_BRAIN_INSTANCE_ID'];
|
|
975
995
|
});
|
|
976
|
-
filtered.push(
|
|
977
|
-
existing
|
|
978
|
-
|
|
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 };
|
|
996
|
+
filtered.push({ transport: { type: 'stdio', command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'] }, env: cachlyEntry.env });
|
|
997
|
+
existing['experimental'] = { ...exp, modelContextProtocolServers: filtered };
|
|
998
|
+
return JSON.stringify(existing, null, 2);
|
|
988
999
|
}
|
|
989
1000
|
|
|
990
|
-
|
|
991
|
-
|
|
1001
|
+
if (editor === 'zed') {
|
|
1002
|
+
// Merge only context_servers.cachly — preserve all other Zed settings
|
|
1003
|
+
const cs = (existing['context_servers'] ?? {}) as Record<string, unknown>;
|
|
1004
|
+
cs['cachly'] = { command: { path: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env: cachlyEntry.env }, settings: {} };
|
|
1005
|
+
existing['context_servers'] = cs;
|
|
1006
|
+
return JSON.stringify(existing, null, 2);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Standard mcpServers format (Claude Code, Cursor, Windsurf, Copilot, Cline)
|
|
1010
|
+
const servers = (existing['mcpServers'] ?? {}) as Record<string, unknown>;
|
|
1011
|
+
servers['cachly'] = cachlyEntry;
|
|
1012
|
+
existing['mcpServers'] = servers;
|
|
1013
|
+
return JSON.stringify(existing, null, 2);
|
|
992
1014
|
}
|
|
993
1015
|
|
|
994
1016
|
function buildClaudeMdBlock(instanceId: string): string {
|
|
@@ -997,15 +1019,23 @@ function buildClaudeMdBlock(instanceId: string): string {
|
|
|
997
1019
|
|
|
998
1020
|
The Brain MCP server runs automatically. Instance: \`${instanceId}\`
|
|
999
1021
|
|
|
1000
|
-
Sessions start and end automatically — never call \`session_start\` or \`session_end\`.
|
|
1001
|
-
|
|
1002
1022
|
---
|
|
1003
1023
|
|
|
1024
|
+
### AT THE START of every new conversation (mandatory — first thing, before anything else):
|
|
1025
|
+
\`\`\`
|
|
1026
|
+
session_start(instance_id="${instanceId}", workspace_path="<absolute path to this project>", focus="<first topic the user mentioned>")
|
|
1027
|
+
\`\`\`
|
|
1028
|
+
Show the full briefing output to the user. If the response says the brain is empty or has 0 lessons, immediately seed it:
|
|
1029
|
+
\`\`\`
|
|
1030
|
+
brain_from_git(instance_id="${instanceId}", repo_path="<workspace_path>", limit=100)
|
|
1031
|
+
\`\`\`
|
|
1032
|
+
Then tell the user: "Your brain just learned from your git history. You're now pre-briefed."
|
|
1033
|
+
|
|
1004
1034
|
### BEFORE every task (mandatory — no exceptions):
|
|
1005
1035
|
\`\`\`
|
|
1006
1036
|
smart_recall(instance_id="${instanceId}", query="<what you are about to do>")
|
|
1007
1037
|
\`\`\`
|
|
1008
|
-
|
|
1038
|
+
If relevant lessons are found, say: "Your brain knows this pattern — [lesson]." Then proceed.
|
|
1009
1039
|
|
|
1010
1040
|
### BEFORE editing any file (mandatory):
|
|
1011
1041
|
\`\`\`
|
|
@@ -1020,6 +1050,11 @@ learn_from_attempts(instance_id="${instanceId}", topic="<category:keyword>", out
|
|
|
1020
1050
|
file_paths=["..."], commands=["..."], tags=["..."])
|
|
1021
1051
|
\`\`\`
|
|
1022
1052
|
|
|
1053
|
+
### AT THE END of every conversation (mandatory):
|
|
1054
|
+
\`\`\`
|
|
1055
|
+
session_end(instance_id="${instanceId}", summary="<one sentence: what was done>", files_changed=["..."])
|
|
1056
|
+
\`\`\`
|
|
1057
|
+
|
|
1023
1058
|
### WHEN debugging any error (mandatory before grepping/reading files):
|
|
1024
1059
|
\`\`\`
|
|
1025
1060
|
causal_trace(instance_id="${instanceId}", problem="<exact error or symptom>")
|
|
@@ -1063,6 +1098,682 @@ async function writeClaudeMd(projectDir: string, instanceId: string): Promise<'w
|
|
|
1063
1098
|
return 'written';
|
|
1064
1099
|
}
|
|
1065
1100
|
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
// ── CLI: cachly digest ────────────────────────────────────────────────────────
|
|
1104
|
+
// Usage: npx @cachly-dev/mcp-server@latest digest
|
|
1105
|
+
// Weekly brain summary — what your AI learned this week. Shareable output.
|
|
1106
|
+
|
|
1107
|
+
if (process.argv[2] === 'digest') {
|
|
1108
|
+
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1109
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1110
|
+
|
|
1111
|
+
if (!apiKey || !instanceId) {
|
|
1112
|
+
console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1113
|
+
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
process.stdout.write('\n🧠 Fetching your weekly Brain digest...\n\n');
|
|
1118
|
+
|
|
1119
|
+
try {
|
|
1120
|
+
// Fetch current stats
|
|
1121
|
+
const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
1122
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1123
|
+
signal: AbortSignal.timeout(8000),
|
|
1124
|
+
});
|
|
1125
|
+
if (!statsRes.ok) throw new Error(`stats HTTP ${statsRes.status}`);
|
|
1126
|
+
const stats = await statsRes.json() as {
|
|
1127
|
+
lesson_count?: number; context_count?: number; quality_score?: number;
|
|
1128
|
+
total_recall_count?: number; top_lessons?: Array<{ topic: string; recall_count: number; what_worked: string }>;
|
|
1129
|
+
team_authors?: string[];
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
const lessons = stats.lesson_count ?? 0;
|
|
1133
|
+
const recalls = stats.total_recall_count ?? 0;
|
|
1134
|
+
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
1135
|
+
const topLessons = (stats.top_lessons ?? []).slice(0, 5);
|
|
1136
|
+
const teamAuthors = stats.team_authors ?? [];
|
|
1137
|
+
const tokensSaved = recalls * 1200;
|
|
1138
|
+
const costSaved = (tokensSaved * 0.000003).toFixed(2);
|
|
1139
|
+
|
|
1140
|
+
const level = lessons < 10 ? 'Junior Dev 🔧' : lessons < 30 ? 'Mid Dev ⚡' :
|
|
1141
|
+
lessons < 60 ? 'Senior Dev 🧠' : lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
1142
|
+
|
|
1143
|
+
const now = new Date();
|
|
1144
|
+
const weekStart = new Date(now); weekStart.setDate(now.getDate() - 7);
|
|
1145
|
+
const fmt = (d: Date) => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
1146
|
+
|
|
1147
|
+
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
1148
|
+
console.log(`│ \x1b[1m🧠 Brain Weekly Digest\x1b[0m \x1b[90m${fmt(weekStart)} – ${fmt(now)}\x1b[0m │`);
|
|
1149
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1150
|
+
console.log(`│ Total lessons : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level : \x1b[32m${level.padEnd(18)}\x1b[0m│`);
|
|
1151
|
+
console.log(`│ Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m │`);
|
|
1152
|
+
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│`);
|
|
1153
|
+
|
|
1154
|
+
if (teamAuthors.length > 0) {
|
|
1155
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1156
|
+
console.log(`│ \x1b[36m👥 Team contributors:\x1b[0m ${teamAuthors.slice(0, 5).join(', ').slice(0, 42).padEnd(42)} │`);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (topLessons.length > 0) {
|
|
1160
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1161
|
+
console.log('│ \x1b[33m🔥 Most recalled lessons this week:\x1b[0m │');
|
|
1162
|
+
for (const l of topLessons) {
|
|
1163
|
+
const line = `${l.topic}: ${l.what_worked}`.slice(0, 57);
|
|
1164
|
+
console.log(`│ \x1b[90m• ${line.padEnd(58)}\x1b[0m│`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1169
|
+
console.log('');
|
|
1170
|
+
|
|
1171
|
+
// ── Shareable tweet card ────────────────────────────────────────────────
|
|
1172
|
+
const topLesson = topLessons[0];
|
|
1173
|
+
const tweetLines = [
|
|
1174
|
+
`🧠 My AI Brain weekly digest (${fmt(weekStart)} – ${fmt(now)}):`,
|
|
1175
|
+
``,
|
|
1176
|
+
` 📚 ${lessons} lessons learned`,
|
|
1177
|
+
` 🔁 ${recalls} recalls · ~${Math.round(tokensSaved / 1000)}K tokens saved`,
|
|
1178
|
+
` 🎯 Brain Level: ${level}`,
|
|
1179
|
+
topLesson ? ` 🔥 Top lesson: "${topLesson.topic}: ${topLesson.what_worked.slice(0, 60)}${topLesson.what_worked.length > 60 ? '…' : ''}"` : '',
|
|
1180
|
+
``,
|
|
1181
|
+
`Built with @cachly_dev — AI that actually remembers 🚀`,
|
|
1182
|
+
`cachly.dev`,
|
|
1183
|
+
].filter(Boolean).join('\n');
|
|
1184
|
+
|
|
1185
|
+
const tweetUrl = 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(tweetLines);
|
|
1186
|
+
|
|
1187
|
+
console.log('\x1b[1m📣 Share your digest:\x1b[0m');
|
|
1188
|
+
console.log('');
|
|
1189
|
+
console.log('\x1b[90m' + '─'.repeat(63) + '\x1b[0m');
|
|
1190
|
+
for (const line of tweetLines.split('\n')) {
|
|
1191
|
+
console.log(` ${line}`);
|
|
1192
|
+
}
|
|
1193
|
+
console.log('\x1b[90m' + '─'.repeat(63) + '\x1b[0m');
|
|
1194
|
+
console.log('');
|
|
1195
|
+
console.log(' \x1b[36m🐦 Tweet this:\x1b[0m');
|
|
1196
|
+
console.log(` \x1b[4m${tweetUrl.slice(0, 90)}...\x1b[0m`);
|
|
1197
|
+
console.log('');
|
|
1198
|
+
console.log(' \x1b[2m💡 Invite your team: npx @cachly-dev/mcp-server@latest invite\x1b[0m');
|
|
1199
|
+
console.log(' \x1b[2m Cron: 0 9 * * 1 npx @cachly-dev/mcp-server@latest digest\x1b[0m');
|
|
1200
|
+
console.log('');
|
|
1201
|
+
} catch (e) {
|
|
1202
|
+
console.log(`\n❌ Could not fetch digest: ${(e as Error).message}\n`);
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
process.exit(0);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// ── CLI: cachly invite ────────────────────────────────────────────────────────
|
|
1209
|
+
// Usage: npx @cachly-dev/mcp-server@latest invite [email]
|
|
1210
|
+
// Invites a teammate to share your Brain. Fastest referral loop.
|
|
1211
|
+
|
|
1212
|
+
if (process.argv[2] === 'invite') {
|
|
1213
|
+
// ── invite: fetch the user's unique referral link and show shareable messages ──
|
|
1214
|
+
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1215
|
+
|
|
1216
|
+
if (!apiKey) {
|
|
1217
|
+
console.log('\n⚠️ CACHLY_JWT must be set.');
|
|
1218
|
+
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
try {
|
|
1223
|
+
const res = await fetch(`${API_URL}/api/v1/referral/me`, {
|
|
1224
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1225
|
+
signal: AbortSignal.timeout(8000),
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1229
|
+
const data = await res.json() as { code?: string; url?: string; referral_count?: number };
|
|
1230
|
+
const referralUrl = data.url ?? `https://cachly.dev/r/${data.code}`;
|
|
1231
|
+
const count = data.referral_count ?? 0;
|
|
1232
|
+
|
|
1233
|
+
const slackMsg = `Hey, I've been using cachly to give my AI persistent memory across sessions — no more re-explaining my stack every morning.\n\nYou get a free Brain: ${referralUrl}`;
|
|
1234
|
+
const tweetText = encodeURIComponent(`I gave my AI persistent memory. It remembers fixes, patterns, and context across every session — no more re-explaining.\n\nTry it free: ${referralUrl}\n\n@cachlydev`);
|
|
1235
|
+
|
|
1236
|
+
console.log('');
|
|
1237
|
+
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
1238
|
+
console.log('│ 🧠 Your cachly Brain invite link │');
|
|
1239
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1240
|
+
console.log(`│ ${referralUrl.padEnd(61)}│`);
|
|
1241
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1242
|
+
if (count > 0) {
|
|
1243
|
+
console.log(`│ \x1b[32m✓ ${count} developer${count === 1 ? '' : 's'} joined via your link so far\x1b[0m`.padEnd(72) + '│');
|
|
1244
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1245
|
+
}
|
|
1246
|
+
console.log('│ \x1b[33mSlack / DM message:\x1b[0m │');
|
|
1247
|
+
console.log('│ │');
|
|
1248
|
+
for (const line of slackMsg.split('\n')) {
|
|
1249
|
+
console.log(`│ \x1b[90m${line.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1250
|
+
}
|
|
1251
|
+
console.log('│ │');
|
|
1252
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1253
|
+
console.log(`│ \x1b[36m𝕏 Tweet:\x1b[0m https://twitter.com/intent/tweet?text=${tweetText.slice(0, 10)}... │`);
|
|
1254
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1255
|
+
console.log('');
|
|
1256
|
+
console.log('\x1b[2m Each developer who signs up via your link earns you 1 month Pro.\x1b[0m');
|
|
1257
|
+
console.log('');
|
|
1258
|
+
} catch (e) {
|
|
1259
|
+
console.log(`\n❌ Could not fetch invite link: ${(e as Error).message}\n`);
|
|
1260
|
+
process.exit(1);
|
|
1261
|
+
}
|
|
1262
|
+
process.exit(0);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// ── CLI: cachly join <token> ──────────────────────────────────────────────────
|
|
1266
|
+
// Usage: npx @cachly-dev/mcp-server@latest join <token>
|
|
1267
|
+
// Accepts a Brain invite link — writes the shared instance ID into all
|
|
1268
|
+
// detected editor MCP configs and the global ~/.claude/mcp.json.
|
|
1269
|
+
// No account needed if CACHLY_JWT is already set; otherwise prompts for auth.
|
|
1270
|
+
|
|
1271
|
+
if (process.argv[2] === 'join') {
|
|
1272
|
+
const { writeFile: wfJ, readFile: rfJ, mkdir: mkJ } = await import('node:fs/promises');
|
|
1273
|
+
const { existsSync: exJ } = await import('node:fs');
|
|
1274
|
+
const { resolve: resJ, dirname: dirJ } = await import('node:path');
|
|
1275
|
+
const { homedir } = await import('node:os');
|
|
1276
|
+
const { createInterface: ciJ } = await import('node:readline');
|
|
1277
|
+
|
|
1278
|
+
const token = process.argv[3] ?? '';
|
|
1279
|
+
if (!token) {
|
|
1280
|
+
console.log('\n❌ Usage: npx @cachly-dev/mcp-server@latest join <token>\n');
|
|
1281
|
+
console.log(' Get a token from a teammate: npx @cachly-dev/mcp-server@latest invite\n');
|
|
1282
|
+
process.exit(1);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
console.log('\n⏳ Looking up invite...');
|
|
1286
|
+
|
|
1287
|
+
// ── Step 1: Fetch invite info (public, no auth) ───────────────────────────
|
|
1288
|
+
let inviteInfo: { instance_id: string; instance_name: string; tier: string; expires_at: string; label?: string };
|
|
1289
|
+
try {
|
|
1290
|
+
const res = await fetch(`${API_URL}/api/invite/${token}`, { signal: AbortSignal.timeout(8000) });
|
|
1291
|
+
if (res.status === 404) { console.log('\n❌ Invite not found — the link may be invalid.\n'); process.exit(1); }
|
|
1292
|
+
if (res.status === 410) { console.log('\n❌ This invite link has expired (7-day limit).\n'); process.exit(1); }
|
|
1293
|
+
if (!res.ok) { console.log(`\n❌ Could not fetch invite: HTTP ${res.status}\n`); process.exit(1); }
|
|
1294
|
+
inviteInfo = await res.json() as typeof inviteInfo;
|
|
1295
|
+
} catch (e) {
|
|
1296
|
+
console.log(`\n❌ Network error: ${(e as Error).message}\n`);
|
|
1297
|
+
process.exit(1);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const { instance_id, instance_name, tier, label } = inviteInfo;
|
|
1301
|
+
const expires = new Date(inviteInfo.expires_at).toLocaleDateString();
|
|
1302
|
+
|
|
1303
|
+
console.log('');
|
|
1304
|
+
console.log(' \x1b[35m╔══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1305
|
+
console.log(' \x1b[35m║\x1b[0m \x1b[1m🧠 Brain Invite\x1b[0m \x1b[35m║\x1b[0m');
|
|
1306
|
+
console.log(' \x1b[35m╚══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1307
|
+
console.log('');
|
|
1308
|
+
console.log(` Brain : \x1b[1m${instance_name}\x1b[0m`);
|
|
1309
|
+
console.log(` Tier : ${tier}`);
|
|
1310
|
+
if (label) console.log(` Note : ${label}`);
|
|
1311
|
+
console.log(` Expires : ${expires}`);
|
|
1312
|
+
console.log('');
|
|
1313
|
+
|
|
1314
|
+
// ── Step 2: Confirm ───────────────────────────────────────────────────────
|
|
1315
|
+
const rlJ = ciJ({ input: process.stdin, output: process.stdout });
|
|
1316
|
+
const confirmJ = await new Promise<string>((resolve) => {
|
|
1317
|
+
rlJ.question(' Join this Brain? [Y/n] ', (a) => { rlJ.close(); resolve(a.trim().toLowerCase() || 'y'); });
|
|
1318
|
+
});
|
|
1319
|
+
if (confirmJ !== 'y' && confirmJ !== 'yes') {
|
|
1320
|
+
console.log('\n Cancelled.\n');
|
|
1321
|
+
process.exit(0);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ── Step 3: Ensure JWT ────────────────────────────────────────────────────
|
|
1325
|
+
let joinJwt = JWT;
|
|
1326
|
+
if (!joinJwt) {
|
|
1327
|
+
console.log('\n No API key found — starting sign-in (10 seconds)...\n');
|
|
1328
|
+
try {
|
|
1329
|
+
const dcRes = await fetch(`${API_URL}/api/v1/auth/device/code`, {
|
|
1330
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1331
|
+
body: JSON.stringify({ client_id: 'cachly-mcp-cli' }),
|
|
1332
|
+
signal: AbortSignal.timeout(10000),
|
|
1333
|
+
});
|
|
1334
|
+
if (dcRes.ok) {
|
|
1335
|
+
const dc = await dcRes.json() as { verification_uri: string; user_code: string; device_code: string; interval: number };
|
|
1336
|
+
console.log(` 👉 Open: \x1b[36m${dc.verification_uri}\x1b[0m`);
|
|
1337
|
+
console.log(` Code: \x1b[1m${dc.user_code}\x1b[0m\n`);
|
|
1338
|
+
// Poll for token
|
|
1339
|
+
const pollInterval = (dc.interval ?? 5) * 1000;
|
|
1340
|
+
for (let i = 0; i < 24; i++) {
|
|
1341
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
1342
|
+
try {
|
|
1343
|
+
const tokRes = await fetch(`${API_URL}/api/v1/auth/device/token`, {
|
|
1344
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1345
|
+
body: JSON.stringify({ device_code: dc.device_code, client_id: 'cachly-mcp-cli' }),
|
|
1346
|
+
signal: AbortSignal.timeout(8000),
|
|
1347
|
+
});
|
|
1348
|
+
if (tokRes.ok) {
|
|
1349
|
+
const tok = await tokRes.json() as { access_token?: string };
|
|
1350
|
+
if (tok.access_token) { joinJwt = tok.access_token; break; }
|
|
1351
|
+
}
|
|
1352
|
+
} catch { /* polling — keep going */ }
|
|
1353
|
+
}
|
|
1354
|
+
if (!joinJwt) { console.log('\n❌ Sign-in timed out. Run the command again.\n'); process.exit(1); }
|
|
1355
|
+
console.log(' ✅ Signed in!\n');
|
|
1356
|
+
}
|
|
1357
|
+
} catch (e) {
|
|
1358
|
+
console.log(`\n ⚠️ Could not start sign-in: ${(e as Error).message}`);
|
|
1359
|
+
console.log(` Set CACHLY_JWT manually from https://cachly.dev/setup-ai\n`);
|
|
1360
|
+
process.exit(1);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// ── Step 4: Write instance ID into all editor MCP configs ────────────────
|
|
1365
|
+
const cwd = process.cwd();
|
|
1366
|
+
const home = homedir();
|
|
1367
|
+
|
|
1368
|
+
const editorFiles: Array<{ label: string; path: string; global?: boolean }> = [
|
|
1369
|
+
{ label: '.mcp.json (Claude Code project)', path: resJ(cwd, '.mcp.json') },
|
|
1370
|
+
{ label: '.cursor/mcp.json', path: resJ(cwd, '.cursor', 'mcp.json') },
|
|
1371
|
+
{ label: '.windsurf/mcp.json', path: resJ(cwd, '.windsurf', 'mcp.json') },
|
|
1372
|
+
{ label: '.vscode/mcp.json (Copilot/Cline)', path: resJ(cwd, '.vscode', 'mcp.json') },
|
|
1373
|
+
{ label: '~/.claude/mcp.json (global)', path: resJ(home, '.claude', 'mcp.json'), global: true },
|
|
1374
|
+
];
|
|
1375
|
+
|
|
1376
|
+
let written = 0;
|
|
1377
|
+
for (const { label: eLabel, path: ePath, global: isGlobal } of editorFiles) {
|
|
1378
|
+
// Only write project configs if the directory exists (or global)
|
|
1379
|
+
const dir = dirJ(ePath);
|
|
1380
|
+
if (!isGlobal && !exJ(dir) && !exJ(ePath)) continue;
|
|
1381
|
+
try {
|
|
1382
|
+
await mkJ(dir, { recursive: true });
|
|
1383
|
+
let cfg: Record<string, unknown> = {};
|
|
1384
|
+
if (exJ(ePath)) {
|
|
1385
|
+
try {
|
|
1386
|
+
cfg = JSON.parse(await rfJ(ePath, 'utf-8')) as Record<string, unknown>;
|
|
1387
|
+
} catch {
|
|
1388
|
+
try { await (await import('node:fs/promises')).rename(ePath, ePath + '.bak'); } catch { /* backup failed */ }
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// Update or create the cachly entry with the shared instance ID
|
|
1392
|
+
const servers = (cfg['mcpServers'] ?? {}) as Record<string, unknown>;
|
|
1393
|
+
const existing = (servers['cachly'] ?? {}) as Record<string, unknown>;
|
|
1394
|
+
const env = (existing['env'] ?? {}) as Record<string, string>;
|
|
1395
|
+
env['CACHLY_BRAIN_INSTANCE_ID'] = instance_id;
|
|
1396
|
+
if (joinJwt && !env['CACHLY_JWT']) env['CACHLY_JWT'] = joinJwt;
|
|
1397
|
+
env['CACHLY_API_URL'] ??= 'https://api.cachly.dev';
|
|
1398
|
+
servers['cachly'] = { ...existing, command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env };
|
|
1399
|
+
cfg['mcpServers'] = servers;
|
|
1400
|
+
await wfJ(ePath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
1401
|
+
console.log(` ✅ ${eLabel}`);
|
|
1402
|
+
written++;
|
|
1403
|
+
} catch (joinWriteErr) {
|
|
1404
|
+
// Only surface unexpected errors — missing editor dirs are expected and silently skipped above
|
|
1405
|
+
if ((joinWriteErr as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
1406
|
+
console.log(` ⚠️ ${eLabel}: ${(joinWriteErr as Error).message}`);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
console.log('');
|
|
1412
|
+
if (written === 0) {
|
|
1413
|
+
console.log(' ⚠️ No editor configs found — run `cachly setup` first.\n');
|
|
1414
|
+
} else {
|
|
1415
|
+
console.log(` 🧠 You're now sharing Brain: \x1b[1m${instance_name}\x1b[0m`);
|
|
1416
|
+
console.log(' Restart your editor — your AI arrives pre-briefed from the team Brain.\n');
|
|
1417
|
+
console.log(' 📛 Add the badge to your README:');
|
|
1418
|
+
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
1419
|
+
}
|
|
1420
|
+
process.exit(0);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// ── CLI: cachly upgrade ───────────────────────────────────────────────────────
|
|
1424
|
+
// Usage: npx @cachly-dev/mcp-server@latest upgrade
|
|
1425
|
+
// Checks npm for the latest version and prints upgrade instructions if outdated.
|
|
1426
|
+
|
|
1427
|
+
if (process.argv[2] === 'upgrade') {
|
|
1428
|
+
process.stdout.write('\n⏳ Checking for updates...\n');
|
|
1429
|
+
try {
|
|
1430
|
+
const res = await fetch('https://registry.npmjs.org/@cachly-dev/mcp-server/latest', {
|
|
1431
|
+
signal: AbortSignal.timeout(8000),
|
|
1432
|
+
});
|
|
1433
|
+
if (!res.ok) throw new Error(`registry HTTP ${res.status}`);
|
|
1434
|
+
const pkg = await res.json() as { version?: string };
|
|
1435
|
+
const latest = pkg.version;
|
|
1436
|
+
if (!latest || !/^\d+\.\d+\.\d+/.test(latest)) throw new Error('npm registry returned unexpected response');
|
|
1437
|
+
|
|
1438
|
+
if (latest === CURRENT_VERSION) {
|
|
1439
|
+
console.log(`\n✅ Already on the latest version: \x1b[32mv${CURRENT_VERSION}\x1b[0m\n`);
|
|
1440
|
+
} else {
|
|
1441
|
+
console.log('');
|
|
1442
|
+
console.log('\x1b[35m ╔══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1443
|
+
console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly update available\x1b[0m \x1b[35m║\x1b[0m');
|
|
1444
|
+
console.log('\x1b[35m ╚══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1445
|
+
console.log('');
|
|
1446
|
+
console.log(` Current : \x1b[31mv${CURRENT_VERSION}\x1b[0m`);
|
|
1447
|
+
console.log(` Latest : \x1b[32mv${latest}\x1b[0m`);
|
|
1448
|
+
console.log('');
|
|
1449
|
+
console.log(' Update your editor configs to pick up the new version:');
|
|
1450
|
+
console.log('');
|
|
1451
|
+
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m');
|
|
1452
|
+
console.log('');
|
|
1453
|
+
console.log(' \x1b[2m(npx always fetches the latest when @latest is specified)\x1b[0m');
|
|
1454
|
+
console.log('');
|
|
1455
|
+
}
|
|
1456
|
+
} catch (e) {
|
|
1457
|
+
console.log(`\n❌ Could not check for updates: ${(e as Error).message}\n`);
|
|
1458
|
+
process.exit(1);
|
|
1459
|
+
}
|
|
1460
|
+
process.exit(0);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// ── CLI: cachly demo ──────────────────────────────────────────────────────────
|
|
1464
|
+
// Usage: npx @cachly-dev/mcp-server@latest demo
|
|
1465
|
+
// Zero-signup. Reads local git history and shows what the AI Brain would know.
|
|
1466
|
+
// The fastest path from "what is this?" to "I need this".
|
|
1467
|
+
|
|
1468
|
+
if (process.argv[2] === 'demo') {
|
|
1469
|
+
const { execSync } = await import('node:child_process');
|
|
1470
|
+
const { existsSync, readFileSync } = await import('node:fs');
|
|
1471
|
+
const { resolve, basename } = await import('node:path');
|
|
1472
|
+
const cwd = process.cwd();
|
|
1473
|
+
|
|
1474
|
+
console.log('\n\x1b[35m🧠 cachly Brain — Live Demo\x1b[0m');
|
|
1475
|
+
console.log('\x1b[90mNo account needed. Reading your git history...\x1b[0m\n');
|
|
1476
|
+
|
|
1477
|
+
// Verify git repo
|
|
1478
|
+
if (!existsSync(resolve(cwd, '.git'))) {
|
|
1479
|
+
console.log('⚠️ No git repository found here.');
|
|
1480
|
+
console.log(' Run this command inside a project directory with git history.\n');
|
|
1481
|
+
console.log(' Example: cd ~/my-project && npx @cachly-dev/mcp-server@latest demo\n');
|
|
1482
|
+
process.exit(0);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Get developer name from git config
|
|
1486
|
+
let devName = '';
|
|
1487
|
+
try {
|
|
1488
|
+
devName = execSync('git config user.name', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim().split(' ')[0] ?? '';
|
|
1489
|
+
} catch { /* ignore */ }
|
|
1490
|
+
|
|
1491
|
+
// Get project name from package.json or directory name
|
|
1492
|
+
let projectName = basename(cwd);
|
|
1493
|
+
try {
|
|
1494
|
+
const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf-8')) as { name?: string };
|
|
1495
|
+
if (pkg.name) projectName = pkg.name.replace(/^@[^/]+\//, '');
|
|
1496
|
+
} catch { /* ignore */ }
|
|
1497
|
+
|
|
1498
|
+
// Read git log
|
|
1499
|
+
let logOutput = '';
|
|
1500
|
+
try {
|
|
1501
|
+
logOutput = execSync(
|
|
1502
|
+
'git log HEAD --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n 200',
|
|
1503
|
+
{ cwd, encoding: 'utf-8', stdio: 'pipe' }
|
|
1504
|
+
);
|
|
1505
|
+
} catch {
|
|
1506
|
+
console.log('⚠️ Could not read git log. Make sure you have commits.\n');
|
|
1507
|
+
process.exit(0);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
1511
|
+
const [sha, subject, date, author] = line.split('|||');
|
|
1512
|
+
return { sha: (sha ?? '').trim().slice(0, 8), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
if (commits.length === 0) {
|
|
1516
|
+
console.log('⚠️ No commits found.\n');
|
|
1517
|
+
process.exit(0);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Classify commits (same logic as brain_from_git)
|
|
1521
|
+
const classify = (s: string) => {
|
|
1522
|
+
const t = s.toLowerCase();
|
|
1523
|
+
if (/\b(fix|fixed|bug|hotfix|patch|revert|resolve)\b/.test(t)) return 'fix';
|
|
1524
|
+
if (/\b(feat|feature|add|implement|new|introduce)\b/.test(t)) return 'feat';
|
|
1525
|
+
if (/\b(perf|optim|speed|cache|latency)\b/.test(t)) return 'perf';
|
|
1526
|
+
if (/\b(security|cve|auth|csrf|xss|inject|sanitize)\b/.test(t)) return 'security';
|
|
1527
|
+
if (/\b(deploy|ci|cd|docker|k8s|infra)\b/.test(t)) return 'deploy';
|
|
1528
|
+
if (/\b(refactor|clean|simplify|extract)\b/.test(t)) return 'refactor';
|
|
1529
|
+
return 'chore';
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
// Count categories
|
|
1533
|
+
const cats = new Map<string, number>();
|
|
1534
|
+
const fixes: string[] = [];
|
|
1535
|
+
const feats: string[] = [];
|
|
1536
|
+
const security: string[] = [];
|
|
1537
|
+
const authors = new Set<string>();
|
|
1538
|
+
|
|
1539
|
+
for (const c of commits) {
|
|
1540
|
+
const cat = classify(c.subject);
|
|
1541
|
+
cats.set(cat, (cats.get(cat) ?? 0) + 1);
|
|
1542
|
+
if (cat === 'fix' && fixes.length < 5) fixes.push(c.subject.slice(0, 72));
|
|
1543
|
+
if (cat === 'feat' && feats.length < 5) feats.push(c.subject.slice(0, 72));
|
|
1544
|
+
if (cat === 'security' && security.length < 3) security.push(c.subject.slice(0, 72));
|
|
1545
|
+
if (c.author) authors.add(c.author.split(' ')[0]!);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const totalLessons = commits.length - (cats.get('chore') ?? 0);
|
|
1549
|
+
const dateRange = `${commits[commits.length - 1]?.date ?? '?'} → ${commits[0]?.date ?? '?'}`;
|
|
1550
|
+
|
|
1551
|
+
// Estimate time wasted re-explaining (45 min/day * workdays since first commit)
|
|
1552
|
+
const firstDate = commits[commits.length - 1]?.date;
|
|
1553
|
+
let daysActive = 0;
|
|
1554
|
+
if (firstDate) {
|
|
1555
|
+
const ms = Date.now() - new Date(firstDate).getTime();
|
|
1556
|
+
daysActive = Math.max(1, Math.round(ms / (1000 * 60 * 60 * 24) * 5 / 7)); // workdays
|
|
1557
|
+
}
|
|
1558
|
+
const hoursWasted = Math.round(daysActive * 0.75); // 45 min/day
|
|
1559
|
+
|
|
1560
|
+
// Brain Level based on total lessons (matches Go ComputeBrainLevel thresholds)
|
|
1561
|
+
const brainLevelName = totalLessons >= 501 ? 'Oracle' : totalLessons >= 201 ? 'Architect' : totalLessons >= 51 ? 'Expert' : totalLessons >= 11 ? 'Explorer' : 'Apprentice';
|
|
1562
|
+
|
|
1563
|
+
// Personalized header
|
|
1564
|
+
const greeting = devName ? `Hey ${devName} — ` : '';
|
|
1565
|
+
const headerTitle = `${greeting}here's what your AI would know about ${projectName}`;
|
|
1566
|
+
|
|
1567
|
+
// Display the demo
|
|
1568
|
+
process.stdout.write('\x1b[2K\r');
|
|
1569
|
+
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
1570
|
+
console.log(`│ \x1b[1m🧠 ${headerTitle.slice(0, 58).padEnd(58)}\x1b[0m│`);
|
|
1571
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1572
|
+
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 │`);
|
|
1573
|
+
console.log(`│ Lessons extracted: \x1b[32m${String(totalLessons).padEnd(6)}\x1b[0m Contributors: \x1b[36m${String(authors.size).padEnd(20)}\x1b[0m│`);
|
|
1574
|
+
console.log(`│ Brain Level : \x1b[35m${brainLevelName.padEnd(14)}\x1b[0m Time wasted re-explaining: \x1b[31m${String(hoursWasted + 'h').padEnd(5)}\x1b[0m│`);
|
|
1575
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1576
|
+
|
|
1577
|
+
// Category breakdown
|
|
1578
|
+
const sorted = [...cats.entries()].filter(([k]) => k !== 'chore').sort((a, b) => b[1] - a[1]);
|
|
1579
|
+
const bar = (n: number, max: number) => '█'.repeat(Math.round((n / max) * 20)).padEnd(20);
|
|
1580
|
+
const maxVal = Math.max(...sorted.map(([, v]) => v), 1);
|
|
1581
|
+
console.log('│ \x1b[90mCategory breakdown:\x1b[0m │');
|
|
1582
|
+
for (const [cat, count] of sorted) {
|
|
1583
|
+
const emoji = { fix: '🔧', feat: '✨', perf: '⚡', security: '🔒', deploy: '🚀', refactor: '🔄' }[cat] ?? '•';
|
|
1584
|
+
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 │`);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (security.length > 0) {
|
|
1588
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1589
|
+
console.log('│ \x1b[31m🔒 Security fixes your AI would know:\x1b[0m │');
|
|
1590
|
+
for (const s of security) console.log(`│ \x1b[90m• ${s.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
if (fixes.length > 0) {
|
|
1594
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1595
|
+
console.log('│ \x1b[33m🔧 Bug fixes your AI would never repeat:\x1b[0m │');
|
|
1596
|
+
for (const f of fixes.slice(0, 4)) console.log(`│ \x1b[90m• ${f.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (feats.length > 0) {
|
|
1600
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1601
|
+
console.log('│ \x1b[32m✨ Features & decisions it would recall:\x1b[0m │');
|
|
1602
|
+
for (const f of feats.slice(0, 3)) console.log(`│ \x1b[90m• ${f.slice(0, 58).padEnd(58)}\x1b[0m │`);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1606
|
+
console.log('│ \x1b[32m💡 With cachly, your AI arrives pre-briefed every session.\x1b[0m │');
|
|
1607
|
+
console.log('│ \x1b[32m No more re-explaining. No more repeated mistakes.\x1b[0m │');
|
|
1608
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1609
|
+
console.log('');
|
|
1610
|
+
// Generate shareable preview URL with encoded stats
|
|
1611
|
+
const previewParams = new URLSearchParams({
|
|
1612
|
+
repo: projectName,
|
|
1613
|
+
commits: String(commits.length),
|
|
1614
|
+
lessons: String(totalLessons),
|
|
1615
|
+
level: brainLevelName,
|
|
1616
|
+
authors: String(authors.size),
|
|
1617
|
+
hours: String(hoursWasted),
|
|
1618
|
+
});
|
|
1619
|
+
const previewURL = `https://cachly.dev/preview?${previewParams.toString()}`;
|
|
1620
|
+
|
|
1621
|
+
console.log(' \x1b[1mMake this permanent (free, 1–5 minutes):\x1b[0m');
|
|
1622
|
+
console.log(' \x1b[32m$ npx @cachly-dev/mcp-server@latest setup\x1b[0m');
|
|
1623
|
+
console.log('');
|
|
1624
|
+
console.log(` \x1b[90m🔗 Shareable preview:\x1b[0m \x1b[36m${previewURL}\x1b[0m`);
|
|
1625
|
+
console.log('');
|
|
1626
|
+
console.log(' Works with: Claude Code · Cursor · Windsurf · Copilot · Cline · Zed');
|
|
1627
|
+
console.log(' Free forever · GDPR · German servers · No credit card');
|
|
1628
|
+
console.log('');
|
|
1629
|
+
process.exit(0);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// ── CLI: cachly share ─────────────────────────────────────────────────────────
|
|
1633
|
+
// Usage: npx @cachly-dev/mcp-server@latest share
|
|
1634
|
+
// Generates a shareable ASCII card + tweet text with your Brain stats.
|
|
1635
|
+
|
|
1636
|
+
if (process.argv[2] === 'share') {
|
|
1637
|
+
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1638
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1639
|
+
|
|
1640
|
+
if (!apiKey || !instanceId) {
|
|
1641
|
+
console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1642
|
+
console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 1–5 minutes)\n');
|
|
1643
|
+
process.exit(1);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
process.stdout.write('\n🧠 Fetching your Brain stats...\n');
|
|
1647
|
+
|
|
1648
|
+
try {
|
|
1649
|
+
const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
1650
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1651
|
+
signal: AbortSignal.timeout(8000),
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
if (!statsRes.ok) throw new Error(`HTTP ${statsRes.status}`);
|
|
1655
|
+
const stats = await statsRes.json() as {
|
|
1656
|
+
lesson_count?: number; context_count?: number;
|
|
1657
|
+
quality_score?: number; total_recall_count?: number;
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
const lessons = stats.lesson_count ?? 0;
|
|
1661
|
+
const recalls = stats.total_recall_count ?? 0;
|
|
1662
|
+
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
1663
|
+
const tokensSaved = recalls * 1200;
|
|
1664
|
+
const costSaved = (tokensSaved * 0.000003).toFixed(2);
|
|
1665
|
+
|
|
1666
|
+
const level = lessons === 0 ? 'Intern 🌱' :
|
|
1667
|
+
lessons < 10 ? 'Junior Dev 🔧' :
|
|
1668
|
+
lessons < 30 ? 'Mid Dev ⚡' :
|
|
1669
|
+
lessons < 60 ? 'Senior Dev 🧠' :
|
|
1670
|
+
lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
1671
|
+
|
|
1672
|
+
console.log('');
|
|
1673
|
+
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
1674
|
+
console.log('│ 🧠 My AI Brain · powered by \x1b[35mcachly.dev\x1b[0m │');
|
|
1675
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1676
|
+
console.log(`│ Lessons stored : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level: \x1b[32m${level.padEnd(18)}\x1b[0m│`);
|
|
1677
|
+
console.log(`│ Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m │`);
|
|
1678
|
+
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│`);
|
|
1679
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1680
|
+
console.log('│ Works with: Claude Code · Cursor · Windsurf · Copilot │');
|
|
1681
|
+
console.log('│ Free · GDPR · German servers │');
|
|
1682
|
+
console.log('│ \x1b[35mnpx @cachly-dev/mcp-server@latest setup\x1b[0m │');
|
|
1683
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1684
|
+
console.log('');
|
|
1685
|
+
|
|
1686
|
+
// Tweet text
|
|
1687
|
+
const shareUrl = 'https://cachly.dev?ref=share&utm_source=x&utm_medium=social&utm_campaign=cli-share';
|
|
1688
|
+
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`;
|
|
1689
|
+
|
|
1690
|
+
console.log(' \x1b[1m📋 Share on Twitter/X (copy this):\x1b[0m');
|
|
1691
|
+
console.log(' ─────────────────────────────────────');
|
|
1692
|
+
console.log(tweet.split('\n').map(l => ` ${l}`).join('\n'));
|
|
1693
|
+
console.log(' ─────────────────────────────────────');
|
|
1694
|
+
console.log('');
|
|
1695
|
+
console.log(' \x1b[2mPro tip: add a screenshot of your Brain panel for 3x more engagement\x1b[0m');
|
|
1696
|
+
console.log(' \x1b[2mAlso add a live badge to your README:\x1b[0m');
|
|
1697
|
+
console.log(` \x1b[90m npx @cachly-dev/mcp-server@latest badge\x1b[0m`);
|
|
1698
|
+
console.log('');
|
|
1699
|
+
} catch (e) {
|
|
1700
|
+
console.log(`\n❌ Could not fetch stats: ${(e as Error).message}\n`);
|
|
1701
|
+
process.exit(1);
|
|
1702
|
+
}
|
|
1703
|
+
process.exit(0);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// ── CLI: cachly badge ─────────────────────────────────────────────────────────
|
|
1707
|
+
// Outputs the Markdown + HTML snippet for embedding a live Brain lesson-count
|
|
1708
|
+
// badge in any README or website. Badge SVG served by cachly API (public, no auth).
|
|
1709
|
+
|
|
1710
|
+
if (process.argv[2] === 'badge') {
|
|
1711
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1712
|
+
|
|
1713
|
+
if (!instanceId) {
|
|
1714
|
+
console.log('\n⚠️ CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1715
|
+
console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 1–5 minutes)\n');
|
|
1716
|
+
process.exit(1);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const badgeUrl = `${API_URL}/api/v1/badge/${instanceId}`;
|
|
1720
|
+
const targetUrl = 'https://cachly.dev';
|
|
1721
|
+
const markdown = `[](${targetUrl})`;
|
|
1722
|
+
const htmlBadge = `<a href="${targetUrl}"><img src="${badgeUrl}" alt="cachly Brain" /></a>`;
|
|
1723
|
+
|
|
1724
|
+
console.log('');
|
|
1725
|
+
console.log('\x1b[35m ╔═══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1726
|
+
console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly Brain Badge\x1b[0m \x1b[35m║\x1b[0m');
|
|
1727
|
+
console.log('\x1b[35m ╚═══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1728
|
+
console.log('');
|
|
1729
|
+
console.log(' Paste into your README.md:');
|
|
1730
|
+
console.log('');
|
|
1731
|
+
console.log(` \x1b[32m${markdown}\x1b[0m`);
|
|
1732
|
+
console.log('');
|
|
1733
|
+
console.log(' ─────────────────────────────────────────────────────');
|
|
1734
|
+
console.log(' Or paste into HTML:');
|
|
1735
|
+
console.log('');
|
|
1736
|
+
console.log(` \x1b[36m${htmlBadge}\x1b[0m`);
|
|
1737
|
+
console.log('');
|
|
1738
|
+
console.log(' ─────────────────────────────────────────────────────');
|
|
1739
|
+
console.log(' Badge URL (live, updates every hour):');
|
|
1740
|
+
console.log(` \x1b[90m${badgeUrl}\x1b[0m`);
|
|
1741
|
+
console.log('');
|
|
1742
|
+
console.log(' \x1b[2mThe badge shows your current lesson count — no auth required,');
|
|
1743
|
+
console.log(' safe to embed in public repos. Updates hourly.\x1b[0m');
|
|
1744
|
+
console.log('');
|
|
1745
|
+
process.exit(0);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// ── CLI: no-args splash screen ────────────────────────────────────────────────
|
|
1749
|
+
// When run with no recognized subcommand, show a compelling pitch + help.
|
|
1750
|
+
|
|
1751
|
+
if (!process.argv[2] && process.stdout.isTTY) {
|
|
1752
|
+
console.log('');
|
|
1753
|
+
console.log('\x1b[35m ╔═══════════════════════════════════════════════════════╗\x1b[0m');
|
|
1754
|
+
console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly — Persistent AI Memory for Developers\x1b[0m \x1b[35m║\x1b[0m');
|
|
1755
|
+
console.log('\x1b[35m ╚═══════════════════════════════════════════════════════╝\x1b[0m');
|
|
1756
|
+
console.log('');
|
|
1757
|
+
console.log(' Stop re-teaching your AI every morning.');
|
|
1758
|
+
console.log(' cachly gives it a permanent brain — pre-briefed every session.');
|
|
1759
|
+
console.log('');
|
|
1760
|
+
console.log(' \x1b[1mCommands:\x1b[0m');
|
|
1761
|
+
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest demo\x1b[0m ← Start here (no account)');
|
|
1762
|
+
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m ← Wire up your AI editors');
|
|
1763
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest health\x1b[0m ← Check everything works');
|
|
1764
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest digest\x1b[0m ← Weekly Brain summary');
|
|
1765
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest share\x1b[0m ← Share your Brain stats');
|
|
1766
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest badge\x1b[0m ← README badge for your Brain');
|
|
1767
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest invite\x1b[0m ← Invite a teammate');
|
|
1768
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest join <token>\x1b[0m ← Accept a Brain invite');
|
|
1769
|
+
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
|
|
1770
|
+
console.log('');
|
|
1771
|
+
console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
|
|
1772
|
+
console.log(' \x1b[90mFree forever · GDPR · German servers · 89 MCP tools\x1b[0m');
|
|
1773
|
+
console.log('');
|
|
1774
|
+
process.exit(0);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1066
1777
|
// ── CLI: cachly init ──────────────────────────────────────────────────────────
|
|
1067
1778
|
// Usage: npx @cachly-dev/mcp-server init --instance-id <id> --api-key <key> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]
|
|
1068
1779
|
|
|
@@ -1087,11 +1798,11 @@ if (process.argv[2] === 'init') {
|
|
|
1087
1798
|
|
|
1088
1799
|
const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
|
|
1089
1800
|
const configPath = resolve(projectDir, configFile);
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
console.log(`\n${
|
|
1801
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1802
|
+
const { existsSync: exInit } = await import('node:fs');
|
|
1803
|
+
const merged = await mergeMcpConfig(configPath, apiKey, instanceId, editor, { readFile, existsSync: exInit });
|
|
1804
|
+
await writeFile(configPath, merged, 'utf-8');
|
|
1805
|
+
console.log(`\n✅ ${exInit(configPath) ? 'Updated' : 'Written'}: ${configFile}`);
|
|
1095
1806
|
|
|
1096
1807
|
// Always write CLAUDE.md (idempotent — safe to run multiple times)
|
|
1097
1808
|
const result = await writeClaudeMd(projectDir, instanceId);
|
|
@@ -1134,7 +1845,282 @@ if (process.argv[2] === 'init') {
|
|
|
1134
1845
|
} catch { /* non-critical — git hook is a best-effort feature */ }
|
|
1135
1846
|
|
|
1136
1847
|
console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}!`);
|
|
1137
|
-
console.log(` Restart your editor — the \`cachly\` MCP tools will appear
|
|
1848
|
+
console.log(` Restart your editor — the \`cachly\` MCP tools will appear.`);
|
|
1849
|
+
console.log(`\n 📛 Add a live badge to your README:`);
|
|
1850
|
+
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
1851
|
+
process.exit(0);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// ── CLI: cachly health ────────────────────────────────────────────────────────
|
|
1855
|
+
// Usage: npx @cachly-dev/mcp-server@latest health
|
|
1856
|
+
// Checks: JWT valid, Brain API reachable, editor configs found, git hook present.
|
|
1857
|
+
|
|
1858
|
+
// ── CLI: cachly status ────────────────────────────────────────────────────────
|
|
1859
|
+
// Usage: npx @cachly-dev/mcp-server@latest status
|
|
1860
|
+
// Shows Brain health, team size, and quick stats at a glance.
|
|
1861
|
+
|
|
1862
|
+
if (process.argv[2] === 'status') {
|
|
1863
|
+
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1864
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1865
|
+
|
|
1866
|
+
if (!apiKey || !instanceId) {
|
|
1867
|
+
console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1868
|
+
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
1869
|
+
process.exit(1);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
try {
|
|
1873
|
+
const [statsRes, referralRes] = await Promise.all([
|
|
1874
|
+
fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
1875
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1876
|
+
signal: AbortSignal.timeout(8000),
|
|
1877
|
+
}),
|
|
1878
|
+
fetch(`${API_URL}/api/v1/referral/me`, {
|
|
1879
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1880
|
+
signal: AbortSignal.timeout(8000),
|
|
1881
|
+
}),
|
|
1882
|
+
]);
|
|
1883
|
+
|
|
1884
|
+
if (!statsRes.ok) throw new Error(`stats HTTP ${statsRes.status}`);
|
|
1885
|
+
const stats = await statsRes.json() as {
|
|
1886
|
+
lesson_count?: number; total_recall_count?: number;
|
|
1887
|
+
quality_score?: number; team_authors?: string[];
|
|
1888
|
+
};
|
|
1889
|
+
const referral = referralRes.ok
|
|
1890
|
+
? await referralRes.json() as { url?: string; referral_count?: number }
|
|
1891
|
+
: null;
|
|
1892
|
+
|
|
1893
|
+
const lessons = stats.lesson_count ?? 0;
|
|
1894
|
+
const recalls = stats.total_recall_count ?? 0;
|
|
1895
|
+
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
1896
|
+
const team = stats.team_authors ?? [];
|
|
1897
|
+
const refCount = referral?.referral_count ?? 0;
|
|
1898
|
+
const refUrl = referral?.url ?? '';
|
|
1899
|
+
|
|
1900
|
+
const level = lessons === 0 ? 'Intern 🌱' :
|
|
1901
|
+
lessons < 10 ? 'Junior Dev 🔧' :
|
|
1902
|
+
lessons < 30 ? 'Mid Dev ⚡' :
|
|
1903
|
+
lessons < 60 ? 'Senior Dev 🧠' :
|
|
1904
|
+
lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
1905
|
+
|
|
1906
|
+
const statusIcon = lessons > 0 ? '\x1b[32m●\x1b[0m' : '\x1b[33m●\x1b[0m';
|
|
1907
|
+
|
|
1908
|
+
console.log('');
|
|
1909
|
+
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
1910
|
+
console.log(`│ ${statusIcon} \x1b[1mBrain status\x1b[0m · instance ${instanceId.slice(0, 8)}... │`);
|
|
1911
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1912
|
+
console.log(`│ Lessons : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Level : \x1b[32m${level.padEnd(18)}\x1b[0m│`);
|
|
1913
|
+
console.log(`│ Recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m │`);
|
|
1914
|
+
|
|
1915
|
+
if (team.length > 0) {
|
|
1916
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1917
|
+
const teamStr = team.slice(0, 4).join(', ');
|
|
1918
|
+
console.log(`│ \x1b[36m👥 Team Brain\x1b[0m · ${String(team.length).padEnd(2)} developer${team.length === 1 ? ' ' : 's'} sharing lessons${' '.repeat(Math.max(0, 19 - teamStr.length))} │`);
|
|
1919
|
+
console.log(`│ ${teamStr.slice(0, 56).padEnd(56)} │`);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
if (refUrl) {
|
|
1923
|
+
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
1924
|
+
const refLine = `${refUrl.slice(0, 44)}${refUrl.length > 44 ? '…' : ''}`;
|
|
1925
|
+
console.log(`│ \x1b[35m🔗 Invite link\x1b[0m · ${refCount > 0 ? `\x1b[32m${refCount} joined\x1b[0m` : 'share to grow your team'}${' '.repeat(Math.max(0, 35 - String(refCount).length))} │`);
|
|
1926
|
+
console.log(`│ \x1b[2m${refLine.padEnd(56)}\x1b[0m │`);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
1930
|
+
console.log('');
|
|
1931
|
+
if (lessons === 0) {
|
|
1932
|
+
console.log(' \x1b[33m💡 No lessons yet.\x1b[0m Run a session with cachly connected to start learning.');
|
|
1933
|
+
console.log('');
|
|
1934
|
+
}
|
|
1935
|
+
} catch (e) {
|
|
1936
|
+
console.log(`\n❌ Could not fetch status: ${(e as Error).message}\n`);
|
|
1937
|
+
process.exit(1);
|
|
1938
|
+
}
|
|
1939
|
+
process.exit(0);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
if (process.argv[2] === 'health') {
|
|
1943
|
+
const { existsSync } = await import('node:fs');
|
|
1944
|
+
const { readFile } = await import('node:fs/promises');
|
|
1945
|
+
const { resolve, join: pJoin } = await import('node:path');
|
|
1946
|
+
|
|
1947
|
+
const cwd = process.cwd();
|
|
1948
|
+
let passed = 0;
|
|
1949
|
+
let failed = 0;
|
|
1950
|
+
|
|
1951
|
+
const ok = (msg: string) => { console.log(` ✅ ${msg}`); passed++; };
|
|
1952
|
+
const warn = (msg: string) => { console.log(` ⚠️ ${msg}`); };
|
|
1953
|
+
const fail = (msg: string) => { console.log(` ❌ ${msg}`); failed++; };
|
|
1954
|
+
|
|
1955
|
+
console.log('\n🧠 cachly health check\n');
|
|
1956
|
+
|
|
1957
|
+
// ── 1. JWT token ────────────────────────────────────────────────────────────
|
|
1958
|
+
console.log('🔑 Auth token');
|
|
1959
|
+
const jwt = process.env.CACHLY_JWT ?? '';
|
|
1960
|
+
if (!jwt) {
|
|
1961
|
+
fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
|
|
1962
|
+
} else {
|
|
1963
|
+
try {
|
|
1964
|
+
const parts = jwt.split('.');
|
|
1965
|
+
if (parts.length !== 3) throw new Error('not a JWT');
|
|
1966
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
|
|
1967
|
+
sub?: string; exp?: number;
|
|
1968
|
+
};
|
|
1969
|
+
const expMs = payload.exp ? payload.exp * 1000 : null;
|
|
1970
|
+
const minsLeft = expMs ? Math.floor((expMs - Date.now()) / 60_000) : null;
|
|
1971
|
+
if (expMs && expMs < Date.now()) {
|
|
1972
|
+
fail(`JWT expired ${Math.abs(minsLeft!)} minute(s) ago — get a new one: https://cachly.dev/setup-ai`);
|
|
1973
|
+
} else if (minsLeft !== null && minsLeft < 30) {
|
|
1974
|
+
warn(`JWT expires in ${minsLeft} minute(s) — refresh soon at https://cachly.dev/setup-ai`);
|
|
1975
|
+
} else {
|
|
1976
|
+
ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
|
|
1977
|
+
}
|
|
1978
|
+
} catch {
|
|
1979
|
+
fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// ── 2. Brain API reachability ───────────────────────────────────────────────
|
|
1984
|
+
console.log('\n🌐 Brain API');
|
|
1985
|
+
const apiUrl = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
1986
|
+
try {
|
|
1987
|
+
const res = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(5000) });
|
|
1988
|
+
if (res.ok) {
|
|
1989
|
+
const body = await res.json() as { status?: string };
|
|
1990
|
+
ok(`${apiUrl} reachable (status: ${body.status ?? 'ok'})`);
|
|
1991
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
1992
|
+
ok(`${apiUrl} reachable (auth required — expected)`);
|
|
1993
|
+
} else {
|
|
1994
|
+
fail(`${apiUrl}/health returned HTTP ${res.status}`);
|
|
1995
|
+
}
|
|
1996
|
+
} catch (e) {
|
|
1997
|
+
fail(`${apiUrl} unreachable — ${(e as Error).message}`);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// ── 3. Brain instance ───────────────────────────────────────────────────────
|
|
2001
|
+
console.log('\n🧠 Brain instance');
|
|
2002
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
2003
|
+
if (!instanceId) {
|
|
2004
|
+
warn('CACHLY_BRAIN_INSTANCE_ID not set — will auto-resolve on first tool call');
|
|
2005
|
+
} else if (jwt) {
|
|
2006
|
+
try {
|
|
2007
|
+
const instRes = await fetch(`${apiUrl}/api/v1/instances/${instanceId}`, {
|
|
2008
|
+
headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' },
|
|
2009
|
+
signal: AbortSignal.timeout(5000),
|
|
2010
|
+
});
|
|
2011
|
+
if (instRes.ok) {
|
|
2012
|
+
const inst = await instRes.json() as { name?: string; status?: string; host?: string; port?: number; tls_enabled?: boolean };
|
|
2013
|
+
if (inst.status === 'running') {
|
|
2014
|
+
ok(`Instance "${inst.name}" (${instanceId.slice(0, 8)}…) is running`);
|
|
2015
|
+
// Attempt actual Redis PING to verify connectivity end-to-end
|
|
2016
|
+
console.log('\n🔌 Redis connectivity');
|
|
2017
|
+
if (inst.host && inst.port) {
|
|
2018
|
+
try {
|
|
2019
|
+
let pingPassword: string | undefined;
|
|
2020
|
+
let pingTls = inst.tls_enabled !== false;
|
|
2021
|
+
try {
|
|
2022
|
+
const connRes = await fetch(`${apiUrl}/api/v1/instances/${instanceId}/connection`, {
|
|
2023
|
+
headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' },
|
|
2024
|
+
signal: AbortSignal.timeout(5000),
|
|
2025
|
+
});
|
|
2026
|
+
if (connRes.ok) {
|
|
2027
|
+
const conn = await connRes.json() as { password?: string; tls_enabled?: boolean };
|
|
2028
|
+
pingPassword = conn.password;
|
|
2029
|
+
pingTls = conn.tls_enabled !== false;
|
|
2030
|
+
}
|
|
2031
|
+
} catch { /* proceed without password */ }
|
|
2032
|
+
const pingClient = new Redis({
|
|
2033
|
+
host: inst.host, port: inst.port,
|
|
2034
|
+
password: pingPassword,
|
|
2035
|
+
...(pingTls ? { tls: {} } : {}),
|
|
2036
|
+
lazyConnect: true, connectTimeout: 5000, retryStrategy: () => null,
|
|
2037
|
+
});
|
|
2038
|
+
await pingClient.connect();
|
|
2039
|
+
const pong = await pingClient.ping();
|
|
2040
|
+
pingClient.disconnect();
|
|
2041
|
+
if (pong === 'PONG') {
|
|
2042
|
+
ok(`Redis PING → PONG (${inst.host}:${inst.port})`);
|
|
2043
|
+
} else {
|
|
2044
|
+
warn(`Redis responded but not PONG: ${pong}`);
|
|
2045
|
+
}
|
|
2046
|
+
} catch (pingErr) {
|
|
2047
|
+
fail(`Redis unreachable — ${(pingErr as Error).message}`);
|
|
2048
|
+
}
|
|
2049
|
+
} else {
|
|
2050
|
+
warn('Instance has no host/port yet');
|
|
2051
|
+
}
|
|
2052
|
+
} else {
|
|
2053
|
+
warn(`Instance "${inst.name}" status: ${inst.status}`);
|
|
2054
|
+
}
|
|
2055
|
+
} else {
|
|
2056
|
+
fail(`Instance ${instanceId.slice(0, 8)}… not found (HTTP ${instRes.status})`);
|
|
2057
|
+
}
|
|
2058
|
+
} catch (e) {
|
|
2059
|
+
fail(`Could not check instance — ${(e as Error).message}`);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// ── 4. Editor configs ───────────────────────────────────────────────────────
|
|
2064
|
+
console.log('\n🛠 Editor configs (project dir)');
|
|
2065
|
+
const editorConfigs: Array<{ editor: string; path: string }> = [
|
|
2066
|
+
{ editor: 'Claude Code', path: '.mcp.json' },
|
|
2067
|
+
{ editor: 'Cursor', path: '.cursor/mcp.json' },
|
|
2068
|
+
{ editor: 'Windsurf', path: '.windsurf/mcp.json' },
|
|
2069
|
+
{ editor: 'Copilot / Cline', path: '.vscode/mcp.json' },
|
|
2070
|
+
{ editor: 'Zed', path: '.zed/settings.json' },
|
|
2071
|
+
{ editor: 'Continue.dev', path: '.continue/config.json' },
|
|
2072
|
+
];
|
|
2073
|
+
let configsFound = 0;
|
|
2074
|
+
for (const { editor, path } of editorConfigs) {
|
|
2075
|
+
const fullPath = resolve(cwd, path);
|
|
2076
|
+
if (existsSync(fullPath)) {
|
|
2077
|
+
try {
|
|
2078
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
2079
|
+
const hasCachly = content.includes('@cachly-dev/mcp-server') || content.includes('cachly');
|
|
2080
|
+
if (hasCachly) {
|
|
2081
|
+
ok(`${editor}: ${path}`);
|
|
2082
|
+
configsFound++;
|
|
2083
|
+
} else {
|
|
2084
|
+
warn(`${editor}: ${path} exists but cachly not found inside`);
|
|
2085
|
+
}
|
|
2086
|
+
} catch {
|
|
2087
|
+
warn(`${editor}: ${path} exists but could not read`);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
if (configsFound === 0) {
|
|
2092
|
+
fail('No editor MCP configs found — run: npx @cachly-dev/mcp-server@latest setup');
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// ── 5. Git hook ─────────────────────────────────────────────────────────────
|
|
2096
|
+
console.log('\n🪝 Git hook (ambient learning)');
|
|
2097
|
+
const hookPath = resolve(cwd, '.git', 'hooks', 'post-commit');
|
|
2098
|
+
if (!existsSync(resolve(cwd, '.git'))) {
|
|
2099
|
+
warn('Not in a git repository — skip');
|
|
2100
|
+
} else if (existsSync(hookPath)) {
|
|
2101
|
+
try {
|
|
2102
|
+
const hook = await readFile(hookPath, 'utf-8');
|
|
2103
|
+
if (hook.includes('cachly')) {
|
|
2104
|
+
ok('.git/hooks/post-commit (cachly CLS hook present)');
|
|
2105
|
+
} else {
|
|
2106
|
+
warn('.git/hooks/post-commit exists but cachly hook not found — run setup to add it');
|
|
2107
|
+
}
|
|
2108
|
+
} catch {
|
|
2109
|
+
warn('.git/hooks/post-commit exists but could not read');
|
|
2110
|
+
}
|
|
2111
|
+
} else {
|
|
2112
|
+
warn('.git/hooks/post-commit not found — run: npx @cachly-dev/mcp-server@latest setup');
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
2116
|
+
console.log(`\n${'─'.repeat(40)}`);
|
|
2117
|
+
if (failed === 0) {
|
|
2118
|
+
console.log(`✅ All checks passed (${passed} ok)\n`);
|
|
2119
|
+
} else {
|
|
2120
|
+
console.log(`❌ ${failed} check(s) failed, ${passed} passed\n`);
|
|
2121
|
+
console.log(`💡 Fix issues with: npx @cachly-dev/mcp-server@latest setup\n`);
|
|
2122
|
+
process.exit(1);
|
|
2123
|
+
}
|
|
1138
2124
|
process.exit(0);
|
|
1139
2125
|
}
|
|
1140
2126
|
|
|
@@ -1147,6 +2133,8 @@ if (process.argv[2] === 'setup') {
|
|
|
1147
2133
|
const { resolve, dirname } = await import('node:path');
|
|
1148
2134
|
const { createInterface } = await import('node:readline');
|
|
1149
2135
|
|
|
2136
|
+
sendFunnelEvent('setup_started');
|
|
2137
|
+
|
|
1150
2138
|
// --yes / -y → non-interactive mode (skips all prompts, picks defaults)
|
|
1151
2139
|
const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
1152
2140
|
|
|
@@ -1372,18 +2360,22 @@ if (process.argv[2] === 'setup') {
|
|
|
1372
2360
|
console.log(`Step 3: Configuring for: ${editorsToSetup.map(editorLabel).join(', ')}\n`);
|
|
1373
2361
|
|
|
1374
2362
|
// ── Step 4: Write editor configs (project-level) ─────────────────────────
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
|
|
1378
|
-
// .cachly-backup file next to the original for one-step recovery.
|
|
2363
|
+
// Uses mergeMcpConfig to preserve all existing MCP servers — only adds/updates
|
|
2364
|
+
// the cachly entry without touching filesystem, github, or other servers.
|
|
2365
|
+
const { existsSync: exSetup } = await import('node:fs');
|
|
1379
2366
|
for (const editor of editorsToSetup) {
|
|
1380
2367
|
const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
|
|
1381
2368
|
const configPath = resolve(cwd, configFile);
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
2369
|
+
try {
|
|
2370
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
2371
|
+
const wasExisting = exSetup(configPath);
|
|
2372
|
+
const merged = await mergeMcpConfig(configPath, token, instance.id, editor, { readFile, existsSync: exSetup });
|
|
2373
|
+
await writeFile(configPath, merged, 'utf-8');
|
|
2374
|
+
console.log(`✅ ${wasExisting ? 'Updated' : 'Written'}: ${configFile}`);
|
|
2375
|
+
} catch (writeErr) {
|
|
2376
|
+
console.log(`⚠️ Could not write ${configFile}: ${(writeErr as Error).message}`);
|
|
2377
|
+
console.log(` Fix permissions or run with sudo, then re-run setup.`);
|
|
2378
|
+
}
|
|
1387
2379
|
}
|
|
1388
2380
|
|
|
1389
2381
|
// ── Step 4b: Write global Claude Code config (~/.claude/mcp.json) ─────────
|
|
@@ -1424,8 +2416,8 @@ if (process.argv[2] === 'setup') {
|
|
|
1424
2416
|
JWT = token; // set global JWT so handleTool can authenticate
|
|
1425
2417
|
const gitResult = await handleTool('brain_from_git', {
|
|
1426
2418
|
instance_id: instance.id,
|
|
1427
|
-
|
|
1428
|
-
|
|
2419
|
+
repo_path: cwd,
|
|
2420
|
+
limit: 100,
|
|
1429
2421
|
});
|
|
1430
2422
|
const match = gitResult.match(/(\d+) lesson/);
|
|
1431
2423
|
console.log(` ✓ ${match ? match[1] + ' lessons' : 'done'} extracted from git history`);
|
|
@@ -1502,9 +2494,12 @@ if (process.argv[2] === 'setup') {
|
|
|
1502
2494
|
}
|
|
1503
2495
|
} catch { console.log(' (skipped)'); }
|
|
1504
2496
|
|
|
1505
|
-
console.log(`\n
|
|
1506
|
-
console.log(`
|
|
1507
|
-
console.log(`
|
|
2497
|
+
console.log(`\n🎉 Your AI just got a permanent brain.`);
|
|
2498
|
+
console.log(` Restart your editor — from this session on, it arrives pre-briefed.\n`);
|
|
2499
|
+
console.log(` No more re-explaining your stack. No repeated mistakes.\n`);
|
|
2500
|
+
console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
|
|
2501
|
+
console.log(`\n 📛 Add a live badge to your README:`);
|
|
2502
|
+
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
1508
2503
|
|
|
1509
2504
|
// ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
|
|
1510
2505
|
if (!nonInteractive) {
|
|
@@ -1554,7 +2549,13 @@ if (process.argv[2] === 'setup') {
|
|
|
1554
2549
|
}
|
|
1555
2550
|
} catch { /* non-critical */ }
|
|
1556
2551
|
|
|
2552
|
+
// Setup reached the end successfully — close the funnel.
|
|
2553
|
+
JWT = token;
|
|
2554
|
+
sendFunnelEvent('setup_completed', { instance_id: instance.id });
|
|
2555
|
+
|
|
1557
2556
|
rl.close();
|
|
2557
|
+
// Give the fire-and-forget telemetry a moment to flush before exit.
|
|
2558
|
+
await new Promise(r => setTimeout(r, 300));
|
|
1558
2559
|
process.exit(0);
|
|
1559
2560
|
}
|
|
1560
2561
|
|
|
@@ -1600,12 +2601,59 @@ if (process.argv[2] === 'index') {
|
|
|
1600
2601
|
process.exit(0);
|
|
1601
2602
|
}
|
|
1602
2603
|
|
|
2604
|
+
// ── CLI: cachly learn-git ─────────────────────────────────────────────────────
|
|
2605
|
+
// Usage: npx @cachly-dev/mcp-server@latest learn-git [./repo] [--max-commits 50]
|
|
2606
|
+
// Auto-learns brain lessons from recent git commits — ideal for CI on PR merge.
|
|
2607
|
+
// Each meaningful commit becomes a lesson, so the Brain grows with zero manual work.
|
|
2608
|
+
|
|
2609
|
+
if (process.argv[2] === 'learn-git') {
|
|
2610
|
+
const { resolve } = await import('node:path');
|
|
2611
|
+
const argv = process.argv.slice(3);
|
|
2612
|
+
const flag = (name: string) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
|
|
2613
|
+
|
|
2614
|
+
const repoDir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
2615
|
+
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2616
|
+
const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
|
|
2617
|
+
|
|
2618
|
+
if (!instanceId || !JWT) {
|
|
2619
|
+
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2620
|
+
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2621
|
+
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
2622
|
+
console.error(' npx @cachly-dev/mcp-server@latest learn-git .\n');
|
|
2623
|
+
process.exit(1);
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
console.log(`\n🧠 Learning from git history: ${repoDir}`);
|
|
2627
|
+
console.log(` Instance: ${instanceId.slice(0, 8)}… Max commits: ${maxCommits}\n`);
|
|
2628
|
+
|
|
2629
|
+
try {
|
|
2630
|
+
const result = await handleTool('brain_from_git', {
|
|
2631
|
+
instance_id: instanceId,
|
|
2632
|
+
repo_path: repoDir,
|
|
2633
|
+
limit: maxCommits,
|
|
2634
|
+
});
|
|
2635
|
+
console.log(result);
|
|
2636
|
+
sendFunnelEvent('brain_from_git', { api_key: JWT, instance_id: instanceId });
|
|
2637
|
+
console.log('\n✅ Brain learned from your commits.\n');
|
|
2638
|
+
// Let fire-and-forget telemetry flush before exit.
|
|
2639
|
+
await new Promise(r => setTimeout(r, 300));
|
|
2640
|
+
} catch (err) {
|
|
2641
|
+
console.error(`\n❌ learn-git failed: ${(err as Error).message}\n`);
|
|
2642
|
+
process.exit(1);
|
|
2643
|
+
}
|
|
2644
|
+
process.exit(0);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
1603
2647
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
1604
2648
|
|
|
1605
2649
|
// Warn on stderr when credentials are missing so the user sees a clear
|
|
1606
2650
|
// actionable message in their editor's MCP log instead of silent failures.
|
|
1607
|
-
|
|
1608
|
-
|
|
2651
|
+
// Skip for CLI commands that intentionally run without credentials.
|
|
2652
|
+
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
|
|
2653
|
+
if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
2654
|
+
// No args + no JWT in a non-TTY context = running as MCP server without credentials.
|
|
2655
|
+
// Print actionable setup banner to stdout so it's captured by callers/tests.
|
|
2656
|
+
const banner =
|
|
1609
2657
|
'\n' +
|
|
1610
2658
|
'╔══════════════════════════════════════════════════════════════════╗\n' +
|
|
1611
2659
|
'║ 🧠 cachly AI Brain — Setup required ║\n' +
|
|
@@ -1621,8 +2669,15 @@ if (!JWT) {
|
|
|
1621
2669
|
'║ ║\n' +
|
|
1622
2670
|
'║ Free tier — no credit card required. ║\n' +
|
|
1623
2671
|
'╚══════════════════════════════════════════════════════════════════╝\n' +
|
|
1624
|
-
'\n'
|
|
1625
|
-
|
|
2672
|
+
'\n';
|
|
2673
|
+
// When invoked with no args as the main entry point, write to stdout + exit cleanly.
|
|
2674
|
+
// When used as MCP server (args present), write to stderr so editors see it.
|
|
2675
|
+
if (!process.argv[2] && _isMain) {
|
|
2676
|
+
process.stdout.write(banner);
|
|
2677
|
+
process.exit(0);
|
|
2678
|
+
} else {
|
|
2679
|
+
process.stderr.write(banner);
|
|
2680
|
+
}
|
|
1626
2681
|
} else {
|
|
1627
2682
|
// Warn if the JWT is already expired or expiring within the hour.
|
|
1628
2683
|
const expMs = jwtExpiryMs(JWT);
|