@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/handlers/brain.js +8 -1
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/index.js +163 -50
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
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.
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(`${
|
|
2330
|
+
const deviceRes = await fetch(`${API_URL}/api/v1/auth/device/code`, {
|
|
2285
2331
|
method: 'POST',
|
|
2286
|
-
headers: { 'Content-Type': 'application/
|
|
2287
|
-
body:
|
|
2332
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2333
|
+
body: JSON.stringify({ client_id: 'cachly-mcp-cli' }),
|
|
2334
|
+
signal: AbortSignal.timeout(8000),
|
|
2288
2335
|
});
|
|
2289
|
-
if (
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
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
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
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
|
-
|
|
2343
|
-
if (tokenData.error === 'slow_down')
|
|
2419
|
+
if (tokenData?.error === 'slow_down')
|
|
2344
2420
|
pollInterval = Math.min(pollInterval + 2000, 15000);
|
|
2345
|
-
else if (tokenData
|
|
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"),
|