@cachly-dev/mcp-server 0.10.24 → 0.10.25

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +0 -131
  2. package/README.md +112 -136
  3. package/dist/__tests__/brain-predict-failures.test.d.ts +17 -0
  4. package/dist/__tests__/brain-predict-failures.test.js +339 -0
  5. package/dist/__tests__/brain-predict-failures.test.js.map +1 -0
  6. package/dist/handlers/brain.js +67 -7
  7. package/dist/handlers/brain.js.map +1 -1
  8. package/dist/handlers/fedbrain.js +44 -93
  9. package/dist/handlers/fedbrain.js.map +1 -1
  10. package/dist/handlers/roadmap.js +2 -8
  11. package/dist/handlers/roadmap.js.map +1 -1
  12. package/dist/handlers/syndicate.js +7 -15
  13. package/dist/handlers/syndicate.js.map +1 -1
  14. package/dist/handlers/team.js +50 -62
  15. package/dist/handlers/team.js.map +1 -1
  16. package/dist/index.js +122 -979
  17. package/dist/index.js.map +1 -1
  18. package/dist/tools.d.ts +54 -3
  19. package/dist/tools.js +62 -8
  20. package/dist/tools.js.map +1 -1
  21. package/package.json +13 -29
  22. package/scripts/postinstall.js +12 -12
  23. package/server.json +9 -3
  24. package/smithery.yaml +8 -15
  25. package/src/__tests__/brain-predict-failures.test.ts +430 -0
  26. package/src/handlers/brain.ts +64 -7
  27. package/src/handlers/fedbrain.ts +38 -88
  28. package/src/handlers/roadmap.ts +2 -7
  29. package/src/handlers/syndicate.ts +7 -13
  30. package/src/handlers/team.ts +33 -57
  31. package/src/index.ts +135 -1011
  32. package/src/tools.ts +66 -8
  33. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  34. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  35. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  36. package/.github/workflows/publish.yml +0 -72
  37. package/LAUNCH_TEXTS.md +0 -182
  38. package/LICENSE +0 -202
  39. package/dist/__tests__/auth.test.d.ts +0 -1
  40. package/dist/__tests__/auth.test.js +0 -107
  41. package/dist/__tests__/auth.test.js.map +0 -1
  42. package/glama.json +0 -36
  43. package/llms.txt +0 -155
  44. package/src/__tests__/auth.test.ts +0 -147
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { jwtExpiryMs, checkJwt, handleApiError } from './auth.js';
3
- import { readFile } from 'node:fs/promises';
4
3
  /**
5
4
  * cachly MCP Server v0.4.0
6
5
  *
@@ -43,7 +42,7 @@ import { Redis } from 'ioredis';
43
42
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
44
43
  let JWT = process.env.CACHLY_JWT ?? '';
45
44
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
46
- const CURRENT_VERSION = '0.10.24';
45
+ const CURRENT_VERSION = '0.10.10';
47
46
  // ── Default Instance Resolution (for Smithery & single-credential setups) ────
48
47
  // When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
49
48
  // When neither is set, we auto-fetch the first running instance once per process.
@@ -140,7 +139,6 @@ async function pollDeviceFlow(flow) {
140
139
  JWT = apiKey;
141
140
  setEmbedJwt(apiKey); // keep embeddings.ts in sync
142
141
  _deviceFlow = null;
143
- sendFunnelEvent('device_flow_completed');
144
142
  // Auto-provision: find or create the user's brain instance.
145
143
  _defaultInstanceLastAttempt = 0;
146
144
  await resolveDefaultInstanceId();
@@ -157,17 +155,8 @@ async function pollDeviceFlow(flow) {
157
155
  if (id)
158
156
  _defaultInstanceId = id;
159
157
  }
160
- else {
161
- // Log so telemetry can detect free-tier auto-provision failures
162
- void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
163
- method: 'POST',
164
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
165
- body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
166
- signal: AbortSignal.timeout(3000),
167
- }).catch(() => { });
168
- }
169
158
  }
170
- catch { /* non-fatal — server unreachable, getConnection will surface the error */ }
159
+ catch { /* non-fatal */ }
171
160
  }
172
161
  // Give a provisioning instance a head-start so getConnection's wait is shorter.
173
162
  // The instance is almost always ready by the time the user's tool re-enters.
@@ -248,28 +237,11 @@ async function getConnection(instance_id) {
248
237
  body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
249
238
  signal: AbortSignal.timeout(3000),
250
239
  }).catch(() => { });
251
- let hint;
252
- if (inst.status === 'provisioning') {
253
- 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.`;
254
- }
255
- else if (inst.status === 'failed') {
256
- hint = `❌ Brain instance "${inst.name}" failed to start.\n\n` +
257
- `Our system will retry automatically. Check status at: https://cachly.dev/instances\n` +
258
- `If this persists, contact support@cachly.dev.`;
259
- }
260
- else if (inst.status === 'suspended') {
261
- hint = `⏸ Brain instance "${inst.name}" is suspended (billing issue).\n\n` +
262
- `Update your payment method at: https://cachly.dev/billing`;
263
- }
264
- else if (inst.status === 'pending_payment') {
265
- hint = `💳 Brain instance "${inst.name}" is waiting for payment.\n\n` +
266
- `Complete your checkout at: https://cachly.dev/instances`;
267
- }
268
- else {
269
- hint = `Brain instance "${inst.name}" is not reachable (status: ${inst.status}).\n\n` +
240
+ const hint = inst.status === 'provisioning'
241
+ ? `⏳ Brain instance "${inst.name}" is still starting up.\n\nThis usually finishes within 30 seconds. Please try again in a moment.`
242
+ : `Brain instance "${inst.name}" is not reachable (status: ${inst.status}).\n\n` +
270
243
  `• View your instance at: https://cachly.dev/instances\n` +
271
244
  `• Run \`get_api_status\` for a full diagnostic.`;
272
- }
273
245
  throw new McpError(ErrorCode.InvalidRequest, hint);
274
246
  }
275
247
  if (!inst.host || !inst.port) {
@@ -283,14 +255,8 @@ async function getConnection(instance_id) {
283
255
  password = conn.password ?? undefined;
284
256
  tlsEnabled = conn.tls_enabled !== false;
285
257
  }
286
- catch (connErr) {
287
- // If the connection endpoint fails and the instance has a password, we cannot connect.
288
- // Surface the error so the user knows why Redis auth will fail rather than getting a cryptic NOAUTH.
289
- const msg = connErr instanceof Error ? connErr.message : String(connErr);
290
- if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
291
- throw new McpError(ErrorCode.InvalidRequest, `API key rejected — run \`cachly setup\` to refresh your credentials.`);
292
- }
293
- // Other failures (timeout, 500): proceed without password, Redis will surface NOAUTH if needed
258
+ catch {
259
+ // Fallback: use TLS flag from instance metadata
294
260
  }
295
261
  const client = new Redis({
296
262
  host: inst.host,
@@ -361,40 +327,23 @@ import { TOOLS } from './tools.js';
361
327
  // ── Handlers ──────────────────────────────────────────────────────────────────
362
328
  // Fires once per process when no JWT is set (anonymous, opt-out via CACHLY_NO_TELEMETRY=1)
363
329
  let _telemetryPingSent = false;
364
- // Fires once per process on the first successful Brain tool call — key activation metric
365
- let _firstCallSuccessSent = false;
366
- function detectEditor() {
367
- return process.env.CURSOR_TRACE_ID ? 'cursor'
368
- : process.env.WINDSURF_SESSION_ID ? 'windsurf'
369
- : process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
370
- : process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
371
- : 'unknown';
372
- }
373
- function sendFunnelEvent(event, extra) {
374
- if (process.env.CACHLY_NO_TELEMETRY === '1')
375
- return;
376
- void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
377
- method: 'POST',
378
- headers: { 'Content-Type': 'application/json' },
379
- body: JSON.stringify({
380
- event, version: CURRENT_VERSION, editor: detectEditor(),
381
- ...(JWT ? { jwt: JWT } : {}),
382
- ...extra,
383
- }),
384
- signal: AbortSignal.timeout(3000),
385
- }).catch(() => { });
386
- }
387
330
  async function sendAnonymousTelemetry(toolName) {
388
331
  if (_telemetryPingSent)
389
332
  return;
390
333
  if (process.env.CACHLY_NO_TELEMETRY === '1')
391
334
  return;
392
335
  _telemetryPingSent = true;
336
+ // Detect editor from common env vars injected by IDE extensions
337
+ const editor = process.env.CURSOR_TRACE_ID ? 'cursor'
338
+ : process.env.WINDSURF_SESSION_ID ? 'windsurf'
339
+ : process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
340
+ : process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
341
+ : 'unknown';
393
342
  try {
394
343
  await fetch(`${API_URL}/api/v1/telemetry/mcp`, {
395
344
  method: 'POST',
396
345
  headers: { 'Content-Type': 'application/json' },
397
- body: JSON.stringify({ event: 'first_call_no_jwt', version: CURRENT_VERSION, editor: detectEditor(), tool: toolName }),
346
+ body: JSON.stringify({ event: 'first_call_no_jwt', version: CURRENT_VERSION, editor, tool: toolName }),
398
347
  signal: AbortSignal.timeout(3000),
399
348
  });
400
349
  }
@@ -431,29 +380,18 @@ async function handleTool(name, args) {
431
380
  const flow = await startDeviceFlow();
432
381
  if (flow) {
433
382
  _deviceFlow = flow;
434
- sendFunnelEvent('device_flow_started', { tool: name });
435
- // Try to open the browser automatically — fire-and-forget, never block
436
- void (async () => {
437
- try {
438
- const { exec } = await import('node:child_process');
439
- const url = flow.verifyUrl;
440
- const cmd = process.platform === 'win32' ? `start "" "${url}"`
441
- : process.platform === 'darwin' ? `open "${url}"`
442
- : `xdg-open "${url}"`;
443
- exec(cmd);
444
- }
445
- catch { /* non-critical */ }
446
- })();
447
383
  return [
448
- '🧠 **cachly AI Brain — sign in to activate** (browser opening...)',
384
+ '🧠 **cachly AI Brain — One-click sign in**',
449
385
  '',
450
- `👉 **${flow.verifyUrl}**`,
386
+ '1. Open this URL in your browser (it may open automatically):',
387
+ ` **${flow.verifyUrl}**`,
451
388
  '',
452
- `Code: **${flow.userCode}** (pre-filled if browser opened automatically)`,
389
+ `2. Enter this code if prompted: **${flow.userCode}**`,
453
390
  '',
454
- 'After sign-in: call **any tool again** — your Brain activates instantly.',
391
+ '3. After sign-in, call this tool again — it will proceed automatically.',
455
392
  '',
456
- '✨ Free forever · No credit card · 89 MCP tools · GDPR · EU servers',
393
+ '✨ Free tier includes: 1 Brain instance, persistent memory, 63 MCP tools.',
394
+ ' No credit card required.',
457
395
  ].join('\n');
458
396
  }
459
397
  // Device flow unavailable (network issue) — fall back to manual setup
@@ -478,22 +416,12 @@ async function handleTool(name, args) {
478
416
  }
479
417
  // Delegate brain tools (learn, recall, session, etc.)
480
418
  const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
481
- if (brainResult !== null) {
482
- if (!_firstCallSuccessSent && JWT) {
483
- _firstCallSuccessSent = true;
484
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
485
- }
419
+ if (brainResult !== null)
486
420
  return brainResult;
487
- }
488
421
  // Delegate context tools (remember/recall/list/forget)
489
422
  const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
490
- if (contextResult !== null) {
491
- if (!_firstCallSuccessSent && JWT) {
492
- _firstCallSuccessSent = true;
493
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
494
- }
423
+ if (contextResult !== null)
495
424
  return contextResult;
496
- }
497
425
  // Delegate instance + cache tools
498
426
  const instanceResult = await handleInstanceTool(name, args, getConnection, apiFetch);
499
427
  if (instanceResult !== null)
@@ -629,7 +557,32 @@ async function handleTool(name, args) {
629
557
  }
630
558
  }
631
559
  const server = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} } });
632
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
560
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
561
+ if (!JWT) {
562
+ // No credentials — inject sign-up CTA into session_start description
563
+ const tools = TOOLS.map(t => {
564
+ if (t.name !== 'session_start')
565
+ return t;
566
+ return {
567
+ ...t,
568
+ description: `⚡ FREE AI BRAIN — sign up once at https://cachly.dev/sign-up (no credit card) ` +
569
+ `then run: npx @cachly-dev/mcp-server@latest setup\n\n` + t.description,
570
+ };
571
+ });
572
+ return { tools };
573
+ }
574
+ // ── Auto-start session on connection (fire and forget) ────────────────────
575
+ // When the AI reads the tools list, it has just connected. Proactively start
576
+ // the Brain session in the background so by the time the first tool call
577
+ // arrives, session context is already loaded and ready to inject.
578
+ if (!_autoSessionStarted && !_autoSessionStarting) {
579
+ void resolveDefaultInstanceId().then(instanceId => {
580
+ if (instanceId)
581
+ void autoStartSession(instanceId).catch(() => undefined);
582
+ }).catch(() => undefined);
583
+ }
584
+ return { tools: TOOLS };
585
+ });
633
586
  // ── Auto-session management ───────────────────────────────────────────────────
634
587
  // Transparently starts a Brain session on the first tool call of a process
635
588
  // and saves a session_end summary on SIGTERM/exit so users never have to call
@@ -638,62 +591,87 @@ let _autoSessionInstanceId = null;
638
591
  let _autoSessionStarted = false;
639
592
  let _autoSessionStarting = null; // guard against double-start race
640
593
  let _autoSessionToolCount = 0;
594
+ let _autoSessionContext = null; // result of session_start, injected into first tool response
595
+ let _autoSessionContextInjected = false; // only inject once
641
596
  // ── brain_from_git concurrency limiter ────────────────────────────────────────
642
597
  // Spawning many `git log` subprocesses in parallel can fork-bomb the host.
643
598
  // This simple semaphore caps concurrent brain_from_git calls to 10 per process.
644
599
  async function autoStartSession(instanceId) {
645
600
  if (_autoSessionStarted)
646
- return;
601
+ return null;
647
602
  // If another call already started the session, await it instead of double-starting.
648
603
  if (_autoSessionStarting) {
649
- await _autoSessionStarting;
650
- return;
604
+ return _autoSessionStarting;
651
605
  }
652
606
  _autoSessionStarting = (async () => {
653
607
  _autoSessionStarted = true;
654
608
  _autoSessionInstanceId = instanceId;
609
+ let sessionResult = null;
655
610
  try {
656
- await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)' });
611
+ // Auto-detect git context for a meaningful session focus
612
+ let focus = 'auto (MCP session)';
613
+ try {
614
+ const { execSync } = await import('node:child_process');
615
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
616
+ const lastMsg = execSync('git log -1 --pretty=%s', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
617
+ if (branch && branch !== 'HEAD') {
618
+ focus = lastMsg ? `${branch}: ${lastMsg.slice(0, 80)}` : `branch: ${branch}`;
619
+ }
620
+ }
621
+ catch { /* git not available in this workspace */ }
622
+ sessionResult = await handleTool('session_start', { instance_id: instanceId, focus });
623
+ _autoSessionContext = sessionResult;
657
624
  }
658
625
  catch { /* non-fatal — session tracking is a best-effort feature */ }
659
626
  finally {
660
627
  _autoSessionStarting = null;
661
628
  }
662
- })();
663
- await _autoSessionStarting;
664
- // Auto-index the project if it hasn't been indexed in the last 24h.
665
- // This is the main lever for growing token savings: more indexed code →
666
- // more semantic cache hits → fewer LLM calls for repeated questions.
667
- if (process.env.CACHLY_AUTO_INDEX !== 'false') {
668
- try {
669
- const redis = await getConnection(instanceId);
670
- const lastIndexed = await redis.get(`cachly:index:last_indexed:${instanceId}`);
671
- const staleMs = 24 * 60 * 60 * 1000;
672
- const isStale = !lastIndexed || (Date.now() - parseInt(lastIndexed, 10)) > staleMs;
673
- if (isStale) {
674
- // Mark as indexing now to prevent concurrent re-runs.
675
- await redis.set(`cachly:index:last_indexed:${instanceId}`, String(Date.now()), 'EX', 90000);
676
- // Run in background — don't block the first tool call.
677
- handleTool('index_project', {
678
- instance_id: instanceId,
679
- dir: process.cwd(),
680
- max_files: 150,
681
- ttl: 86400 * 7, // cache for 7 days
682
- namespace: 'cachly:sem:code',
683
- }).catch(() => undefined);
629
+ // Auto-index the project if it hasn't been indexed in the last 24h.
630
+ // This is the main lever for growing token savings: more indexed code →
631
+ // more semantic cache hits → fewer LLM calls for repeated questions.
632
+ if (process.env.CACHLY_AUTO_INDEX !== 'false') {
633
+ try {
634
+ const redis = await getConnection(instanceId);
635
+ const lastIndexed = await redis.get(`cachly:index:last_indexed:${instanceId}`);
636
+ const staleMs = 24 * 60 * 60 * 1000;
637
+ const isStale = !lastIndexed || (Date.now() - parseInt(lastIndexed, 10)) > staleMs;
638
+ if (isStale) {
639
+ // Mark as indexing now to prevent concurrent re-runs.
640
+ await redis.set(`cachly:index:last_indexed:${instanceId}`, String(Date.now()), 'EX', 90000);
641
+ // Run in background — don't block the first tool call.
642
+ handleTool('index_project', {
643
+ instance_id: instanceId,
644
+ dir: process.cwd(),
645
+ max_files: 150,
646
+ ttl: 86400 * 7, // cache for 7 days
647
+ namespace: 'cachly:sem:code',
648
+ }).catch(() => undefined);
649
+ }
684
650
  }
651
+ catch { /* never block the session on indexing errors */ }
685
652
  }
686
- catch { /* never block the session on indexing errors */ }
687
- }
653
+ return sessionResult;
654
+ })();
655
+ return _autoSessionStarting;
688
656
  }
689
657
  async function autoEndSession() {
690
658
  if (!_autoSessionStarted || !_autoSessionInstanceId)
691
659
  return;
692
660
  _autoSessionStarted = false;
693
661
  try {
662
+ // Try to derive a meaningful summary from git
663
+ let summary = `Auto-ended after ${_autoSessionToolCount} tool call(s).`;
664
+ try {
665
+ const { execSync } = await import('node:child_process');
666
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
667
+ const lastMsg = execSync('git log -1 --pretty=%s', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
668
+ if (lastMsg)
669
+ summary = `${branch ? `[${branch}] ` : ''}${lastMsg} · ${_autoSessionToolCount} tool calls`;
670
+ }
671
+ catch { /* git unavailable */ }
694
672
  await handleTool('session_end', {
695
673
  instance_id: _autoSessionInstanceId,
696
- summary: `Auto-ended after ${_autoSessionToolCount} tool call(s). Session was started automatically.`,
674
+ summary,
697
675
  files_changed: [],
698
676
  lessons_learned: 0,
699
677
  });
@@ -702,7 +680,7 @@ async function autoEndSession() {
702
680
  }
703
681
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
704
682
  const { name, arguments: args } = request.params;
705
- // Auto-start brain session on first tool call that has an instance_id.
683
+ // Auto-start brain session if not yet started (fallback: ListTools already fired this).
706
684
  // Skip session management tools to avoid recursion.
707
685
  const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
708
686
  if (!sessionTools.has(name) && !_autoSessionStarted && !_autoSessionStarting) {
@@ -721,6 +699,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
721
699
  _autoSessionToolCount++;
722
700
  try {
723
701
  const text = await handleTool(name, (args ?? {}));
702
+ // ── Inject session context into the FIRST tool response ─────────────────
703
+ // This is the key: the AI sees the previous session summary, handoff tasks,
704
+ // and recent lessons automatically — without needing to call session_start.
705
+ // Only injected once per process (on the very first tool call).
706
+ if (!_autoSessionContextInjected && _autoSessionContext && !sessionTools.has(name)) {
707
+ _autoSessionContextInjected = true;
708
+ const ctx = _autoSessionContext;
709
+ _autoSessionContext = null; // free memory
710
+ return { content: [{ type: 'text', text: `${ctx}\n\n---\n\n${text}` }] };
711
+ }
724
712
  return { content: [{ type: 'text', text }] };
725
713
  }
726
714
  catch (err) {
@@ -796,54 +784,6 @@ function buildMcpConfig(apiKey, instanceId, editor) {
796
784
  },
797
785
  }, null, 2);
798
786
  }
799
- // mergeMcpConfig reads an existing config file (if any), merges the cachly entry,
800
- // and returns the updated JSON string — preserving all other MCP servers and settings.
801
- async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsOps) {
802
- const cachlyEntry = {
803
- command: 'npx',
804
- args: ['-y', '@cachly-dev/mcp-server@latest'],
805
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
806
- };
807
- if (!fsOps.existsSync(configPath))
808
- return buildMcpConfig(apiKey, instanceId, editor);
809
- let existing = {};
810
- try {
811
- existing = JSON.parse(await fsOps.readFile(configPath, 'utf-8'));
812
- }
813
- catch {
814
- // Corrupt or unreadable JSON — back it up by appending .bak, then start fresh
815
- try {
816
- const { rename } = await import('node:fs/promises');
817
- await rename(configPath, configPath + '.bak');
818
- process.stderr.write(`⚠️ ${configPath} was not valid JSON — backed up to ${configPath}.bak\n`);
819
- }
820
- catch { /* backup failed — still safe to overwrite with valid config */ }
821
- }
822
- if (editor === 'continue') {
823
- // Merge into experimental.modelContextProtocolServers array — replace cachly entry if present
824
- const exp = (existing['experimental'] ?? {});
825
- const servers = (exp['modelContextProtocolServers'] ?? []);
826
- const filtered = servers.filter((s) => {
827
- const env = (s['env'] ?? {});
828
- return !env['CACHLY_JWT'] && !env['CACHLY_BRAIN_INSTANCE_ID'];
829
- });
830
- filtered.push({ transport: { type: 'stdio', command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'] }, env: cachlyEntry.env });
831
- existing['experimental'] = { ...exp, modelContextProtocolServers: filtered };
832
- return JSON.stringify(existing, null, 2);
833
- }
834
- if (editor === 'zed') {
835
- // Merge only context_servers.cachly — preserve all other Zed settings
836
- const cs = (existing['context_servers'] ?? {});
837
- cs['cachly'] = { command: { path: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env: cachlyEntry.env }, settings: {} };
838
- existing['context_servers'] = cs;
839
- return JSON.stringify(existing, null, 2);
840
- }
841
- // Standard mcpServers format (Claude Code, Cursor, Windsurf, Copilot, Cline)
842
- const servers = (existing['mcpServers'] ?? {});
843
- servers['cachly'] = cachlyEntry;
844
- existing['mcpServers'] = servers;
845
- return JSON.stringify(existing, null, 2);
846
- }
847
787
  function buildClaudeMdBlock(instanceId) {
848
788
  return `${CLAUDE_MD_MARKER_START}
849
789
  ## Cachly AI Brain — Always Active
@@ -909,584 +849,6 @@ async function writeClaudeMd(projectDir, instanceId) {
909
849
  await writeFile(claudeMdPath, block.trimStart(), 'utf-8');
910
850
  return 'written';
911
851
  }
912
- // ── CLI: cachly digest ────────────────────────────────────────────────────────
913
- // Usage: npx @cachly-dev/mcp-server@latest digest
914
- // Weekly brain summary — what your AI learned this week. Shareable output.
915
- if (process.argv[2] === 'digest') {
916
- const apiKey = process.env.CACHLY_JWT ?? '';
917
- const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
918
- if (!apiKey || !instanceId) {
919
- console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
920
- console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
921
- process.exit(1);
922
- }
923
- process.stdout.write('\n🧠 Fetching your weekly Brain digest...\n\n');
924
- try {
925
- // Fetch current stats
926
- const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
927
- headers: { Authorization: `Bearer ${apiKey}` },
928
- signal: AbortSignal.timeout(8000),
929
- });
930
- if (!statsRes.ok)
931
- throw new Error(`stats HTTP ${statsRes.status}`);
932
- const stats = await statsRes.json();
933
- const lessons = stats.lesson_count ?? 0;
934
- const recalls = stats.total_recall_count ?? 0;
935
- const score = Math.round((stats.quality_score ?? 0) * 100);
936
- const topLessons = (stats.top_lessons ?? []).slice(0, 5);
937
- const teamAuthors = stats.team_authors ?? [];
938
- const tokensSaved = recalls * 1200;
939
- const costSaved = (tokensSaved * 0.000003).toFixed(2);
940
- const level = lessons < 10 ? 'Junior Dev 🔧' : lessons < 30 ? 'Mid Dev ⚡' :
941
- lessons < 60 ? 'Senior Dev 🧠' : lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
942
- const now = new Date();
943
- const weekStart = new Date(now);
944
- weekStart.setDate(now.getDate() - 7);
945
- const fmt = (d) => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
946
- console.log('┌─────────────────────────────────────────────────────────────┐');
947
- console.log(`│ \x1b[1m🧠 Brain Weekly Digest\x1b[0m \x1b[90m${fmt(weekStart)} – ${fmt(now)}\x1b[0m │`);
948
- console.log('├─────────────────────────────────────────────────────────────┤');
949
- console.log(`│ Total lessons : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level : \x1b[32m${level.padEnd(18)}\x1b[0m│`);
950
- console.log(`│ Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m │`);
951
- 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│`);
952
- if (teamAuthors.length > 0) {
953
- console.log('├─────────────────────────────────────────────────────────────┤');
954
- console.log(`│ \x1b[36m👥 Team contributors:\x1b[0m ${teamAuthors.slice(0, 5).join(', ').slice(0, 42).padEnd(42)} │`);
955
- }
956
- if (topLessons.length > 0) {
957
- console.log('├─────────────────────────────────────────────────────────────┤');
958
- console.log('│ \x1b[33m🔥 Most recalled lessons this week:\x1b[0m │');
959
- for (const l of topLessons) {
960
- const line = `${l.topic}: ${l.what_worked}`.slice(0, 57);
961
- console.log(`│ \x1b[90m• ${line.padEnd(58)}\x1b[0m│`);
962
- }
963
- }
964
- console.log('├─────────────────────────────────────────────────────────────┤');
965
- console.log('│ \x1b[32m📋 Share this digest:\x1b[0m │');
966
- console.log('│ \x1b[90m npx @cachly-dev/mcp-server@latest share\x1b[0m │');
967
- console.log('│ \x1b[90m Add more: npx @cachly-dev/mcp-server@latest invite\x1b[0m │');
968
- console.log('└─────────────────────────────────────────────────────────────┘');
969
- console.log('');
970
- // Pro tip for cron
971
- console.log(' \x1b[2m💡 Automate: add to crontab for a weekly team email\x1b[0m');
972
- console.log(' \x1b[2m 0 9 * * 1 npx @cachly-dev/mcp-server@latest digest\x1b[0m');
973
- console.log('');
974
- }
975
- catch (e) {
976
- console.log(`\n❌ Could not fetch digest: ${e.message}\n`);
977
- process.exit(1);
978
- }
979
- process.exit(0);
980
- }
981
- // ── CLI: cachly invite ────────────────────────────────────────────────────────
982
- // Usage: npx @cachly-dev/mcp-server@latest invite [email]
983
- // Invites a teammate to share your Brain. Fastest referral loop.
984
- if (process.argv[2] === 'invite') {
985
- const { createInterface } = await import('node:readline');
986
- const apiKey = process.env.CACHLY_JWT ?? '';
987
- if (!apiKey) {
988
- console.log('\n⚠️ CACHLY_JWT must be set.');
989
- console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
990
- process.exit(1);
991
- }
992
- let email = process.argv[3] ?? '';
993
- if (!email) {
994
- const rl = createInterface({ input: process.stdin, output: process.stdout });
995
- email = await new Promise((resolve) => {
996
- rl.question('\n 📬 Teammate email to invite: ', (ans) => { rl.close(); resolve(ans.trim()); });
997
- });
998
- }
999
- if (!email || !email.includes('@')) {
1000
- console.log('\n❌ Invalid email address.\n');
1001
- process.exit(1);
1002
- }
1003
- console.log(`\n ⏳ Inviting ${email}...`);
1004
- try {
1005
- const res = await fetch(`${API_URL}/api/v1/team/invite`, {
1006
- method: 'POST',
1007
- headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
1008
- body: JSON.stringify({ email, role: 'member', source: 'cli-invite' }),
1009
- signal: AbortSignal.timeout(8000),
1010
- });
1011
- if (res.ok) {
1012
- console.log(`\n ✅ Invite sent to \x1b[32m${email}\x1b[0m`);
1013
- console.log(` They'll get a link to join your Brain — one click, 30 seconds.\n`);
1014
- console.log(` 💡 Once they join, your AI assistants share lessons automatically.\n`);
1015
- }
1016
- else if (res.status === 409) {
1017
- console.log(`\n ✓ ${email} is already a team member.\n`);
1018
- }
1019
- else {
1020
- const body = await res.json().catch(() => ({}));
1021
- console.log(`\n ❌ Invite failed: ${body.error ?? `HTTP ${res.status}`}\n`);
1022
- process.exit(1);
1023
- }
1024
- }
1025
- catch (e) {
1026
- console.log(`\n ❌ Network error: ${e.message}\n`);
1027
- process.exit(1);
1028
- }
1029
- process.exit(0);
1030
- }
1031
- // ── CLI: cachly join <token> ──────────────────────────────────────────────────
1032
- // Usage: npx @cachly-dev/mcp-server@latest join <token>
1033
- // Accepts a Brain invite link — writes the shared instance ID into all
1034
- // detected editor MCP configs and the global ~/.claude/mcp.json.
1035
- // No account needed if CACHLY_JWT is already set; otherwise prompts for auth.
1036
- if (process.argv[2] === 'join') {
1037
- const { writeFile: wfJ, readFile: rfJ, mkdir: mkJ } = await import('node:fs/promises');
1038
- const { existsSync: exJ } = await import('node:fs');
1039
- const { resolve: resJ, dirname: dirJ } = await import('node:path');
1040
- const { homedir } = await import('node:os');
1041
- const { createInterface: ciJ } = await import('node:readline');
1042
- const token = process.argv[3] ?? '';
1043
- if (!token) {
1044
- console.log('\n❌ Usage: npx @cachly-dev/mcp-server@latest join <token>\n');
1045
- console.log(' Get a token from a teammate: npx @cachly-dev/mcp-server@latest invite\n');
1046
- process.exit(1);
1047
- }
1048
- console.log('\n⏳ Looking up invite...');
1049
- // ── Step 1: Fetch invite info (public, no auth) ───────────────────────────
1050
- let inviteInfo;
1051
- try {
1052
- const res = await fetch(`${API_URL}/api/invite/${token}`, { signal: AbortSignal.timeout(8000) });
1053
- if (res.status === 404) {
1054
- console.log('\n❌ Invite not found — the link may be invalid.\n');
1055
- process.exit(1);
1056
- }
1057
- if (res.status === 410) {
1058
- console.log('\n❌ This invite link has expired (7-day limit).\n');
1059
- process.exit(1);
1060
- }
1061
- if (!res.ok) {
1062
- console.log(`\n❌ Could not fetch invite: HTTP ${res.status}\n`);
1063
- process.exit(1);
1064
- }
1065
- inviteInfo = await res.json();
1066
- }
1067
- catch (e) {
1068
- console.log(`\n❌ Network error: ${e.message}\n`);
1069
- process.exit(1);
1070
- }
1071
- const { instance_id, instance_name, tier, label } = inviteInfo;
1072
- const expires = new Date(inviteInfo.expires_at).toLocaleDateString();
1073
- console.log('');
1074
- console.log(' \x1b[35m╔══════════════════════════════════════════════════════╗\x1b[0m');
1075
- console.log(' \x1b[35m║\x1b[0m \x1b[1m🧠 Brain Invite\x1b[0m \x1b[35m║\x1b[0m');
1076
- console.log(' \x1b[35m╚══════════════════════════════════════════════════════╝\x1b[0m');
1077
- console.log('');
1078
- console.log(` Brain : \x1b[1m${instance_name}\x1b[0m`);
1079
- console.log(` Tier : ${tier}`);
1080
- if (label)
1081
- console.log(` Note : ${label}`);
1082
- console.log(` Expires : ${expires}`);
1083
- console.log('');
1084
- // ── Step 2: Confirm ───────────────────────────────────────────────────────
1085
- const rlJ = ciJ({ input: process.stdin, output: process.stdout });
1086
- const confirmJ = await new Promise((resolve) => {
1087
- rlJ.question(' Join this Brain? [Y/n] ', (a) => { rlJ.close(); resolve(a.trim().toLowerCase() || 'y'); });
1088
- });
1089
- if (confirmJ !== 'y' && confirmJ !== 'yes') {
1090
- console.log('\n Cancelled.\n');
1091
- process.exit(0);
1092
- }
1093
- // ── Step 3: Ensure JWT ────────────────────────────────────────────────────
1094
- let joinJwt = JWT;
1095
- if (!joinJwt) {
1096
- console.log('\n No API key found — starting sign-in (10 seconds)...\n');
1097
- try {
1098
- const dcRes = await fetch(`${API_URL}/api/v1/auth/device/code`, {
1099
- method: 'POST', headers: { 'Content-Type': 'application/json' },
1100
- body: JSON.stringify({ client_id: 'cachly-mcp-cli' }),
1101
- signal: AbortSignal.timeout(10000),
1102
- });
1103
- if (dcRes.ok) {
1104
- const dc = await dcRes.json();
1105
- console.log(` 👉 Open: \x1b[36m${dc.verification_uri}\x1b[0m`);
1106
- console.log(` Code: \x1b[1m${dc.user_code}\x1b[0m\n`);
1107
- // Poll for token
1108
- const pollInterval = (dc.interval ?? 5) * 1000;
1109
- for (let i = 0; i < 24; i++) {
1110
- await new Promise(r => setTimeout(r, pollInterval));
1111
- try {
1112
- const tokRes = await fetch(`${API_URL}/api/v1/auth/device/token`, {
1113
- method: 'POST', headers: { 'Content-Type': 'application/json' },
1114
- body: JSON.stringify({ device_code: dc.device_code, client_id: 'cachly-mcp-cli' }),
1115
- signal: AbortSignal.timeout(8000),
1116
- });
1117
- if (tokRes.ok) {
1118
- const tok = await tokRes.json();
1119
- if (tok.access_token) {
1120
- joinJwt = tok.access_token;
1121
- break;
1122
- }
1123
- }
1124
- }
1125
- catch { /* polling — keep going */ }
1126
- }
1127
- if (!joinJwt) {
1128
- console.log('\n❌ Sign-in timed out. Run the command again.\n');
1129
- process.exit(1);
1130
- }
1131
- console.log(' ✅ Signed in!\n');
1132
- }
1133
- }
1134
- catch (e) {
1135
- console.log(`\n ⚠️ Could not start sign-in: ${e.message}`);
1136
- console.log(` Set CACHLY_JWT manually from https://cachly.dev/setup-ai\n`);
1137
- process.exit(1);
1138
- }
1139
- }
1140
- // ── Step 4: Write instance ID into all editor MCP configs ────────────────
1141
- const cwd = process.cwd();
1142
- const home = homedir();
1143
- const editorFiles = [
1144
- { label: '.mcp.json (Claude Code project)', path: resJ(cwd, '.mcp.json') },
1145
- { label: '.cursor/mcp.json', path: resJ(cwd, '.cursor', 'mcp.json') },
1146
- { label: '.windsurf/mcp.json', path: resJ(cwd, '.windsurf', 'mcp.json') },
1147
- { label: '.vscode/mcp.json (Copilot/Cline)', path: resJ(cwd, '.vscode', 'mcp.json') },
1148
- { label: '~/.claude/mcp.json (global)', path: resJ(home, '.claude', 'mcp.json'), global: true },
1149
- ];
1150
- let written = 0;
1151
- for (const { label: eLabel, path: ePath, global: isGlobal } of editorFiles) {
1152
- // Only write project configs if the directory exists (or global)
1153
- const dir = dirJ(ePath);
1154
- if (!isGlobal && !exJ(dir) && !exJ(ePath))
1155
- continue;
1156
- try {
1157
- await mkJ(dir, { recursive: true });
1158
- let cfg = {};
1159
- if (exJ(ePath)) {
1160
- try {
1161
- cfg = JSON.parse(await rfJ(ePath, 'utf-8'));
1162
- }
1163
- catch {
1164
- try {
1165
- await (await import('node:fs/promises')).rename(ePath, ePath + '.bak');
1166
- }
1167
- catch { /* backup failed */ }
1168
- }
1169
- }
1170
- // Update or create the cachly entry with the shared instance ID
1171
- const servers = (cfg['mcpServers'] ?? {});
1172
- const existing = (servers['cachly'] ?? {});
1173
- const env = (existing['env'] ?? {});
1174
- env['CACHLY_BRAIN_INSTANCE_ID'] = instance_id;
1175
- if (joinJwt && !env['CACHLY_JWT'])
1176
- env['CACHLY_JWT'] = joinJwt;
1177
- env['CACHLY_API_URL'] ??= 'https://api.cachly.dev';
1178
- servers['cachly'] = { ...existing, command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env };
1179
- cfg['mcpServers'] = servers;
1180
- await wfJ(ePath, JSON.stringify(cfg, null, 2), 'utf-8');
1181
- console.log(` ✅ ${eLabel}`);
1182
- written++;
1183
- }
1184
- catch (joinWriteErr) {
1185
- // Only surface unexpected errors — missing editor dirs are expected and silently skipped above
1186
- if (joinWriteErr.code !== 'ENOENT') {
1187
- console.log(` ⚠️ ${eLabel}: ${joinWriteErr.message}`);
1188
- }
1189
- }
1190
- }
1191
- console.log('');
1192
- if (written === 0) {
1193
- console.log(' ⚠️ No editor configs found — run `cachly setup` first.\n');
1194
- }
1195
- else {
1196
- console.log(` 🧠 You're now sharing Brain: \x1b[1m${instance_name}\x1b[0m`);
1197
- console.log(' Restart your editor — your AI arrives pre-briefed from the team Brain.\n');
1198
- console.log(' 📛 Add the badge to your README:');
1199
- console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
1200
- }
1201
- process.exit(0);
1202
- }
1203
- // ── CLI: cachly upgrade ───────────────────────────────────────────────────────
1204
- // Usage: npx @cachly-dev/mcp-server@latest upgrade
1205
- // Checks npm for the latest version and prints upgrade instructions if outdated.
1206
- if (process.argv[2] === 'upgrade') {
1207
- process.stdout.write('\n⏳ Checking for updates...\n');
1208
- try {
1209
- const res = await fetch('https://registry.npmjs.org/@cachly-dev/mcp-server/latest', {
1210
- signal: AbortSignal.timeout(8000),
1211
- });
1212
- if (!res.ok)
1213
- throw new Error(`registry HTTP ${res.status}`);
1214
- const pkg = await res.json();
1215
- const latest = pkg.version;
1216
- if (!latest || !/^\d+\.\d+\.\d+/.test(latest))
1217
- throw new Error('npm registry returned unexpected response');
1218
- if (latest === CURRENT_VERSION) {
1219
- console.log(`\n✅ Already on the latest version: \x1b[32mv${CURRENT_VERSION}\x1b[0m\n`);
1220
- }
1221
- else {
1222
- console.log('');
1223
- console.log('\x1b[35m ╔══════════════════════════════════════════════════════╗\x1b[0m');
1224
- console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly update available\x1b[0m \x1b[35m║\x1b[0m');
1225
- console.log('\x1b[35m ╚══════════════════════════════════════════════════════╝\x1b[0m');
1226
- console.log('');
1227
- console.log(` Current : \x1b[31mv${CURRENT_VERSION}\x1b[0m`);
1228
- console.log(` Latest : \x1b[32mv${latest}\x1b[0m`);
1229
- console.log('');
1230
- console.log(' Update your editor configs to pick up the new version:');
1231
- console.log('');
1232
- console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m');
1233
- console.log('');
1234
- console.log(' \x1b[2m(npx always fetches the latest when @latest is specified)\x1b[0m');
1235
- console.log('');
1236
- }
1237
- }
1238
- catch (e) {
1239
- console.log(`\n❌ Could not check for updates: ${e.message}\n`);
1240
- process.exit(1);
1241
- }
1242
- process.exit(0);
1243
- }
1244
- // ── CLI: cachly demo ──────────────────────────────────────────────────────────
1245
- // Usage: npx @cachly-dev/mcp-server@latest demo
1246
- // Zero-signup. Reads local git history and shows what the AI Brain would know.
1247
- // The fastest path from "what is this?" to "I need this".
1248
- if (process.argv[2] === 'demo') {
1249
- const { execSync } = await import('node:child_process');
1250
- const { existsSync } = await import('node:fs');
1251
- const { resolve } = await import('node:path');
1252
- const cwd = process.cwd();
1253
- console.log('\n\x1b[35m🧠 cachly Brain — Live Demo\x1b[0m');
1254
- console.log('\x1b[90mNo account needed. Reading your git history...\x1b[0m\n');
1255
- // Verify git repo
1256
- if (!existsSync(resolve(cwd, '.git'))) {
1257
- console.log('⚠️ No git repository found here.');
1258
- console.log(' Run this command inside a project directory with git history.\n');
1259
- console.log(' Example: cd ~/my-project && npx @cachly-dev/mcp-server@latest demo\n');
1260
- process.exit(0);
1261
- }
1262
- // Read git log
1263
- let logOutput = '';
1264
- try {
1265
- logOutput = execSync('git log HEAD --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n 200', { cwd, encoding: 'utf-8', stdio: 'pipe' });
1266
- }
1267
- catch {
1268
- console.log('⚠️ Could not read git log. Make sure you have commits.\n');
1269
- process.exit(0);
1270
- }
1271
- const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
1272
- const [sha, subject, date, author] = line.split('|||');
1273
- return { sha: (sha ?? '').trim().slice(0, 8), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
1274
- });
1275
- if (commits.length === 0) {
1276
- console.log('⚠️ No commits found.\n');
1277
- process.exit(0);
1278
- }
1279
- // Classify commits (same logic as brain_from_git)
1280
- const classify = (s) => {
1281
- const t = s.toLowerCase();
1282
- if (/\b(fix|fixed|bug|hotfix|patch|revert|resolve)\b/.test(t))
1283
- return 'fix';
1284
- if (/\b(feat|feature|add|implement|new|introduce)\b/.test(t))
1285
- return 'feat';
1286
- if (/\b(perf|optim|speed|cache|latency)\b/.test(t))
1287
- return 'perf';
1288
- if (/\b(security|cve|auth|csrf|xss|inject|sanitize)\b/.test(t))
1289
- return 'security';
1290
- if (/\b(deploy|ci|cd|docker|k8s|infra)\b/.test(t))
1291
- return 'deploy';
1292
- if (/\b(refactor|clean|simplify|extract)\b/.test(t))
1293
- return 'refactor';
1294
- return 'chore';
1295
- };
1296
- // Count categories
1297
- const cats = new Map();
1298
- const fixes = [];
1299
- const feats = [];
1300
- const security = [];
1301
- const authors = new Set();
1302
- for (const c of commits) {
1303
- const cat = classify(c.subject);
1304
- cats.set(cat, (cats.get(cat) ?? 0) + 1);
1305
- if (cat === 'fix' && fixes.length < 5)
1306
- fixes.push(c.subject.slice(0, 72));
1307
- if (cat === 'feat' && feats.length < 5)
1308
- feats.push(c.subject.slice(0, 72));
1309
- if (cat === 'security' && security.length < 3)
1310
- security.push(c.subject.slice(0, 72));
1311
- if (c.author)
1312
- authors.add(c.author.split(' ')[0]);
1313
- }
1314
- const totalLessons = commits.length - (cats.get('chore') ?? 0);
1315
- const dateRange = `${commits[commits.length - 1]?.date ?? '?'} → ${commits[0]?.date ?? '?'}`;
1316
- // Display the demo
1317
- process.stdout.write('\x1b[2K\r');
1318
- console.log('┌─────────────────────────────────────────────────────────────┐');
1319
- console.log('│ \x1b[1m🧠 Brain Preview — What your AI would know\x1b[0m │');
1320
- console.log('├─────────────────────────────────────────────────────────────┤');
1321
- 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 │`);
1322
- console.log(`│ Lessons extracted: \x1b[32m${String(totalLessons).padEnd(6)}\x1b[0m Contributors: \x1b[36m${String(authors.size).padEnd(20)}\x1b[0m│`);
1323
- console.log('├─────────────────────────────────────────────────────────────┤');
1324
- // Category breakdown
1325
- const sorted = [...cats.entries()].filter(([k]) => k !== 'chore').sort((a, b) => b[1] - a[1]);
1326
- const bar = (n, max) => '█'.repeat(Math.round((n / max) * 20)).padEnd(20);
1327
- const maxVal = Math.max(...sorted.map(([, v]) => v), 1);
1328
- console.log('│ \x1b[90mCategory breakdown:\x1b[0m │');
1329
- for (const [cat, count] of sorted) {
1330
- const emoji = { fix: '🔧', feat: '✨', perf: '⚡', security: '🔒', deploy: '🚀', refactor: '🔄' }[cat] ?? '•';
1331
- 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 │`);
1332
- }
1333
- if (security.length > 0) {
1334
- console.log('├─────────────────────────────────────────────────────────────┤');
1335
- console.log('│ \x1b[31m🔒 Security fixes your AI would know:\x1b[0m │');
1336
- for (const s of security)
1337
- console.log(`│ \x1b[90m• ${s.slice(0, 58).padEnd(58)}\x1b[0m │`);
1338
- }
1339
- if (fixes.length > 0) {
1340
- console.log('├─────────────────────────────────────────────────────────────┤');
1341
- console.log('│ \x1b[33m🔧 Bug fixes your AI would remember:\x1b[0m │');
1342
- for (const f of fixes.slice(0, 4))
1343
- console.log(`│ \x1b[90m• ${f.slice(0, 58).padEnd(58)}\x1b[0m │`);
1344
- }
1345
- if (feats.length > 0) {
1346
- console.log('├─────────────────────────────────────────────────────────────┤');
1347
- console.log('│ \x1b[32m✨ Features & decisions it would recall:\x1b[0m │');
1348
- for (const f of feats.slice(0, 3))
1349
- console.log(`│ \x1b[90m• ${f.slice(0, 58).padEnd(58)}\x1b[0m │`);
1350
- }
1351
- console.log('├─────────────────────────────────────────────────────────────┤');
1352
- console.log('│ \x1b[32m💡 With cachly, your AI arrives pre-briefed every session.\x1b[0m │');
1353
- console.log('│ \x1b[32m No more re-explaining. No more repeated mistakes.\x1b[0m │');
1354
- console.log('└─────────────────────────────────────────────────────────────┘');
1355
- console.log('');
1356
- console.log(' \x1b[1mMake this permanent (free, 30 seconds):\x1b[0m');
1357
- console.log(' \x1b[32m$ npx @cachly-dev/mcp-server@latest setup\x1b[0m');
1358
- console.log('');
1359
- console.log(' Works with: Claude Code · Cursor · Windsurf · Copilot · Cline · Zed');
1360
- console.log(' Free forever · GDPR · German servers · No credit card');
1361
- console.log('');
1362
- process.exit(0);
1363
- }
1364
- // ── CLI: cachly share ─────────────────────────────────────────────────────────
1365
- // Usage: npx @cachly-dev/mcp-server@latest share
1366
- // Generates a shareable ASCII card + tweet text with your Brain stats.
1367
- if (process.argv[2] === 'share') {
1368
- const apiKey = process.env.CACHLY_JWT ?? '';
1369
- const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
1370
- if (!apiKey || !instanceId) {
1371
- console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
1372
- console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 30 seconds)\n');
1373
- process.exit(1);
1374
- }
1375
- process.stdout.write('\n🧠 Fetching your Brain stats...\n');
1376
- try {
1377
- const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
1378
- headers: { Authorization: `Bearer ${apiKey}` },
1379
- signal: AbortSignal.timeout(8000),
1380
- });
1381
- if (!statsRes.ok)
1382
- throw new Error(`HTTP ${statsRes.status}`);
1383
- const stats = await statsRes.json();
1384
- const lessons = stats.lesson_count ?? 0;
1385
- const recalls = stats.total_recall_count ?? 0;
1386
- const score = Math.round((stats.quality_score ?? 0) * 100);
1387
- const tokensSaved = recalls * 1200;
1388
- const costSaved = (tokensSaved * 0.000003).toFixed(2);
1389
- const level = lessons === 0 ? 'Intern 🌱' :
1390
- lessons < 10 ? 'Junior Dev 🔧' :
1391
- lessons < 30 ? 'Mid Dev ⚡' :
1392
- lessons < 60 ? 'Senior Dev 🧠' :
1393
- lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
1394
- console.log('');
1395
- console.log('┌─────────────────────────────────────────────────────────────┐');
1396
- console.log('│ 🧠 My AI Brain · powered by \x1b[35mcachly.dev\x1b[0m │');
1397
- console.log('├─────────────────────────────────────────────────────────────┤');
1398
- console.log(`│ Lessons stored : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level: \x1b[32m${level.padEnd(18)}\x1b[0m│`);
1399
- console.log(`│ Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m │`);
1400
- 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│`);
1401
- console.log('├─────────────────────────────────────────────────────────────┤');
1402
- console.log('│ Works with: Claude Code · Cursor · Windsurf · Copilot │');
1403
- console.log('│ Free · GDPR · German servers │');
1404
- console.log('│ \x1b[35mnpx @cachly-dev/mcp-server@latest setup\x1b[0m │');
1405
- console.log('└─────────────────────────────────────────────────────────────┘');
1406
- console.log('');
1407
- // Tweet text
1408
- const shareUrl = 'https://cachly.dev?ref=share&utm_source=x&utm_medium=social&utm_campaign=cli-share';
1409
- 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`;
1410
- console.log(' \x1b[1m📋 Share on Twitter/X (copy this):\x1b[0m');
1411
- console.log(' ─────────────────────────────────────');
1412
- console.log(tweet.split('\n').map(l => ` ${l}`).join('\n'));
1413
- console.log(' ─────────────────────────────────────');
1414
- console.log('');
1415
- console.log(' \x1b[2mPro tip: add a screenshot of your Brain panel for 3x more engagement\x1b[0m');
1416
- console.log(' \x1b[2mAlso add a live badge to your README:\x1b[0m');
1417
- console.log(` \x1b[90m npx @cachly-dev/mcp-server@latest badge\x1b[0m`);
1418
- console.log('');
1419
- }
1420
- catch (e) {
1421
- console.log(`\n❌ Could not fetch stats: ${e.message}\n`);
1422
- process.exit(1);
1423
- }
1424
- process.exit(0);
1425
- }
1426
- // ── CLI: cachly badge ─────────────────────────────────────────────────────────
1427
- // Outputs the Markdown + HTML snippet for embedding a live Brain lesson-count
1428
- // badge in any README or website. Badge SVG served by cachly API (public, no auth).
1429
- if (process.argv[2] === 'badge') {
1430
- const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
1431
- if (!instanceId) {
1432
- console.log('\n⚠️ CACHLY_BRAIN_INSTANCE_ID must be set.');
1433
- console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 30 seconds)\n');
1434
- process.exit(1);
1435
- }
1436
- const badgeUrl = `${API_URL}/api/v1/badge/${instanceId}`;
1437
- const targetUrl = 'https://cachly.dev';
1438
- const markdown = `[![cachly Brain](${badgeUrl})](${targetUrl})`;
1439
- const htmlBadge = `<a href="${targetUrl}"><img src="${badgeUrl}" alt="cachly Brain" /></a>`;
1440
- console.log('');
1441
- console.log('\x1b[35m ╔═══════════════════════════════════════════════════════╗\x1b[0m');
1442
- console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly Brain Badge\x1b[0m \x1b[35m║\x1b[0m');
1443
- console.log('\x1b[35m ╚═══════════════════════════════════════════════════════╝\x1b[0m');
1444
- console.log('');
1445
- console.log(' Paste into your README.md:');
1446
- console.log('');
1447
- console.log(` \x1b[32m${markdown}\x1b[0m`);
1448
- console.log('');
1449
- console.log(' ─────────────────────────────────────────────────────');
1450
- console.log(' Or paste into HTML:');
1451
- console.log('');
1452
- console.log(` \x1b[36m${htmlBadge}\x1b[0m`);
1453
- console.log('');
1454
- console.log(' ─────────────────────────────────────────────────────');
1455
- console.log(' Badge URL (live, updates every hour):');
1456
- console.log(` \x1b[90m${badgeUrl}\x1b[0m`);
1457
- console.log('');
1458
- console.log(' \x1b[2mThe badge shows your current lesson count — no auth required,');
1459
- console.log(' safe to embed in public repos. Updates hourly.\x1b[0m');
1460
- console.log('');
1461
- process.exit(0);
1462
- }
1463
- // ── CLI: no-args splash screen ────────────────────────────────────────────────
1464
- // When run with no recognized subcommand, show a compelling pitch + help.
1465
- if (!process.argv[2] && process.stdout.isTTY) {
1466
- console.log('');
1467
- console.log('\x1b[35m ╔═══════════════════════════════════════════════════════╗\x1b[0m');
1468
- console.log('\x1b[35m ║\x1b[0m \x1b[1m🧠 cachly — Persistent AI Memory for Developers\x1b[0m \x1b[35m║\x1b[0m');
1469
- console.log('\x1b[35m ╚═══════════════════════════════════════════════════════╝\x1b[0m');
1470
- console.log('');
1471
- console.log(' Your AI forgets everything when a session ends.');
1472
- console.log(' cachly gives it a brain that survives forever.');
1473
- console.log('');
1474
- console.log(' \x1b[1mCommands:\x1b[0m');
1475
- console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest demo\x1b[0m ← Start here (no account)');
1476
- console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m ← Wire up your AI editors');
1477
- console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest health\x1b[0m ← Check everything works');
1478
- console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest digest\x1b[0m ← Weekly Brain summary');
1479
- console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest share\x1b[0m ← Share your Brain stats');
1480
- console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest badge\x1b[0m ← README badge for your Brain');
1481
- console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest invite\x1b[0m ← Invite a teammate');
1482
- console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest join <token>\x1b[0m ← Accept a Brain invite');
1483
- console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
1484
- console.log('');
1485
- console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
1486
- console.log(' \x1b[90mFree forever · GDPR · German servers · 89 MCP tools\x1b[0m');
1487
- console.log('');
1488
- process.exit(0);
1489
- }
1490
852
  // ── CLI: cachly init ──────────────────────────────────────────────────────────
1491
853
  // Usage: npx @cachly-dev/mcp-server init --instance-id <id> --api-key <key> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]
1492
854
  if (process.argv[2] === 'init') {
@@ -1507,10 +869,8 @@ if (process.argv[2] === 'init') {
1507
869
  const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
1508
870
  const configPath = resolve(projectDir, configFile);
1509
871
  await mkdir(dirname(configPath), { recursive: true });
1510
- const { existsSync: exInit } = await import('node:fs');
1511
- const merged = await mergeMcpConfig(configPath, apiKey, instanceId, editor, { readFile, existsSync: exInit });
1512
- await writeFile(configPath, merged, 'utf-8');
1513
- console.log(`\n✅ ${exInit(configPath) ? 'Updated' : 'Written'}: ${configFile}`);
872
+ await writeFile(configPath, buildMcpConfig(apiKey, instanceId, editor), 'utf-8');
873
+ console.log(`\n✅ Written: ${configFile}`);
1514
874
  // Always write CLAUDE.md (idempotent — safe to run multiple times)
1515
875
  const result = await writeClaudeMd(projectDir, instanceId);
1516
876
  const action = result === 'updated' ? '✅ Updated' : result === 'appended' ? '✅ Appended to' : '✅ Written';
@@ -1557,208 +917,7 @@ if (process.argv[2] === 'init') {
1557
917
  }
1558
918
  catch { /* non-critical — git hook is a best-effort feature */ }
1559
919
  console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}!`);
1560
- console.log(` Restart your editor — the \`cachly\` MCP tools will appear.`);
1561
- console.log(`\n 📛 Add a live badge to your README:`);
1562
- console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
1563
- process.exit(0);
1564
- }
1565
- // ── CLI: cachly health ────────────────────────────────────────────────────────
1566
- // Usage: npx @cachly-dev/mcp-server@latest health
1567
- // Checks: JWT valid, Brain API reachable, editor configs found, git hook present.
1568
- if (process.argv[2] === 'health') {
1569
- const { existsSync } = await import('node:fs');
1570
- const { readFile } = await import('node:fs/promises');
1571
- const { resolve, join: pJoin } = await import('node:path');
1572
- const cwd = process.cwd();
1573
- let passed = 0;
1574
- let failed = 0;
1575
- const ok = (msg) => { console.log(` ✅ ${msg}`); passed++; };
1576
- const warn = (msg) => { console.log(` ⚠️ ${msg}`); };
1577
- const fail = (msg) => { console.log(` ❌ ${msg}`); failed++; };
1578
- console.log('\n🧠 cachly health check\n');
1579
- // ── 1. JWT token ────────────────────────────────────────────────────────────
1580
- console.log('🔑 Auth token');
1581
- const jwt = process.env.CACHLY_JWT ?? '';
1582
- if (!jwt) {
1583
- fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
1584
- }
1585
- else {
1586
- try {
1587
- const parts = jwt.split('.');
1588
- if (parts.length !== 3)
1589
- throw new Error('not a JWT');
1590
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
1591
- const expMs = payload.exp ? payload.exp * 1000 : null;
1592
- const minsLeft = expMs ? Math.floor((expMs - Date.now()) / 60_000) : null;
1593
- if (expMs && expMs < Date.now()) {
1594
- fail(`JWT expired ${Math.abs(minsLeft)} minute(s) ago — get a new one: https://cachly.dev/setup-ai`);
1595
- }
1596
- else if (minsLeft !== null && minsLeft < 30) {
1597
- warn(`JWT expires in ${minsLeft} minute(s) — refresh soon at https://cachly.dev/setup-ai`);
1598
- }
1599
- else {
1600
- ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
1601
- }
1602
- }
1603
- catch {
1604
- fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
1605
- }
1606
- }
1607
- // ── 2. Brain API reachability ───────────────────────────────────────────────
1608
- console.log('\n🌐 Brain API');
1609
- const apiUrl = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
1610
- try {
1611
- const res = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(5000) });
1612
- if (res.ok) {
1613
- const body = await res.json();
1614
- ok(`${apiUrl} reachable (status: ${body.status ?? 'ok'})`);
1615
- }
1616
- else if (res.status === 401 || res.status === 403) {
1617
- ok(`${apiUrl} reachable (auth required — expected)`);
1618
- }
1619
- else {
1620
- fail(`${apiUrl}/health returned HTTP ${res.status}`);
1621
- }
1622
- }
1623
- catch (e) {
1624
- fail(`${apiUrl} unreachable — ${e.message}`);
1625
- }
1626
- // ── 3. Brain instance ───────────────────────────────────────────────────────
1627
- console.log('\n🧠 Brain instance');
1628
- const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
1629
- if (!instanceId) {
1630
- warn('CACHLY_BRAIN_INSTANCE_ID not set — will auto-resolve on first tool call');
1631
- }
1632
- else if (jwt) {
1633
- try {
1634
- const instRes = await fetch(`${apiUrl}/api/v1/instances/${instanceId}`, {
1635
- headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' },
1636
- signal: AbortSignal.timeout(5000),
1637
- });
1638
- if (instRes.ok) {
1639
- const inst = await instRes.json();
1640
- if (inst.status === 'running') {
1641
- ok(`Instance "${inst.name}" (${instanceId.slice(0, 8)}…) is running`);
1642
- // Attempt actual Redis PING to verify connectivity end-to-end
1643
- console.log('\n🔌 Redis connectivity');
1644
- if (inst.host && inst.port) {
1645
- try {
1646
- let pingPassword;
1647
- let pingTls = inst.tls_enabled !== false;
1648
- try {
1649
- const connRes = await fetch(`${apiUrl}/api/v1/instances/${instanceId}/connection`, {
1650
- headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' },
1651
- signal: AbortSignal.timeout(5000),
1652
- });
1653
- if (connRes.ok) {
1654
- const conn = await connRes.json();
1655
- pingPassword = conn.password;
1656
- pingTls = conn.tls_enabled !== false;
1657
- }
1658
- }
1659
- catch { /* proceed without password */ }
1660
- const pingClient = new Redis({
1661
- host: inst.host, port: inst.port,
1662
- password: pingPassword,
1663
- ...(pingTls ? { tls: {} } : {}),
1664
- lazyConnect: true, connectTimeout: 5000, retryStrategy: () => null,
1665
- });
1666
- await pingClient.connect();
1667
- const pong = await pingClient.ping();
1668
- pingClient.disconnect();
1669
- if (pong === 'PONG') {
1670
- ok(`Redis PING → PONG (${inst.host}:${inst.port})`);
1671
- }
1672
- else {
1673
- warn(`Redis responded but not PONG: ${pong}`);
1674
- }
1675
- }
1676
- catch (pingErr) {
1677
- fail(`Redis unreachable — ${pingErr.message}`);
1678
- }
1679
- }
1680
- else {
1681
- warn('Instance has no host/port yet');
1682
- }
1683
- }
1684
- else {
1685
- warn(`Instance "${inst.name}" status: ${inst.status}`);
1686
- }
1687
- }
1688
- else {
1689
- fail(`Instance ${instanceId.slice(0, 8)}… not found (HTTP ${instRes.status})`);
1690
- }
1691
- }
1692
- catch (e) {
1693
- fail(`Could not check instance — ${e.message}`);
1694
- }
1695
- }
1696
- // ── 4. Editor configs ───────────────────────────────────────────────────────
1697
- console.log('\n🛠 Editor configs (project dir)');
1698
- const editorConfigs = [
1699
- { editor: 'Claude Code', path: '.mcp.json' },
1700
- { editor: 'Cursor', path: '.cursor/mcp.json' },
1701
- { editor: 'Windsurf', path: '.windsurf/mcp.json' },
1702
- { editor: 'Copilot / Cline', path: '.vscode/mcp.json' },
1703
- { editor: 'Zed', path: '.zed/settings.json' },
1704
- { editor: 'Continue.dev', path: '.continue/config.json' },
1705
- ];
1706
- let configsFound = 0;
1707
- for (const { editor, path } of editorConfigs) {
1708
- const fullPath = resolve(cwd, path);
1709
- if (existsSync(fullPath)) {
1710
- try {
1711
- const content = await readFile(fullPath, 'utf-8');
1712
- const hasCachly = content.includes('@cachly-dev/mcp-server') || content.includes('cachly');
1713
- if (hasCachly) {
1714
- ok(`${editor}: ${path}`);
1715
- configsFound++;
1716
- }
1717
- else {
1718
- warn(`${editor}: ${path} exists but cachly not found inside`);
1719
- }
1720
- }
1721
- catch {
1722
- warn(`${editor}: ${path} exists but could not read`);
1723
- }
1724
- }
1725
- }
1726
- if (configsFound === 0) {
1727
- fail('No editor MCP configs found — run: npx @cachly-dev/mcp-server@latest setup');
1728
- }
1729
- // ── 5. Git hook ─────────────────────────────────────────────────────────────
1730
- console.log('\n🪝 Git hook (ambient learning)');
1731
- const hookPath = resolve(cwd, '.git', 'hooks', 'post-commit');
1732
- if (!existsSync(resolve(cwd, '.git'))) {
1733
- warn('Not in a git repository — skip');
1734
- }
1735
- else if (existsSync(hookPath)) {
1736
- try {
1737
- const hook = await readFile(hookPath, 'utf-8');
1738
- if (hook.includes('cachly')) {
1739
- ok('.git/hooks/post-commit (cachly CLS hook present)');
1740
- }
1741
- else {
1742
- warn('.git/hooks/post-commit exists but cachly hook not found — run setup to add it');
1743
- }
1744
- }
1745
- catch {
1746
- warn('.git/hooks/post-commit exists but could not read');
1747
- }
1748
- }
1749
- else {
1750
- warn('.git/hooks/post-commit not found — run: npx @cachly-dev/mcp-server@latest setup');
1751
- }
1752
- // ── Summary ─────────────────────────────────────────────────────────────────
1753
- console.log(`\n${'─'.repeat(40)}`);
1754
- if (failed === 0) {
1755
- console.log(`✅ All checks passed (${passed} ok)\n`);
1756
- }
1757
- else {
1758
- console.log(`❌ ${failed} check(s) failed, ${passed} passed\n`);
1759
- console.log(`💡 Fix issues with: npx @cachly-dev/mcp-server@latest setup\n`);
1760
- process.exit(1);
1761
- }
920
+ console.log(` Restart your editor — the \`cachly\` MCP tools will appear.\n`);
1762
921
  process.exit(0);
1763
922
  }
1764
923
  // ── CLI: cachly setup (interactive — no flags required) ───────────────────────
@@ -1768,7 +927,6 @@ if (process.argv[2] === 'setup') {
1768
927
  const { existsSync } = await import('node:fs');
1769
928
  const { resolve, dirname } = await import('node:path');
1770
929
  const { createInterface } = await import('node:readline');
1771
- sendFunnelEvent('setup_started');
1772
930
  // --yes / -y → non-interactive mode (skips all prompts, picks defaults)
1773
931
  const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y');
1774
932
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -2015,23 +1173,12 @@ if (process.argv[2] === 'setup') {
2015
1173
  const editorsToSetup = detected;
2016
1174
  console.log(`Step 3: Configuring for: ${editorsToSetup.map(editorLabel).join(', ')}\n`);
2017
1175
  // ── Step 4: Write editor configs (project-level) ─────────────────────────
2018
- // Uses mergeMcpConfig to preserve all existing MCP servers — only adds/updates
2019
- // the cachly entry without touching filesystem, github, or other servers.
2020
- const { existsSync: exSetup } = await import('node:fs');
2021
1176
  for (const editor of editorsToSetup) {
2022
1177
  const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
2023
1178
  const configPath = resolve(cwd, configFile);
2024
- try {
2025
- await mkdir(dirname(configPath), { recursive: true });
2026
- const wasExisting = exSetup(configPath);
2027
- const merged = await mergeMcpConfig(configPath, token, instance.id, editor, { readFile, existsSync: exSetup });
2028
- await writeFile(configPath, merged, 'utf-8');
2029
- console.log(`✅ ${wasExisting ? 'Updated' : 'Written'}: ${configFile}`);
2030
- }
2031
- catch (writeErr) {
2032
- console.log(`⚠️ Could not write ${configFile}: ${writeErr.message}`);
2033
- console.log(` Fix permissions or run with sudo, then re-run setup.`);
2034
- }
1179
+ await mkdir(dirname(configPath), { recursive: true });
1180
+ await writeFile(configPath, buildMcpConfig(token, instance.id, editor), 'utf-8');
1181
+ console.log(`✅ Written: ${configFile}`);
2035
1182
  }
2036
1183
  // ── Step 4b: Write global Claude Code config (~/.claude/mcp.json) ─────────
2037
1184
  // Merges cachly into the existing global config so it works in every project,
@@ -2154,9 +1301,7 @@ if (process.argv[2] === 'setup') {
2154
1301
  }
2155
1302
  console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
2156
1303
  console.log(` Your AI now has persistent memory across every session.\n`);
2157
- console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
2158
- console.log(`\n 📛 Add a live badge to your README:`);
2159
- console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
1304
+ console.log(` Dashboard: https://cachly.dev/instances/${instance.id}\n`);
2160
1305
  // ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
2161
1306
  if (!nonInteractive) {
2162
1307
  const email = await ask(' 📬 Stay in the loop? Email for release notes [Enter to skip]: ');
@@ -2252,9 +1397,7 @@ if (process.argv[2] === 'index') {
2252
1397
  // ── Start ─────────────────────────────────────────────────────────────────────
2253
1398
  // Warn on stderr when credentials are missing so the user sees a clear
2254
1399
  // actionable message in their editor's MCP log instead of silent failures.
2255
- // Skip for CLI commands that intentionally run without credentials.
2256
- const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
2257
- if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
1400
+ if (!JWT) {
2258
1401
  process.stderr.write('\n' +
2259
1402
  '╔══════════════════════════════════════════════════════════════════╗\n' +
2260
1403
  '║ 🧠 cachly AI Brain — Setup required ║\n' +