@cachly-dev/mcp-server 0.10.71 → 0.10.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
  import { jwtExpiryMs, checkJwt, handleApiError, diagnoseAuth, planAuthHeal } from './auth.js';
3
3
  import { handleTcoTool } from './handlers/tco.js';
4
4
  import { notify } from './notifier.js';
5
- import { readFile } from 'node:fs/promises';
6
5
  import { fileURLToPath } from 'node:url';
7
6
  // True only when this file is the entry point (not imported by tests or other modules).
8
7
  const _isMain = process.argv[1] != null &&
@@ -64,7 +63,7 @@ import { Redis } from 'ioredis';
64
63
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
65
64
  let JWT = process.env.CACHLY_JWT ?? '';
66
65
  const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
67
- const CURRENT_VERSION = '0.10.71';
66
+ const CURRENT_VERSION = '0.10.73';
68
67
  // Max time to wait for a freshly-provisioned instance to become "running" before
69
68
  // giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
70
69
  // old hard-coded 25s caused spurious "instance_not_reachable" on first run.
@@ -666,7 +665,7 @@ async function handleTool(name, args) {
666
665
  '',
667
666
  'After sign-in: call **any tool again** — your Brain activates instantly.',
668
667
  '',
669
- '✨ Free forever · No credit card · 102 MCP tools · GDPR · EU servers',
668
+ '✨ Free forever · No credit card · 107 MCP tools · GDPR · EU servers',
670
669
  ].join('\n');
671
670
  }
672
671
  // Device flow unavailable (network issue) — fall back to manual setup
@@ -1869,24 +1868,45 @@ if (!process.argv[2] && process.stdout.isTTY) {
1869
1868
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
1870
1869
  console.log('');
1871
1870
  console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
1872
- console.log(' \x1b[90mFree forever · GDPR · German servers · 102 MCP tools\x1b[0m');
1871
+ console.log(' \x1b[90mFree forever · GDPR · German servers · 107 MCP tools\x1b[0m');
1873
1872
  console.log('');
1874
1873
  process.exit(0);
1875
1874
  }
1876
1875
  // ── CLI: cachly init ──────────────────────────────────────────────────────────
1877
1876
  // Usage: npx @cachly-dev/mcp-server init --instance-id <id> --api-key <key> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]
1878
1877
  if (process.argv[2] === 'init') {
1879
- const { writeFile, mkdir } = await import('node:fs/promises');
1878
+ const initStart = Date.now();
1879
+ const { writeFile, mkdir, readFile } = await import('node:fs/promises');
1880
1880
  const { resolve, dirname } = await import('node:path');
1881
1881
  const argv = process.argv.slice(3);
1882
1882
  const flag = (name) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
1883
- const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
1884
- const apiKey = flag('api-key') ?? process.env.CACHLY_JWT;
1883
+ let instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
1884
+ let apiKey = flag('api-key') ?? process.env.CACHLY_JWT;
1885
1885
  const editor = (flag('editor') ?? 'claude').toLowerCase();
1886
1886
  const projectDir = resolve(flag('project-dir') ?? '.');
1887
+ // Zero-arg path: fall back to credentials saved by a previous `setup`
1888
+ // (~/.claude/mcp.json). Makes `init` a fast, idempotent re-configuration command
1889
+ // you can run in any project without copy-pasting tokens.
1890
+ if (!instanceId || !apiKey) {
1891
+ try {
1892
+ const { existsSync } = await import('node:fs');
1893
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
1894
+ const savedPath = home ? resolve(home, '.claude', 'mcp.json') : '';
1895
+ if (savedPath && existsSync(savedPath)) {
1896
+ const saved = JSON.parse(await readFile(savedPath, 'utf-8'));
1897
+ const env = saved.mcpServers?.['cachly']?.env ?? {};
1898
+ apiKey ??= env['CACHLY_JWT'];
1899
+ instanceId ??= env['CACHLY_BRAIN_INSTANCE_ID'];
1900
+ if (apiKey && instanceId) {
1901
+ console.log('✓ Reusing credentials from ~/.claude/mcp.json (saved by a prior setup)');
1902
+ }
1903
+ }
1904
+ }
1905
+ catch { /* fall through to the usage error below */ }
1906
+ }
1887
1907
  if (!instanceId || !apiKey) {
1888
1908
  console.error('\nUsage: npx @cachly-dev/mcp-server@latest init --instance-id <uuid> --api-key <cky_live_...> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]\n');
1889
- console.error('Or run interactively (no flags needed): npx @cachly-dev/mcp-server@latest setup\n');
1909
+ console.error('First time? Run the zero-config wizard (signs you in, auto-provisions): npx @cachly-dev/mcp-server@latest setup\n');
1890
1910
  console.error('Get your credentials from: https://cachly.dev/setup-ai\n');
1891
1911
  process.exit(1);
1892
1912
  }
@@ -1894,9 +1914,17 @@ if (process.argv[2] === 'init') {
1894
1914
  const configPath = resolve(projectDir, configFile);
1895
1915
  await mkdir(dirname(configPath), { recursive: true });
1896
1916
  const { existsSync: exInit } = await import('node:fs');
1917
+ const configExisted = exInit(configPath);
1918
+ const prevConfig = configExisted ? await readFile(configPath, 'utf-8').catch(() => '') : '';
1897
1919
  const merged = await mergeMcpConfig(configPath, apiKey, instanceId, editor, { readFile, existsSync: exInit });
1898
- await writeFile(configPath, merged, 'utf-8');
1899
- console.log(`\n✅ ${exInit(configPath) ? 'Updated' : 'Written'}: ${configFile}`);
1920
+ // Idempotent: only write when the content actually changes.
1921
+ if (merged !== prevConfig) {
1922
+ await writeFile(configPath, merged, 'utf-8');
1923
+ console.log(`\n✅ ${configExisted ? 'Updated' : 'Written'}: ${configFile}`);
1924
+ }
1925
+ else {
1926
+ console.log(`\n✓ Already configured: ${configFile} (no change)`);
1927
+ }
1900
1928
  // Always write CLAUDE.md (idempotent — safe to run multiple times)
1901
1929
  const result = await writeClaudeMd(projectDir, instanceId);
1902
1930
  const action = result === 'updated' ? '✅ Updated' : result === 'appended' ? '✅ Appended to' : '✅ Written';
@@ -1942,8 +1970,10 @@ if (process.argv[2] === 'init') {
1942
1970
  }
1943
1971
  }
1944
1972
  catch { /* non-critical — git hook is a best-effort feature */ }
1945
- console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}!`);
1973
+ const initSecs = ((Date.now() - initStart) / 1000).toFixed(1);
1974
+ console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}! (${initSecs}s)`);
1946
1975
  console.log(` Restart your editor — the \`cachly\` MCP tools will appear.`);
1976
+ console.log(` Re-run \`init\` anytime — it's idempotent (only writes what changed).`);
1947
1977
  console.log(`\n 📛 Add a live badge to your README:`);
1948
1978
  console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
1949
1979
  process.exit(0);
@@ -2620,6 +2650,32 @@ if (process.argv[2] === 'setup') {
2620
2650
  console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
2621
2651
  console.log(`\n 📛 Add a live badge to your README:`);
2622
2652
  console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
2653
+ // ── Step 6c: Governance bootstrap — assign the first admin role ─────────────
2654
+ // Optional nudge: ask once whether the user wants to establish the role model.
2655
+ // team_assign_role handles idempotency (first call is free; later calls need admin).
2656
+ if (!nonInteractive) {
2657
+ try {
2658
+ console.log('\n👑 Governance (optional — role model: admin · reviewer · contributor · viewer)');
2659
+ console.log('────────────────────────────────────────');
2660
+ const addAdmin = await ask(' Set yourself as the first admin? (y/N): ', 'N');
2661
+ if (addAdmin.toLowerCase() === 'y') {
2662
+ const adminHandle = await ask(' Your handle (e.g. "alice"): ');
2663
+ if (adminHandle.trim()) {
2664
+ const assignOut = await handleTool('team_assign_role', {
2665
+ instance_id: instance.id, handle: adminHandle.trim(), role: 'admin',
2666
+ }).catch(() => '');
2667
+ if (assignOut) {
2668
+ console.log(` ✅ ${adminHandle.trim()} is now admin. Invite teammates:\n`);
2669
+ console.log(` team_assign_role(instance_id="${instance.id}", handle="<teammate>", role="contributor", assigned_by="${adminHandle.trim()}")\n`);
2670
+ }
2671
+ }
2672
+ }
2673
+ else {
2674
+ console.log(' Skipped — run team_assign_role at any time.\n');
2675
+ }
2676
+ }
2677
+ catch { /* non-critical */ }
2678
+ }
2623
2679
  // ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
2624
2680
  if (!nonInteractive) {
2625
2681
  const email = await ask(' 📬 Stay in the loop? Email for release notes [Enter to skip]: ');