@cachly-dev/mcp-server 0.10.48 → 0.10.55
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/.github/workflows/publish.yml +2 -2
- package/dist/handlers/brain.js +89 -4
- package/dist/handlers/brain.js.map +1 -1
- package/dist/handlers/team.js +3 -1
- package/dist/handlers/team.js.map +1 -1
- package/dist/index.js +212 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/handlers/brain.ts +89 -7
- package/src/handlers/team.ts +3 -1
- package/src/index.ts +197 -96
package/dist/index.js
CHANGED
|
@@ -49,8 +49,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
49
49
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
50
50
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
51
51
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
52
|
-
const
|
|
53
|
-
const CURRENT_VERSION = '0.10.47';
|
|
52
|
+
const CURRENT_VERSION = '0.10.55';
|
|
54
53
|
const reqCtx = new AsyncLocalStorage();
|
|
55
54
|
function getJWT() { return reqCtx.getStore()?.jwt || JWT; }
|
|
56
55
|
function getDefaultInstanceId() { return reqCtx.getStore()?.instanceId || _defaultInstanceId; }
|
|
@@ -70,9 +69,16 @@ async function resolveDefaultInstanceId() {
|
|
|
70
69
|
if (!getJWT())
|
|
71
70
|
return '';
|
|
72
71
|
// Cooldown: don't hammer the API on every tool call after a transient failure.
|
|
72
|
+
// 10 s is enough to avoid rate-limit spam while not blocking the user for too long.
|
|
73
73
|
const now = Date.now();
|
|
74
|
-
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt <
|
|
75
|
-
|
|
74
|
+
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 10_000) {
|
|
75
|
+
throw new McpError(ErrorCode.InvalidRequest, '⚠️ Cachly Brain is temporarily unreachable (API error a few seconds ago).\n\n' +
|
|
76
|
+
'The server will retry automatically in a moment. If this persists:\n' +
|
|
77
|
+
'• Run `get_api_status` for a full diagnostic\n' +
|
|
78
|
+
'• Check that CACHLY_JWT is set and valid\n' +
|
|
79
|
+
'• Visit https://cachly.dev/instances to verify your Brain is running');
|
|
80
|
+
}
|
|
81
|
+
;
|
|
76
82
|
_defaultInstanceLastAttempt = now;
|
|
77
83
|
try {
|
|
78
84
|
const res = await fetch(`${API_URL}/api/v1/instances`, {
|
|
@@ -153,6 +159,31 @@ async function pollDeviceFlow(flow) {
|
|
|
153
159
|
catch { /* use raw JWT as fallback */ }
|
|
154
160
|
JWT = apiKey;
|
|
155
161
|
setEmbedJwt(apiKey); // keep embeddings.ts in sync
|
|
162
|
+
// Persist API key to ~/.claude/mcp.json so the next editor restart
|
|
163
|
+
// doesn't start over from a missing CACHLY_JWT (fixes BrainRecallCount=0 bug).
|
|
164
|
+
try {
|
|
165
|
+
const { resolve: _res, dirname: _dir } = await import('node:path');
|
|
166
|
+
const { homedir: _hd } = await import('node:os');
|
|
167
|
+
const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
|
|
168
|
+
const { existsSync: _ex } = await import('node:fs');
|
|
169
|
+
const globalJwtPath = _res(_hd(), '.claude', 'mcp.json');
|
|
170
|
+
await _mk(_dir(globalJwtPath), { recursive: true });
|
|
171
|
+
let cfg = {};
|
|
172
|
+
if (_ex(globalJwtPath)) {
|
|
173
|
+
try {
|
|
174
|
+
cfg = JSON.parse(await _rf(globalJwtPath, 'utf-8'));
|
|
175
|
+
}
|
|
176
|
+
catch { /* corrupt — start fresh */ }
|
|
177
|
+
}
|
|
178
|
+
cfg.mcpServers ??= {};
|
|
179
|
+
cfg.mcpServers['cachly'] ??= {};
|
|
180
|
+
cfg.mcpServers['cachly'].env ??= {};
|
|
181
|
+
cfg.mcpServers['cachly'].env['CACHLY_JWT'] = apiKey;
|
|
182
|
+
if (_defaultInstanceId)
|
|
183
|
+
cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
184
|
+
await _wf(globalJwtPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
185
|
+
}
|
|
186
|
+
catch { /* non-fatal — editor will just prompt again on next restart */ }
|
|
156
187
|
_deviceFlow = null;
|
|
157
188
|
sendFunnelEvent('device_flow_completed');
|
|
158
189
|
// Auto-provision: find or create the user's brain instance.
|
|
@@ -225,12 +256,71 @@ async function pollDeviceFlow(flow) {
|
|
|
225
256
|
return 'pending';
|
|
226
257
|
}
|
|
227
258
|
}
|
|
259
|
+
// ── Silent Auto-Setup (instant-trial, zero-click) ─────────────────────────
|
|
260
|
+
// Attempts to provision a free trial silently on the first tool call when no
|
|
261
|
+
// JWT is set. Returns true if credentials were written and the tool can
|
|
262
|
+
// proceed transparently; false if the API is unreachable or trials are full.
|
|
263
|
+
// Only runs once per process (subsequent failures short-circuit immediately).
|
|
264
|
+
let _silentSetupAttempted = false;
|
|
265
|
+
async function silentAutoSetup() {
|
|
266
|
+
if (_silentSetupAttempted)
|
|
267
|
+
return false;
|
|
268
|
+
_silentSetupAttempted = true;
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch(`${API_URL}/auth/instant-trial`, {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
headers: { 'Content-Type': 'application/json' },
|
|
273
|
+
body: JSON.stringify({}),
|
|
274
|
+
signal: AbortSignal.timeout(8000),
|
|
275
|
+
});
|
|
276
|
+
if (!res.ok)
|
|
277
|
+
return false;
|
|
278
|
+
const data = await res.json();
|
|
279
|
+
if (!data.api_key)
|
|
280
|
+
return false;
|
|
281
|
+
JWT = data.api_key;
|
|
282
|
+
setEmbedJwt(data.api_key);
|
|
283
|
+
if (data.instance_id)
|
|
284
|
+
_defaultInstanceId = data.instance_id;
|
|
285
|
+
sendFunnelEvent('trial_provisioned', { version: CURRENT_VERSION });
|
|
286
|
+
// Persist credentials so the next editor restart doesn't re-provision.
|
|
287
|
+
try {
|
|
288
|
+
const { resolve: _rp, dirname: _dn } = await import('node:path');
|
|
289
|
+
const { homedir: _hd } = await import('node:os');
|
|
290
|
+
const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
|
|
291
|
+
const { existsSync: _ex } = await import('node:fs');
|
|
292
|
+
const cfgPath = _rp(_hd(), '.claude', 'mcp.json');
|
|
293
|
+
await _mk(_dn(cfgPath), { recursive: true });
|
|
294
|
+
let cfg = {};
|
|
295
|
+
if (_ex(cfgPath)) {
|
|
296
|
+
try {
|
|
297
|
+
cfg = JSON.parse(await _rf(cfgPath, 'utf-8'));
|
|
298
|
+
}
|
|
299
|
+
catch { /* corrupt — overwrite */ }
|
|
300
|
+
}
|
|
301
|
+
cfg.mcpServers ??= {};
|
|
302
|
+
cfg.mcpServers['cachly'] ??= {};
|
|
303
|
+
cfg.mcpServers['cachly'].env ??= {};
|
|
304
|
+
cfg.mcpServers['cachly'].env['CACHLY_JWT'] = data.api_key;
|
|
305
|
+
if (_defaultInstanceId)
|
|
306
|
+
cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
307
|
+
await _wf(cfgPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
308
|
+
}
|
|
309
|
+
catch { /* non-fatal — trial is active for this session even if persist fails */ }
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
228
316
|
// ── Embeddings (imported from embeddings.ts) ────────────────────────────────
|
|
229
317
|
import { setEmbedJwt } from './embeddings.js';
|
|
230
318
|
// ── Search Engine (imported from search.ts) ─────────────────────────────────
|
|
231
319
|
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP, ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
|
|
232
320
|
// ── Exported for testing (re-export from search.ts) ─────────────────────────
|
|
233
321
|
export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
|
|
322
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
323
|
+
// Instance is imported from handlers/brain.ts
|
|
234
324
|
// ── Connection pool ───────────────────────────────────────────────────────────
|
|
235
325
|
/** Reuse Redis connections across tool calls (keyed by instance_id). */
|
|
236
326
|
const pool = new Map();
|
|
@@ -254,14 +344,13 @@ async function getConnection(instance_id) {
|
|
|
254
344
|
pool.delete(instance_id);
|
|
255
345
|
cached.disconnect();
|
|
256
346
|
}
|
|
257
|
-
// Fetch instance, waiting up to
|
|
258
|
-
//
|
|
259
|
-
// all happen in quick succession and the instance isn't running yet.
|
|
347
|
+
// Fetch instance, waiting up to 8 s if it is still provisioning.
|
|
348
|
+
// (autoStartSession already gave the instance a head-start; 8 s covers any remaining gap.)
|
|
260
349
|
let inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
261
350
|
if (inst.status === 'provisioning') {
|
|
262
|
-
const deadline = Date.now() +
|
|
351
|
+
const deadline = Date.now() + 8_000;
|
|
263
352
|
while (inst.status === 'provisioning' && Date.now() < deadline) {
|
|
264
|
-
await new Promise(r => setTimeout(r,
|
|
353
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
265
354
|
inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
266
355
|
}
|
|
267
356
|
}
|
|
@@ -302,7 +391,9 @@ async function getConnection(instance_id) {
|
|
|
302
391
|
}
|
|
303
392
|
// Fetch connection details (includes password) from the dedicated endpoint.
|
|
304
393
|
let password;
|
|
305
|
-
|
|
394
|
+
// Default to false — most free/dev instances run without TLS.
|
|
395
|
+
// Only enable TLS when the API explicitly returns tls_enabled: true.
|
|
396
|
+
let tlsEnabled = inst.tls_enabled === true;
|
|
306
397
|
try {
|
|
307
398
|
const conn = await apiFetch(`/api/v1/instances/${instance_id}/connection`);
|
|
308
399
|
password = conn.password ?? undefined;
|
|
@@ -325,6 +416,7 @@ async function getConnection(instance_id) {
|
|
|
325
416
|
lazyConnect: true,
|
|
326
417
|
enableReadyCheck: true,
|
|
327
418
|
connectTimeout: 8000,
|
|
419
|
+
commandTimeout: 10000,
|
|
328
420
|
retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
|
|
329
421
|
});
|
|
330
422
|
const evict = () => pool.delete(instance_id);
|
|
@@ -347,6 +439,7 @@ async function getConnection(instance_id) {
|
|
|
347
439
|
lazyConnect: true,
|
|
348
440
|
enableReadyCheck: true,
|
|
349
441
|
connectTimeout: 8000,
|
|
442
|
+
commandTimeout: 10000,
|
|
350
443
|
retryStrategy: () => null,
|
|
351
444
|
});
|
|
352
445
|
retryClient.on('error', () => pool.delete(instance_id));
|
|
@@ -433,7 +526,10 @@ function sendFunnelEvent(event, extra) {
|
|
|
433
526
|
headers: { 'Content-Type': 'application/json' },
|
|
434
527
|
body: JSON.stringify({
|
|
435
528
|
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
436
|
-
|
|
529
|
+
// Send Cachly API keys (cky_*) for server-side tenant resolution (recall counting, emails).
|
|
530
|
+
// Raw Keycloak JWTs (eyJ...) are intentionally excluded — they are short-lived bearer tokens
|
|
531
|
+
// that should never leave the MCP process boundary.
|
|
532
|
+
...(getJWT().startsWith('cky_') ? { api_key: getJWT() } : {}),
|
|
437
533
|
...extra,
|
|
438
534
|
}),
|
|
439
535
|
signal: AbortSignal.timeout(3000),
|
|
@@ -456,11 +552,18 @@ async function sendAnonymousTelemetry(toolName) {
|
|
|
456
552
|
catch { /* fire-and-forget, never block the user */ }
|
|
457
553
|
}
|
|
458
554
|
async function handleTool(name, args) {
|
|
459
|
-
// Guard: if no JWT,
|
|
555
|
+
// Guard: if no JWT, try silent auto-setup first, then fall back to device flow.
|
|
460
556
|
if (!getJWT()) {
|
|
557
|
+
// ── Step 1: Silent instant-trial (zero-click) ────────────────────────
|
|
558
|
+
// On the very first tool call, attempt to silently provision a free trial.
|
|
559
|
+
// If it succeeds the user never sees an auth prompt — Brain is just active.
|
|
560
|
+
const provisioned = await silentAutoSetup();
|
|
561
|
+
if (provisioned) {
|
|
562
|
+
return handleTool(name, args); // Transparent re-entry with active credentials
|
|
563
|
+
}
|
|
461
564
|
void sendAnonymousTelemetry(name);
|
|
462
|
-
// ── Zero-credential device flow
|
|
463
|
-
// 1st call: start device flow, return code + URL
|
|
565
|
+
// ── Step 2: Zero-credential device flow ─────────────────────────────
|
|
566
|
+
// 1st call after silent setup fails: start device flow, return code + URL
|
|
464
567
|
// 2nd+ calls: poll for token; once authenticated, proceed transparently
|
|
465
568
|
if (_deviceFlow) {
|
|
466
569
|
const result = await pollDeviceFlow(_deviceFlow);
|
|
@@ -532,7 +635,7 @@ async function handleTool(name, args) {
|
|
|
532
635
|
args = { ...args, instance_id: defaultId };
|
|
533
636
|
}
|
|
534
637
|
// Delegate brain tools (learn, recall, session, etc.)
|
|
535
|
-
|
|
638
|
+
let brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
536
639
|
if (brainResult !== null) {
|
|
537
640
|
if (!_firstCallSuccessSent && getJWT()) {
|
|
538
641
|
_firstCallSuccessSent = true;
|
|
@@ -559,28 +662,39 @@ async function handleTool(name, args) {
|
|
|
559
662
|
// Existing brain — session_start acts as implicit recall → increment counter.
|
|
560
663
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
561
664
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
665
|
+
// Auto-bootstrap brain_from_git when workspace_path is provided and it hasn't run yet.
|
|
666
|
+
// We check a sentinel key in Valkey so this only fires once per workspace path,
|
|
667
|
+
// not just on the very first session (handles cases where brain was empty but
|
|
668
|
+
// session data existed, or where the user re-installed).
|
|
669
|
+
if (args.workspace_path) {
|
|
565
670
|
try {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
//
|
|
575
|
-
const
|
|
576
|
-
? gitBootstrap.content.map((c) => c.text ?? '').join('\n')
|
|
577
|
-
: String(gitBootstrap);
|
|
671
|
+
const workspacePath = args.workspace_path;
|
|
672
|
+
const autoKey = `cachly:brain_from_git:auto_done:${Buffer.from(workspacePath).toString('base64').slice(0, 40)}`;
|
|
673
|
+
const redis = await getConnection(instanceId);
|
|
674
|
+
const alreadyDone = await redis.get(autoKey);
|
|
675
|
+
if (!alreadyDone) {
|
|
676
|
+
// Mark immediately to prevent concurrent runs across multiple MCP tool calls
|
|
677
|
+
await redis.set(autoKey, '1', 'EX', 365 * 86400);
|
|
678
|
+
// Append a placeholder so the user gets immediate feedback, then bootstrap in background.
|
|
679
|
+
// Running brain_from_git synchronously would block session_start for 30-120s on large repos.
|
|
680
|
+
const bgMsg = '\n\n---\n\n🚀 **Auto-indexing your git history in the background** — your Brain will learn from recent commits automatically. No action needed.';
|
|
578
681
|
if (typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult) {
|
|
579
|
-
brainResult.content.push({
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
682
|
+
brainResult.content.push({ type: 'text', text: bgMsg });
|
|
683
|
+
}
|
|
684
|
+
else if (typeof brainResult === 'string') {
|
|
685
|
+
brainResult = brainResult + bgMsg;
|
|
583
686
|
}
|
|
687
|
+
// Fire-and-forget — never block the session_start response
|
|
688
|
+
void handleFedbrainTool('brain_from_git', {
|
|
689
|
+
instance_id: instanceId,
|
|
690
|
+
repo_path: workspacePath,
|
|
691
|
+
limit: 50,
|
|
692
|
+
}, getConnection, apiFetch).then((gitBootstrap) => {
|
|
693
|
+
if (gitBootstrap !== null) {
|
|
694
|
+
sendFunnelEvent('brain_from_git', telemetryExtra);
|
|
695
|
+
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
696
|
+
}
|
|
697
|
+
}).catch(() => { });
|
|
584
698
|
}
|
|
585
699
|
}
|
|
586
700
|
catch { /* git bootstrap errors must never break session_start */ }
|
|
@@ -683,27 +797,38 @@ async function handleTool(name, args) {
|
|
|
683
797
|
].join('\n');
|
|
684
798
|
}
|
|
685
799
|
// Decode JWT claims (inspection only, no verification)
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
|
|
695
|
-
const expired = expTs ? expTs < new Date() : false;
|
|
696
|
-
authInfo = [
|
|
697
|
-
`✅ JWT decoded`,
|
|
698
|
-
` Sub (user ID): ${sub}`,
|
|
699
|
-
` Provider: ${provider}`,
|
|
700
|
-
` Issuer: ${iss}`,
|
|
701
|
-
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
|
|
702
|
-
].join('\n');
|
|
703
|
-
}
|
|
800
|
+
const jwt = getJWT();
|
|
801
|
+
let authInfo;
|
|
802
|
+
// API keys (cky_live_... / cky_trial_...) are valid long-lived credentials.
|
|
803
|
+
// Only try to decode as JWT (3-part base64url) if it looks like one.
|
|
804
|
+
if (jwt.startsWith('cky_')) {
|
|
805
|
+
const keyType = jwt.startsWith('cky_trial_') ? 'Trial' : 'Live';
|
|
806
|
+
const keyId = jwt.slice(0, 20) + '…';
|
|
807
|
+
authInfo = `✅ API key (${keyType})\n Key prefix: ${keyId}`;
|
|
704
808
|
}
|
|
705
|
-
|
|
706
|
-
authInfo = '❌
|
|
809
|
+
else {
|
|
810
|
+
authInfo = '❌ invalid credential format';
|
|
811
|
+
try {
|
|
812
|
+
const parts = jwt.split('.');
|
|
813
|
+
if (parts.length === 3) {
|
|
814
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
815
|
+
const sub = payload.sub ?? '(unknown)';
|
|
816
|
+
const iss = payload.iss ?? '(unknown)';
|
|
817
|
+
const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
|
|
818
|
+
const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
|
|
819
|
+
const expired = expTs ? expTs < new Date() : false;
|
|
820
|
+
authInfo = [
|
|
821
|
+
`✅ JWT decoded`,
|
|
822
|
+
` Sub (user ID): ${sub}`,
|
|
823
|
+
` Provider: ${provider}`,
|
|
824
|
+
` Issuer: ${iss}`,
|
|
825
|
+
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
|
|
826
|
+
].join('\n');
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
|
|
831
|
+
}
|
|
707
832
|
}
|
|
708
833
|
// List instances + connectivity
|
|
709
834
|
let instanceInfo = '';
|
|
@@ -845,13 +970,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
845
970
|
|| _defaultInstanceId
|
|
846
971
|
|| process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
847
972
|
if (instanceId) {
|
|
848
|
-
|
|
973
|
+
// Fire-and-forget — session management is non-critical and must never block tool calls.
|
|
974
|
+
// The session_start briefing (which includes brain_from_git) can take 30-120s on large
|
|
975
|
+
// repos; awaiting it here would freeze the user's first MCP tool call entirely.
|
|
976
|
+
void autoStartSession(instanceId);
|
|
849
977
|
}
|
|
850
978
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
await _autoSessionStarting.catch(() => undefined);
|
|
854
|
-
}
|
|
979
|
+
// Note: we intentionally do NOT await _autoSessionStarting for any call.
|
|
980
|
+
// The session tracker runs in background — tool calls must always proceed immediately.
|
|
855
981
|
if (!sessionTools.has(name))
|
|
856
982
|
_autoSessionToolCount++;
|
|
857
983
|
try {
|
|
@@ -1849,7 +1975,7 @@ if (process.argv[2] === 'status') {
|
|
|
1849
1975
|
if (process.argv[2] === 'health') {
|
|
1850
1976
|
const { existsSync } = await import('node:fs');
|
|
1851
1977
|
const { readFile } = await import('node:fs/promises');
|
|
1852
|
-
const { resolve
|
|
1978
|
+
const { resolve } = await import('node:path');
|
|
1853
1979
|
const cwd = process.cwd();
|
|
1854
1980
|
let passed = 0;
|
|
1855
1981
|
let failed = 0;
|
|
@@ -1863,6 +1989,10 @@ if (process.argv[2] === 'health') {
|
|
|
1863
1989
|
if (!jwt) {
|
|
1864
1990
|
fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
|
|
1865
1991
|
}
|
|
1992
|
+
else if (jwt.startsWith('cky_')) {
|
|
1993
|
+
// Opaque cachly API key — no JWT expiry to check, format is valid by prefix
|
|
1994
|
+
ok('API key set (cky_*** opaque key)');
|
|
1995
|
+
}
|
|
1866
1996
|
else {
|
|
1867
1997
|
try {
|
|
1868
1998
|
const parts = jwt.split('.');
|
|
@@ -1882,7 +2012,7 @@ if (process.argv[2] === 'health') {
|
|
|
1882
2012
|
}
|
|
1883
2013
|
}
|
|
1884
2014
|
catch {
|
|
1885
|
-
fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
|
|
2015
|
+
fail('CACHLY_JWT format invalid (expected a cky_*** API key or a JWT with 3 parts)');
|
|
1886
2016
|
}
|
|
1887
2017
|
}
|
|
1888
2018
|
// ── 2. Brain API reachability ───────────────────────────────────────────────
|
|
@@ -1933,11 +2063,27 @@ if (process.argv[2] === 'health') {
|
|
|
1933
2063
|
});
|
|
1934
2064
|
if (connRes.ok) {
|
|
1935
2065
|
const conn = await connRes.json();
|
|
1936
|
-
|
|
1937
|
-
|
|
2066
|
+
// password may be a direct field, or embedded in connection_string as redis://:pass@host:port
|
|
2067
|
+
if (conn.password) {
|
|
2068
|
+
pingPassword = conn.password;
|
|
2069
|
+
}
|
|
2070
|
+
else if (conn.connection_string) {
|
|
2071
|
+
try {
|
|
2072
|
+
const u = new URL(conn.connection_string);
|
|
2073
|
+
if (u.password)
|
|
2074
|
+
pingPassword = decodeURIComponent(u.password);
|
|
2075
|
+
}
|
|
2076
|
+
catch { /* leave pingPassword undefined */ }
|
|
2077
|
+
}
|
|
2078
|
+
pingTls = conn.tls_enabled === true;
|
|
1938
2079
|
}
|
|
2080
|
+
else {
|
|
2081
|
+
warn(`Could not fetch Redis credentials (HTTP ${connRes.status}) — connectivity test may fail`);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
catch (connErr) {
|
|
2085
|
+
warn(`Could not fetch Redis credentials — ${connErr.message}`);
|
|
1939
2086
|
}
|
|
1940
|
-
catch { /* proceed without password */ }
|
|
1941
2087
|
const pingClient = new Redis({
|
|
1942
2088
|
host: inst.host, port: inst.port,
|
|
1943
2089
|
password: pingPassword,
|
|
@@ -2168,7 +2314,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2168
2314
|
token = keyBody.key; // swap JWT → cky_live_...
|
|
2169
2315
|
console.log(' ✓\n');
|
|
2170
2316
|
}
|
|
2171
|
-
catch
|
|
2317
|
+
catch {
|
|
2172
2318
|
console.log(' (skipped)\n');
|
|
2173
2319
|
// Non-fatal: fall back to using the Keycloak JWT directly.
|
|
2174
2320
|
// It will expire but setup still works for now.
|
|
@@ -2563,7 +2709,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2563
2709
|
limit: maxCommits,
|
|
2564
2710
|
});
|
|
2565
2711
|
console.log(result);
|
|
2566
|
-
sendFunnelEvent('brain_from_git', {
|
|
2712
|
+
sendFunnelEvent('brain_from_git', { instance_id: instanceId });
|
|
2567
2713
|
console.log('\n✅ Brain learned from your commits.\n');
|
|
2568
2714
|
// Let fire-and-forget telemetry flush before exit.
|
|
2569
2715
|
await new Promise(r => setTimeout(r, 300));
|