@cachly-dev/mcp-server 0.9.7 → 0.9.8

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/glama.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": [
4
+ "HeinrichNebula"
5
+ ]
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "mcpName": "io.github.cachly-dev/cachly-mcp",
5
5
  "description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 63 MCP tools · Claude Code · Cursor · GitHub Copilot · Windsurf · Free tier · EU servers.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.cachly-dev/cachly-mcp",
4
- "description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 63 MCP tools · Free tier · EU servers.",
4
+ "description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 67 MCP tools · Free tier · EU servers.",
5
5
  "repository": {
6
6
  "url": "https://github.com/cachly-dev/cachly-mcp",
7
7
  "source": "github"
package/src/index.ts CHANGED
@@ -55,7 +55,7 @@ import { Redis } from 'ioredis';
55
55
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
56
56
  let JWT = process.env.CACHLY_JWT ?? '';
57
57
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
58
- const CURRENT_VERSION = '0.9.7';
58
+ const CURRENT_VERSION = '0.9.8';
59
59
 
60
60
  // ── Default Instance Resolution (for Smithery & single-credential setups) ────
61
61
  // When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
@@ -1960,18 +1960,20 @@ async function keywordSearch(
1960
1960
  query: string,
1961
1961
  topK = 10,
1962
1962
  ): Promise<KeywordMatch[]> {
1963
- // ── Step 1: Collect and tokenize all documents ──
1964
- const allKeys: string[] = [];
1965
- for (const pattern of patterns) {
1966
- const stream = redis.scanStream({ match: pattern, count: 200 });
1967
- await new Promise<void>((resolve, reject) => {
1963
+ // ── Step 1: Collect all matching keys — patterns scanned in parallel ──
1964
+ const keyBatches = await Promise.all(patterns.map(pattern => {
1965
+ const keys: string[] = [];
1966
+ const stream = redis.scanStream({ match: pattern, count: 500 });
1967
+ return new Promise<string[]>((resolve, reject) => {
1968
1968
  stream.on('data', (batch: string[]) => {
1969
- allKeys.push(...batch.filter((k: string) => !k.endsWith(':meta')));
1969
+ keys.push(...batch.filter((k: string) => !k.endsWith(':meta')));
1970
1970
  });
1971
- stream.on('end', resolve);
1971
+ stream.on('end', () => resolve(keys));
1972
1972
  stream.on('error', reject);
1973
1973
  });
1974
- }
1974
+ }));
1975
+ // Deduplicate across patterns (a key may match multiple globs)
1976
+ const allKeys = [...new Set(keyBatches.flat())];
1975
1977
 
1976
1978
  if (allKeys.length === 0) return [];
1977
1979
 
@@ -2285,15 +2287,37 @@ async function getConnection(instance_id: string): Promise<Redis> {
2285
2287
 
2286
2288
  // ── API helper ────────────────────────────────────────────────────────────────
2287
2289
 
2290
+ function jwtExpiryMs(token: string): number | null {
2291
+ try {
2292
+ const parts = token.split('.');
2293
+ if (parts.length !== 3) return null;
2294
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as { exp?: number };
2295
+ return payload.exp ? payload.exp * 1000 : null;
2296
+ } catch { return null; }
2297
+ }
2298
+
2288
2299
  async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
2289
2300
  if (!JWT) {
2290
2301
  throw new McpError(
2291
2302
  ErrorCode.InvalidRequest,
2292
- 'CACHLY_JWT env var not set. Get your API token from https://cachly.dev/settings'
2303
+ 'CACHLY_JWT env var not set.\n\nGet your token at https://cachly.dev/setup-ai and add it to your MCP config:\n CACHLY_JWT=<your-token>'
2304
+ );
2305
+ }
2306
+
2307
+ // Detect token expiry before making the network call — gives a clearer error
2308
+ const expMs = jwtExpiryMs(JWT);
2309
+ if (expMs !== null && expMs < Date.now()) {
2310
+ const expiredAt = new Date(expMs).toISOString();
2311
+ throw new McpError(
2312
+ ErrorCode.InvalidRequest,
2313
+ `CACHLY_JWT expired at ${expiredAt}.\n\nGet a fresh token at https://cachly.dev/setup-ai and update CACHLY_JWT in your MCP config.`
2293
2314
  );
2294
2315
  }
2316
+
2317
+ const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
2295
2318
  const res = await fetch(`${API_URL}${path}`, {
2296
2319
  ...options,
2320
+ signal: AbortSignal.timeout(timeoutMs),
2297
2321
  headers: {
2298
2322
  'Content-Type': 'application/json',
2299
2323
  Authorization: `Bearer ${JWT}`,
@@ -2302,9 +2326,22 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
2302
2326
  });
2303
2327
  if (!res.ok) {
2304
2328
  const body = await res.json().catch(() => ({ error: res.statusText }));
2329
+ const detail = (body as { error?: string }).error ?? res.statusText;
2330
+ if (res.status === 401) {
2331
+ throw new McpError(
2332
+ ErrorCode.InvalidRequest,
2333
+ `Authentication failed (401): ${detail}\n\nYour CACHLY_JWT may be expired or invalid. Get a fresh token at https://cachly.dev/setup-ai`
2334
+ );
2335
+ }
2336
+ if (res.status === 403) {
2337
+ throw new McpError(
2338
+ ErrorCode.InvalidRequest,
2339
+ `Access denied (403): ${detail}\n\nCheck that your CACHLY_JWT belongs to an account with access to this resource.`
2340
+ );
2341
+ }
2305
2342
  throw new McpError(
2306
2343
  ErrorCode.InternalError,
2307
- `cachly API error ${res.status}: ${(body as { error?: string }).error ?? res.statusText}`
2344
+ `cachly API error ${res.status}: ${detail}`
2308
2345
  );
2309
2346
  }
2310
2347
  if (res.status === 204) return undefined as T;
@@ -10297,6 +10334,23 @@ if (!JWT) {
10297
10334
  '╚══════════════════════════════════════════════════════════════════╝\n' +
10298
10335
  '\n',
10299
10336
  );
10337
+ } else {
10338
+ // Warn if the JWT is already expired or expiring within the hour.
10339
+ const expMs = jwtExpiryMs(JWT);
10340
+ if (expMs !== null) {
10341
+ const minsLeft = Math.floor((expMs - Date.now()) / 60_000);
10342
+ if (minsLeft <= 0) {
10343
+ process.stderr.write(
10344
+ `\n⚠️ cachly: CACHLY_JWT expired ${Math.abs(minsLeft)} minute(s) ago.\n` +
10345
+ ` All tool calls will fail. Get a fresh token at https://cachly.dev/setup-ai\n\n`,
10346
+ );
10347
+ } else if (minsLeft < 60) {
10348
+ process.stderr.write(
10349
+ `\n⚠️ cachly: CACHLY_JWT expires in ${minsLeft} minute(s).\n` +
10350
+ ` Refresh it soon at https://cachly.dev/setup-ai to avoid interruptions.\n\n`,
10351
+ );
10352
+ }
10353
+ }
10300
10354
  }
10301
10355
 
10302
10356
  // ── Update nudge (non-blocking, fire-and-forget) ─────────────────────────────
package/test-features.cjs DELETED
@@ -1,23 +0,0 @@
1
- const fs = require('fs');
2
-
3
- const src = fs.readFileSync('src/index.ts', 'utf8');
4
- const initSrc = fs.readFileSync('../init/index.js', 'utf8');
5
- const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
6
-
7
- let pass = 0, fail = 0;
8
- function check(name, cond) {
9
- if (cond) { console.log('PASS:', name); pass++; }
10
- else { console.log('FAIL:', name); fail++; }
11
- }
12
-
13
- check('Test 1 - Ambient git learning in session_end', src.includes('git log --since') || src.includes('Ambient git'));
14
- check('Test 2a - author param in learn_from_attempts schema', src.includes('"author"') && src.includes('learn_from_attempts'));
15
- check('Test 2b - Team Telepathy in session_start', src.includes('Team Telepathy') || src.includes('team telepathy'));
16
- check('Test 3 - memory_crystalize tool', src.includes('memory_crystalize'));
17
- check('Test 4 - init team detection (~/.cachly-teams.json)', initSrc.includes('cachly-teams.json'));
18
- check('Test 5 - brain_doctor workspace_path param', src.includes('workspace_path') && src.includes('brain_doctor'));
19
- check('Bugfix - Pre-Warning uses splice(2,0)', src.includes('splice(2, 0'));
20
- check('Bugfix - No broken unshift in pre-warning', !src.includes('lines.unshift('));
21
- check('MCP version is 0.5.26', pkg.version === '0.5.26');
22
-
23
- console.log(`\n${pass}/${pass+fail} tests passed`);