@cachly-dev/mcp-server 0.10.73 → 0.10.75

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
@@ -63,7 +63,7 @@ import { Redis } from 'ioredis';
63
63
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
64
64
  let JWT = process.env.CACHLY_JWT ?? '';
65
65
  const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
66
- const CURRENT_VERSION = '0.10.73';
66
+ const CURRENT_VERSION = '0.10.75';
67
67
  // Max time to wait for a freshly-provisioned instance to become "running" before
68
68
  // giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
69
69
  // old hard-coded 25s caused spurious "instance_not_reachable" on first run.
@@ -209,13 +209,34 @@ async function selfHealAuth() {
209
209
  }
210
210
  let _deviceFlow = null;
211
211
  async function startDeviceFlow() {
212
- const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
213
- const CLIENT_ID = 'cachly-cli';
212
+ // Try the API proxy first (more reliable than direct Keycloak endpoint)
214
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';
215
236
  const res = await fetch(`${AUTH_BASE}/auth/device`, {
216
237
  method: 'POST',
217
238
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
218
- body: `client_id=${CLIENT_ID}&scope=openid`,
239
+ body: `client_id=cachly-cli&scope=openid`,
219
240
  signal: AbortSignal.timeout(8000),
220
241
  });
221
242
  if (!res.ok)
@@ -226,7 +247,7 @@ async function startDeviceFlow() {
226
247
  userCode: data.user_code,
227
248
  verifyUrl: data.verification_uri_complete,
228
249
  pollInterval: (data.interval ?? 5) * 1000,
229
- deadline: Date.now() + 10 * 60 * 1000, // 10 min
250
+ deadline: Date.now() + 10 * 60 * 1000,
230
251
  polling: false,
231
252
  };
232
253
  }
@@ -237,16 +258,34 @@ async function startDeviceFlow() {
237
258
  async function pollDeviceFlow(flow) {
238
259
  if (Date.now() > flow.deadline)
239
260
  return 'expired';
240
- const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
241
- const CLIENT_ID = 'cachly-cli';
242
- 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';
243
275
  const res = await fetch(`${AUTH_BASE}/token`, {
244
276
  method: 'POST',
245
277
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
246
- 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}`,
247
279
  signal: AbortSignal.timeout(8000),
248
280
  });
249
- 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';
250
289
  if (data.access_token) {
251
290
  // Exchange Keycloak JWT → long-lived API key
252
291
  let apiKey = data.access_token;
@@ -2256,8 +2295,15 @@ if (process.argv[2] === 'setup') {
2256
2295
  const { resolve, dirname } = await import('node:path');
2257
2296
  const { createInterface } = await import('node:readline');
2258
2297
  sendFunnelEvent('setup_started');
2259
- // --yes / -y → non-interactive mode (skips all prompts, picks defaults)
2260
- const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y');
2298
+ // --yes / -y → non-interactive mode (skips all prompts, picks defaults).
2299
+ // Also auto-detect a non-interactive stdin (VSCode tasks, CI, piped input):
2300
+ // a readline question against a non-TTY stdin never resolves and hangs the
2301
+ // wizard forever. Treat that exactly like --yes so we never block.
2302
+ const stdinIsInteractive = process.stdin.isTTY === true;
2303
+ const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y') || !stdinIsInteractive;
2304
+ if (!stdinIsInteractive && !process.argv.includes('--yes') && !process.argv.includes('-y')) {
2305
+ console.log('ℹ️ Non-interactive terminal detected — running in automatic mode (no prompts).\n');
2306
+ }
2261
2307
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2262
2308
  const ask = (q, defaultVal = '') => {
2263
2309
  if (nonInteractive) {
@@ -2274,41 +2320,52 @@ if (process.argv[2] === 'setup') {
2274
2320
  console.log('✓ Using token from CACHLY_JWT env var\n');
2275
2321
  }
2276
2322
  else {
2277
- const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
2278
- const CLIENT_ID = 'cachly-cli';
2279
2323
  console.log('Step 1: Sign in to cachly (free, no credit card)\n');
2280
2324
  sendFunnelEvent('setup_auth_started');
2281
- // Start device flow
2325
+ // Start device flow — try API proxy first, fall back to direct Keycloak
2282
2326
  let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
2327
+ let deviceFlowOk = false;
2328
+ // Attempt 1: API proxy (recommended path)
2283
2329
  try {
2284
- const deviceRes = await fetch(`${AUTH_BASE}/auth/device`, {
2330
+ const deviceRes = await fetch(`${API_URL}/api/v1/auth/device/code`, {
2285
2331
  method: 'POST',
2286
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2287
- body: `client_id=${CLIENT_ID}&scope=openid`,
2332
+ headers: { 'Content-Type': 'application/json' },
2333
+ body: JSON.stringify({ client_id: 'cachly-mcp-cli' }),
2334
+ signal: AbortSignal.timeout(8000),
2288
2335
  });
2289
- if (!deviceRes.ok)
2290
- throw new Error(`Device flow error: HTTP ${deviceRes.status}`);
2291
- const data = await deviceRes.json();
2292
- deviceCode = data.device_code;
2293
- userCode = data.user_code;
2294
- verifyUri = data.verification_uri_complete;
2295
- pollInterval = (data.interval ?? 5) * 1000;
2336
+ if (deviceRes.ok) {
2337
+ const data = await deviceRes.json();
2338
+ deviceCode = data.device_code;
2339
+ userCode = data.user_code;
2340
+ verifyUri = data.verification_uri;
2341
+ pollInterval = (data.interval ?? 5) * 1000;
2342
+ deviceFlowOk = true;
2343
+ }
2296
2344
  }
2297
- catch (e) {
2298
- console.error(`\nFailed to start device flow: ${e.message}`);
2299
- console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
2300
- token = await ask(' Paste API token (cky_live_...): ');
2301
- if (!token) {
2302
- console.error('\nToken is required. Aborting.\n');
2303
- rl.close();
2304
- process.exit(1);
2345
+ catch { /* fall through */ }
2346
+ // Attempt 2: Direct Keycloak device flow
2347
+ if (!deviceFlowOk) {
2348
+ try {
2349
+ const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
2350
+ const deviceRes = await fetch(`${AUTH_BASE}/auth/device`, {
2351
+ method: 'POST',
2352
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2353
+ body: `client_id=cachly-cli&scope=openid`,
2354
+ signal: AbortSignal.timeout(8000),
2355
+ });
2356
+ if (deviceRes.ok) {
2357
+ const data = await deviceRes.json();
2358
+ deviceCode = data.device_code;
2359
+ userCode = data.user_code;
2360
+ verifyUri = data.verification_uri_complete;
2361
+ pollInterval = (data.interval ?? 5) * 1000;
2362
+ deviceFlowOk = true;
2363
+ }
2305
2364
  }
2306
- sendFunnelEvent('setup_auth_completed');
2307
- console.log('');
2308
- deviceCode = ''; // mark as fallback so we skip polling
2365
+ catch { /* fall through */ }
2309
2366
  }
2310
- if (deviceCode) {
2311
- // Open browser
2367
+ if (deviceFlowOk && deviceCode) {
2368
+ // Open browser and show code
2312
2369
  console.log(` Code: \x1b[1;33m${userCode}\x1b[0m`);
2313
2370
  console.log(` URL: ${verifyUri}\n`);
2314
2371
  try {
@@ -2320,29 +2377,49 @@ if (process.argv[2] === 'setup') {
2320
2377
  catch {
2321
2378
  console.log(' 👉 Open the URL above in your browser and enter the code.\n');
2322
2379
  }
2323
- // Poll for token
2380
+ // Poll for token with proper timeouts
2324
2381
  process.stdout.write(' Waiting for authorization');
2325
- const deadline = Date.now() + 10 * 60 * 1000; // 10 min
2382
+ const deadline = Date.now() + 10 * 60 * 1000;
2326
2383
  while (Date.now() < deadline) {
2327
2384
  await new Promise(r => setTimeout(r, pollInterval));
2328
2385
  process.stdout.write('.');
2329
2386
  try {
2330
- const tokenRes = await fetch(`${AUTH_BASE}/token`, {
2331
- method: 'POST',
2332
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2333
- body: `client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode}`,
2334
- });
2335
- const tokenData = await tokenRes.json();
2336
- if (tokenData.access_token) {
2387
+ let tokenData = null;
2388
+ try {
2389
+ const proxyRes = await fetch(`${API_URL}/api/v1/auth/device/token`, {
2390
+ method: 'POST',
2391
+ headers: { 'Content-Type': 'application/json' },
2392
+ body: JSON.stringify({ device_code: deviceCode, client_id: 'cachly-mcp-cli' }),
2393
+ signal: AbortSignal.timeout(8000),
2394
+ });
2395
+ if (proxyRes.ok)
2396
+ tokenData = await proxyRes.json();
2397
+ }
2398
+ catch { /* try Keycloak */ }
2399
+ if (!tokenData) {
2400
+ const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
2401
+ try {
2402
+ const kcRes = await fetch(`${AUTH_BASE}/token`, {
2403
+ method: 'POST',
2404
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2405
+ body: `client_id=cachly-cli&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode}`,
2406
+ signal: AbortSignal.timeout(8000),
2407
+ });
2408
+ tokenData = await kcRes.json();
2409
+ }
2410
+ catch { /* network hiccup */ }
2411
+ }
2412
+ if (tokenData?.access_token) {
2337
2413
  token = tokenData.access_token;
2338
2414
  console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
2339
2415
  sendFunnelEvent('setup_auth_completed');
2416
+ sendFunnelEvent('device_flow_completed');
2340
2417
  break;
2341
2418
  }
2342
- // authorization_pending = keep polling; slow_down = increase interval
2343
- if (tokenData.error === 'slow_down')
2419
+ if (tokenData?.error === 'slow_down')
2344
2420
  pollInterval = Math.min(pollInterval + 2000, 15000);
2345
- else if (tokenData.error && tokenData.error !== 'authorization_pending') {
2421
+ else if (tokenData?.error && tokenData.error !== 'authorization_pending') {
2422
+ sendFunnelEvent('device_flow_failed', { reason: tokenData.error });
2346
2423
  console.error(`\nAuth error: ${tokenData.error}. Aborting.\n`);
2347
2424
  rl.close();
2348
2425
  process.exit(1);
@@ -2351,12 +2428,48 @@ if (process.argv[2] === 'setup') {
2351
2428
  catch { /* network hiccup — keep polling */ }
2352
2429
  }
2353
2430
  if (!token) {
2431
+ sendFunnelEvent('device_flow_failed', { reason: 'timeout' });
2354
2432
  console.error('\nTimed out waiting for authorization. Aborting.\n');
2355
2433
  rl.close();
2356
2434
  process.exit(1);
2357
2435
  }
2358
2436
  console.log('');
2359
2437
  }
2438
+ else {
2439
+ // Device flow unavailable — web fallback with automatic browser open
2440
+ sendFunnelEvent('device_flow_failed', { reason: 'device_flow_unavailable' });
2441
+ const signupUrl = 'https://cachly.dev/setup-ai';
2442
+ console.log(' ⚠️ Could not start automatic sign-in. Opening browser for web sign-in...\n');
2443
+ console.log(` URL: \x1b[36m${signupUrl}\x1b[0m`);
2444
+ console.log(' 1. Sign up / log in at the URL above');
2445
+ console.log(' 2. Go to Settings → API Keys → Create new key');
2446
+ console.log(' 3. Copy the key (starts with cky_live_...)\n');
2447
+ try {
2448
+ const { execSync } = await import('node:child_process');
2449
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
2450
+ execSync(`${openCmd} "${signupUrl}"`, { stdio: 'ignore' });
2451
+ }
2452
+ catch { /* can't open browser — instructions shown above */ }
2453
+ // In a non-interactive terminal (VSCode task, CI) we cannot read a pasted
2454
+ // key — exit with clear, actionable instructions instead of hanging.
2455
+ if (nonInteractive) {
2456
+ sendFunnelEvent('device_flow_failed', { reason: 'non_interactive_no_key' });
2457
+ console.error('\n ⚠️ This terminal is non-interactive, so the key cannot be pasted here.');
2458
+ console.error(' Get your key at the URL above, then set it and re-run setup:\n');
2459
+ console.error(' \x1b[1mCACHLY_JWT=cky_live_xxx npx @cachly-dev/mcp-server@latest setup\x1b[0m\n');
2460
+ console.error(' Or add it to your editor\'s MCP config under env.CACHLY_JWT.\n');
2461
+ rl.close();
2462
+ process.exit(1);
2463
+ }
2464
+ token = await ask(' Paste API key (cky_live_...): ');
2465
+ if (!token) {
2466
+ console.error('\nAPI key is required. Aborting.\n');
2467
+ rl.close();
2468
+ process.exit(1);
2469
+ }
2470
+ sendFunnelEvent('setup_auth_completed');
2471
+ console.log('');
2472
+ }
2360
2473
  }
2361
2474
  // ── Step 1b: Exchange Keycloak JWT → long-lived cky_live_ API key ──────────
2362
2475
  // Only do this when the token looks like a Keycloak JWT (starts with "eyJ"),