@cachly-dev/mcp-server 0.10.72 → 0.10.74

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.72';
66
+ const CURRENT_VERSION = '0.10.74';
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.
@@ -210,13 +209,34 @@ async function selfHealAuth() {
210
209
  }
211
210
  let _deviceFlow = null;
212
211
  async function startDeviceFlow() {
213
- const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
214
- const CLIENT_ID = 'cachly-cli';
212
+ // Try the API proxy first (more reliable than direct Keycloak endpoint)
215
213
  try {
214
+ const res = await fetch(`${API_URL}/api/v1/auth/device/code`, {
215
+ method: 'POST',
216
+ headers: { 'Content-Type': 'application/json' },
217
+ body: JSON.stringify({ client_id: 'cachly-mcp-cli' }),
218
+ signal: AbortSignal.timeout(8000),
219
+ });
220
+ if (res.ok) {
221
+ const data = await res.json();
222
+ return {
223
+ deviceCode: data.device_code,
224
+ userCode: data.user_code,
225
+ verifyUrl: data.verification_uri,
226
+ pollInterval: (data.interval ?? 5) * 1000,
227
+ deadline: Date.now() + 10 * 60 * 1000,
228
+ polling: false,
229
+ };
230
+ }
231
+ }
232
+ catch { /* fall through to Keycloak */ }
233
+ // Fallback: direct Keycloak endpoint
234
+ try {
235
+ const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
216
236
  const res = await fetch(`${AUTH_BASE}/auth/device`, {
217
237
  method: 'POST',
218
238
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
219
- body: `client_id=${CLIENT_ID}&scope=openid`,
239
+ body: `client_id=cachly-cli&scope=openid`,
220
240
  signal: AbortSignal.timeout(8000),
221
241
  });
222
242
  if (!res.ok)
@@ -227,7 +247,7 @@ async function startDeviceFlow() {
227
247
  userCode: data.user_code,
228
248
  verifyUrl: data.verification_uri_complete,
229
249
  pollInterval: (data.interval ?? 5) * 1000,
230
- deadline: Date.now() + 10 * 60 * 1000, // 10 min
250
+ deadline: Date.now() + 10 * 60 * 1000,
231
251
  polling: false,
232
252
  };
233
253
  }
@@ -238,16 +258,34 @@ async function startDeviceFlow() {
238
258
  async function pollDeviceFlow(flow) {
239
259
  if (Date.now() > flow.deadline)
240
260
  return 'expired';
241
- const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
242
- const CLIENT_ID = 'cachly-cli';
243
- try {
261
+ // Try API proxy first, then direct Keycloak as fallback
262
+ const tryProxy = async () => {
263
+ const res = await fetch(`${API_URL}/api/v1/auth/device/token`, {
264
+ method: 'POST',
265
+ headers: { 'Content-Type': 'application/json' },
266
+ body: JSON.stringify({ device_code: flow.deviceCode, client_id: 'cachly-mcp-cli' }),
267
+ signal: AbortSignal.timeout(8000),
268
+ });
269
+ if (!res.ok)
270
+ return null;
271
+ return res.json();
272
+ };
273
+ const tryKeycloak = async () => {
274
+ const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
244
275
  const res = await fetch(`${AUTH_BASE}/token`, {
245
276
  method: 'POST',
246
277
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
247
- body: `client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${flow.deviceCode}`,
278
+ body: `client_id=cachly-cli&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${flow.deviceCode}`,
248
279
  signal: AbortSignal.timeout(8000),
249
280
  });
250
- const data = await res.json();
281
+ return res.json();
282
+ };
283
+ try {
284
+ let data = await tryProxy().catch(() => null);
285
+ if (!data)
286
+ data = await tryKeycloak().catch(() => null);
287
+ if (!data)
288
+ return 'pending';
251
289
  if (data.access_token) {
252
290
  // Exchange Keycloak JWT → long-lived API key
253
291
  let apiKey = data.access_token;
@@ -666,7 +704,7 @@ async function handleTool(name, args) {
666
704
  '',
667
705
  'After sign-in: call **any tool again** — your Brain activates instantly.',
668
706
  '',
669
- '✨ Free forever · No credit card · 105 MCP tools · GDPR · EU servers',
707
+ '✨ Free forever · No credit card · 107 MCP tools · GDPR · EU servers',
670
708
  ].join('\n');
671
709
  }
672
710
  // Device flow unavailable (network issue) — fall back to manual setup
@@ -1869,24 +1907,45 @@ if (!process.argv[2] && process.stdout.isTTY) {
1869
1907
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
1870
1908
  console.log('');
1871
1909
  console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
1872
- console.log(' \x1b[90mFree forever · GDPR · German servers · 105 MCP tools\x1b[0m');
1910
+ console.log(' \x1b[90mFree forever · GDPR · German servers · 107 MCP tools\x1b[0m');
1873
1911
  console.log('');
1874
1912
  process.exit(0);
1875
1913
  }
1876
1914
  // ── CLI: cachly init ──────────────────────────────────────────────────────────
1877
1915
  // Usage: npx @cachly-dev/mcp-server init --instance-id <id> --api-key <key> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]
1878
1916
  if (process.argv[2] === 'init') {
1879
- const { writeFile, mkdir } = await import('node:fs/promises');
1917
+ const initStart = Date.now();
1918
+ const { writeFile, mkdir, readFile } = await import('node:fs/promises');
1880
1919
  const { resolve, dirname } = await import('node:path');
1881
1920
  const argv = process.argv.slice(3);
1882
1921
  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;
1922
+ let instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
1923
+ let apiKey = flag('api-key') ?? process.env.CACHLY_JWT;
1885
1924
  const editor = (flag('editor') ?? 'claude').toLowerCase();
1886
1925
  const projectDir = resolve(flag('project-dir') ?? '.');
1926
+ // Zero-arg path: fall back to credentials saved by a previous `setup`
1927
+ // (~/.claude/mcp.json). Makes `init` a fast, idempotent re-configuration command
1928
+ // you can run in any project without copy-pasting tokens.
1929
+ if (!instanceId || !apiKey) {
1930
+ try {
1931
+ const { existsSync } = await import('node:fs');
1932
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
1933
+ const savedPath = home ? resolve(home, '.claude', 'mcp.json') : '';
1934
+ if (savedPath && existsSync(savedPath)) {
1935
+ const saved = JSON.parse(await readFile(savedPath, 'utf-8'));
1936
+ const env = saved.mcpServers?.['cachly']?.env ?? {};
1937
+ apiKey ??= env['CACHLY_JWT'];
1938
+ instanceId ??= env['CACHLY_BRAIN_INSTANCE_ID'];
1939
+ if (apiKey && instanceId) {
1940
+ console.log('✓ Reusing credentials from ~/.claude/mcp.json (saved by a prior setup)');
1941
+ }
1942
+ }
1943
+ }
1944
+ catch { /* fall through to the usage error below */ }
1945
+ }
1887
1946
  if (!instanceId || !apiKey) {
1888
1947
  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');
1948
+ console.error('First time? Run the zero-config wizard (signs you in, auto-provisions): npx @cachly-dev/mcp-server@latest setup\n');
1890
1949
  console.error('Get your credentials from: https://cachly.dev/setup-ai\n');
1891
1950
  process.exit(1);
1892
1951
  }
@@ -1894,9 +1953,17 @@ if (process.argv[2] === 'init') {
1894
1953
  const configPath = resolve(projectDir, configFile);
1895
1954
  await mkdir(dirname(configPath), { recursive: true });
1896
1955
  const { existsSync: exInit } = await import('node:fs');
1956
+ const configExisted = exInit(configPath);
1957
+ const prevConfig = configExisted ? await readFile(configPath, 'utf-8').catch(() => '') : '';
1897
1958
  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}`);
1959
+ // Idempotent: only write when the content actually changes.
1960
+ if (merged !== prevConfig) {
1961
+ await writeFile(configPath, merged, 'utf-8');
1962
+ console.log(`\n✅ ${configExisted ? 'Updated' : 'Written'}: ${configFile}`);
1963
+ }
1964
+ else {
1965
+ console.log(`\n✓ Already configured: ${configFile} (no change)`);
1966
+ }
1900
1967
  // Always write CLAUDE.md (idempotent — safe to run multiple times)
1901
1968
  const result = await writeClaudeMd(projectDir, instanceId);
1902
1969
  const action = result === 'updated' ? '✅ Updated' : result === 'appended' ? '✅ Appended to' : '✅ Written';
@@ -1942,8 +2009,10 @@ if (process.argv[2] === 'init') {
1942
2009
  }
1943
2010
  }
1944
2011
  catch { /* non-critical — git hook is a best-effort feature */ }
1945
- console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}!`);
2012
+ const initSecs = ((Date.now() - initStart) / 1000).toFixed(1);
2013
+ console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}! (${initSecs}s)`);
1946
2014
  console.log(` Restart your editor — the \`cachly\` MCP tools will appear.`);
2015
+ console.log(` Re-run \`init\` anytime — it's idempotent (only writes what changed).`);
1947
2016
  console.log(`\n 📛 Add a live badge to your README:`);
1948
2017
  console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
1949
2018
  process.exit(0);
@@ -2244,41 +2313,52 @@ if (process.argv[2] === 'setup') {
2244
2313
  console.log('✓ Using token from CACHLY_JWT env var\n');
2245
2314
  }
2246
2315
  else {
2247
- const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
2248
- const CLIENT_ID = 'cachly-cli';
2249
2316
  console.log('Step 1: Sign in to cachly (free, no credit card)\n');
2250
2317
  sendFunnelEvent('setup_auth_started');
2251
- // Start device flow
2318
+ // Start device flow — try API proxy first, fall back to direct Keycloak
2252
2319
  let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
2320
+ let deviceFlowOk = false;
2321
+ // Attempt 1: API proxy (recommended path)
2253
2322
  try {
2254
- const deviceRes = await fetch(`${AUTH_BASE}/auth/device`, {
2323
+ const deviceRes = await fetch(`${API_URL}/api/v1/auth/device/code`, {
2255
2324
  method: 'POST',
2256
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2257
- body: `client_id=${CLIENT_ID}&scope=openid`,
2325
+ headers: { 'Content-Type': 'application/json' },
2326
+ body: JSON.stringify({ client_id: 'cachly-mcp-cli' }),
2327
+ signal: AbortSignal.timeout(8000),
2258
2328
  });
2259
- if (!deviceRes.ok)
2260
- throw new Error(`Device flow error: HTTP ${deviceRes.status}`);
2261
- const data = await deviceRes.json();
2262
- deviceCode = data.device_code;
2263
- userCode = data.user_code;
2264
- verifyUri = data.verification_uri_complete;
2265
- pollInterval = (data.interval ?? 5) * 1000;
2329
+ if (deviceRes.ok) {
2330
+ const data = await deviceRes.json();
2331
+ deviceCode = data.device_code;
2332
+ userCode = data.user_code;
2333
+ verifyUri = data.verification_uri;
2334
+ pollInterval = (data.interval ?? 5) * 1000;
2335
+ deviceFlowOk = true;
2336
+ }
2266
2337
  }
2267
- catch (e) {
2268
- console.error(`\nFailed to start device flow: ${e.message}`);
2269
- console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
2270
- token = await ask(' Paste API token (cky_live_...): ');
2271
- if (!token) {
2272
- console.error('\nToken is required. Aborting.\n');
2273
- rl.close();
2274
- process.exit(1);
2338
+ catch { /* fall through */ }
2339
+ // Attempt 2: Direct Keycloak device flow
2340
+ if (!deviceFlowOk) {
2341
+ try {
2342
+ const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
2343
+ const deviceRes = await fetch(`${AUTH_BASE}/auth/device`, {
2344
+ method: 'POST',
2345
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2346
+ body: `client_id=cachly-cli&scope=openid`,
2347
+ signal: AbortSignal.timeout(8000),
2348
+ });
2349
+ if (deviceRes.ok) {
2350
+ const data = await deviceRes.json();
2351
+ deviceCode = data.device_code;
2352
+ userCode = data.user_code;
2353
+ verifyUri = data.verification_uri_complete;
2354
+ pollInterval = (data.interval ?? 5) * 1000;
2355
+ deviceFlowOk = true;
2356
+ }
2275
2357
  }
2276
- sendFunnelEvent('setup_auth_completed');
2277
- console.log('');
2278
- deviceCode = ''; // mark as fallback so we skip polling
2358
+ catch { /* fall through */ }
2279
2359
  }
2280
- if (deviceCode) {
2281
- // Open browser
2360
+ if (deviceFlowOk && deviceCode) {
2361
+ // Open browser and show code
2282
2362
  console.log(` Code: \x1b[1;33m${userCode}\x1b[0m`);
2283
2363
  console.log(` URL: ${verifyUri}\n`);
2284
2364
  try {
@@ -2290,29 +2370,49 @@ if (process.argv[2] === 'setup') {
2290
2370
  catch {
2291
2371
  console.log(' 👉 Open the URL above in your browser and enter the code.\n');
2292
2372
  }
2293
- // Poll for token
2373
+ // Poll for token with proper timeouts
2294
2374
  process.stdout.write(' Waiting for authorization');
2295
- const deadline = Date.now() + 10 * 60 * 1000; // 10 min
2375
+ const deadline = Date.now() + 10 * 60 * 1000;
2296
2376
  while (Date.now() < deadline) {
2297
2377
  await new Promise(r => setTimeout(r, pollInterval));
2298
2378
  process.stdout.write('.');
2299
2379
  try {
2300
- const tokenRes = await fetch(`${AUTH_BASE}/token`, {
2301
- method: 'POST',
2302
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2303
- body: `client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode}`,
2304
- });
2305
- const tokenData = await tokenRes.json();
2306
- if (tokenData.access_token) {
2380
+ let tokenData = null;
2381
+ try {
2382
+ const proxyRes = await fetch(`${API_URL}/api/v1/auth/device/token`, {
2383
+ method: 'POST',
2384
+ headers: { 'Content-Type': 'application/json' },
2385
+ body: JSON.stringify({ device_code: deviceCode, client_id: 'cachly-mcp-cli' }),
2386
+ signal: AbortSignal.timeout(8000),
2387
+ });
2388
+ if (proxyRes.ok)
2389
+ tokenData = await proxyRes.json();
2390
+ }
2391
+ catch { /* try Keycloak */ }
2392
+ if (!tokenData) {
2393
+ const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
2394
+ try {
2395
+ const kcRes = await fetch(`${AUTH_BASE}/token`, {
2396
+ method: 'POST',
2397
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2398
+ body: `client_id=cachly-cli&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode}`,
2399
+ signal: AbortSignal.timeout(8000),
2400
+ });
2401
+ tokenData = await kcRes.json();
2402
+ }
2403
+ catch { /* network hiccup */ }
2404
+ }
2405
+ if (tokenData?.access_token) {
2307
2406
  token = tokenData.access_token;
2308
2407
  console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
2309
2408
  sendFunnelEvent('setup_auth_completed');
2409
+ sendFunnelEvent('device_flow_completed');
2310
2410
  break;
2311
2411
  }
2312
- // authorization_pending = keep polling; slow_down = increase interval
2313
- if (tokenData.error === 'slow_down')
2412
+ if (tokenData?.error === 'slow_down')
2314
2413
  pollInterval = Math.min(pollInterval + 2000, 15000);
2315
- else if (tokenData.error && tokenData.error !== 'authorization_pending') {
2414
+ else if (tokenData?.error && tokenData.error !== 'authorization_pending') {
2415
+ sendFunnelEvent('device_flow_failed', { reason: tokenData.error });
2316
2416
  console.error(`\nAuth error: ${tokenData.error}. Aborting.\n`);
2317
2417
  rl.close();
2318
2418
  process.exit(1);
@@ -2321,12 +2421,37 @@ if (process.argv[2] === 'setup') {
2321
2421
  catch { /* network hiccup — keep polling */ }
2322
2422
  }
2323
2423
  if (!token) {
2424
+ sendFunnelEvent('device_flow_failed', { reason: 'timeout' });
2324
2425
  console.error('\nTimed out waiting for authorization. Aborting.\n');
2325
2426
  rl.close();
2326
2427
  process.exit(1);
2327
2428
  }
2328
2429
  console.log('');
2329
2430
  }
2431
+ else {
2432
+ // Device flow unavailable — web fallback with automatic browser open
2433
+ sendFunnelEvent('device_flow_failed', { reason: 'device_flow_unavailable' });
2434
+ const signupUrl = 'https://cachly.dev/setup-ai';
2435
+ console.log(' ⚠️ Could not start automatic sign-in. Opening browser for web sign-in...\n');
2436
+ console.log(` URL: \x1b[36m${signupUrl}\x1b[0m`);
2437
+ console.log(' 1. Sign up / log in at the URL above');
2438
+ console.log(' 2. Go to Settings → API Keys → Create new key');
2439
+ console.log(' 3. Copy the key (starts with cky_live_...)\n');
2440
+ try {
2441
+ const { execSync } = await import('node:child_process');
2442
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
2443
+ execSync(`${openCmd} "${signupUrl}"`, { stdio: 'ignore' });
2444
+ }
2445
+ catch { /* can't open browser — instructions shown above */ }
2446
+ token = await ask(' Paste API key (cky_live_...): ');
2447
+ if (!token) {
2448
+ console.error('\nAPI key is required. Aborting.\n');
2449
+ rl.close();
2450
+ process.exit(1);
2451
+ }
2452
+ sendFunnelEvent('setup_auth_completed');
2453
+ console.log('');
2454
+ }
2330
2455
  }
2331
2456
  // ── Step 1b: Exchange Keycloak JWT → long-lived cky_live_ API key ──────────
2332
2457
  // Only do this when the token looks like a Keycloak JWT (starts with "eyJ"),