vplex-memory 2.4.2 → 2.4.4

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/README.md CHANGED
@@ -11,7 +11,7 @@ Works with Claude Code, Cursor, VS Code Copilot, Windsurf, Codex, and any MCP-co
11
11
  "mcpServers": {
12
12
  "vplex-memory": {
13
13
  "command": "npx",
14
- "args": ["-y", "vplex-memory@2.4.1"]
14
+ "args": ["-y", "vplex-memory@2.4.2"]
15
15
  }
16
16
  }
17
17
  }
@@ -30,7 +30,7 @@ Add to `.mcp.json` in your project root or `~/.claude/settings.json` globally:
30
30
  "mcpServers": {
31
31
  "vplex-memory": {
32
32
  "command": "npx",
33
- "args": ["-y", "vplex-memory@2.4.1"]
33
+ "args": ["-y", "vplex-memory@2.4.2"]
34
34
  }
35
35
  }
36
36
  }
@@ -46,7 +46,7 @@ Add to `.cursor/mcp.json`:
46
46
  "vplex-memory": {
47
47
  "type": "stdio",
48
48
  "command": "npx",
49
- "args": ["-y", "vplex-memory@2.4.1"]
49
+ "args": ["-y", "vplex-memory@2.4.2"]
50
50
  }
51
51
  }
52
52
  }
@@ -62,7 +62,7 @@ Add to `.vscode/mcp.json`:
62
62
  "vplex-memory": {
63
63
  "type": "stdio",
64
64
  "command": "npx",
65
- "args": ["-y", "vplex-memory@2.4.1"]
65
+ "args": ["-y", "vplex-memory@2.4.2"]
66
66
  }
67
67
  }
68
68
  }
@@ -78,7 +78,7 @@ Add to `~/.windsurf/mcp.json`:
78
78
  "vplex-memory": {
79
79
  "type": "stdio",
80
80
  "command": "npx",
81
- "args": ["-y", "vplex-memory@2.4.1"]
81
+ "args": ["-y", "vplex-memory@2.4.2"]
82
82
  }
83
83
  }
84
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vplex-memory",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "VPLEX Memory MCP Server — persistent cross-session memory for AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -209,7 +209,7 @@ const TOKEN_CACHE_MS = 30_000; // re-read from disk every 30s
209
209
  const TOKEN_REFRESH_MARGIN_S = 120; // refresh if < 2 min until expiry
210
210
 
211
211
  // CLI auth flow state
212
- let cliAuthPending = null; // { sessionId, userCode, verificationUrl, pollInterval, expiresAt }
212
+ let cliAuthPending = null; // { sessionId, userCode, autoApproveUrl, manualUrl, pollInterval, expiresAt }
213
213
 
214
214
  function getSessionPath() {
215
215
  return join(homedir(), ".vplex", "session.json");
@@ -373,44 +373,40 @@ function startCliAuthPolling(sessionId) {
373
373
 
374
374
  /**
375
375
  * Starts CLI auth flow if no token is available.
376
- * Returns an error message with the auth URL and code for the user.
376
+ * Uses the auto-approve page if the user is logged in via browser cookie,
377
+ * the session is approved automatically (no manual code entry needed).
378
+ * Falls back to manual code entry URL if auto-approve is not possible.
377
379
  */
378
380
  async function startCliAuthFlow() {
379
381
  // Don't start if already pending
380
382
  if (cliAuthPending && Date.now() < cliAuthPending.expiresAt) {
381
- return `Not authenticated. Open ${cliAuthPending.verificationUrl} and enter code: ${cliAuthPending.userCode}`;
383
+ return `Not authenticated. Open this URL to sign in:\n${cliAuthPending.autoApproveUrl}\n\nIf that doesn't work, open ${cliAuthPending.manualUrl} and enter code: ${cliAuthPending.userCode}`;
382
384
  }
383
385
 
384
386
  const result = await initCliAuth();
385
387
  if (!result?.cli_session_id) {
386
- return "Not authenticated. Please log in via VPLEX Desktop App.";
388
+ return `Not authenticated. Auth service unavailable — please try again in a moment, or sign in at ${WEB_URL}/auth/signin manually.`;
387
389
  }
388
390
 
389
391
  const pollInterval = startCliAuthPolling(result.cli_session_id);
390
392
 
393
+ const autoApproveUrl = `${WEB_URL}/auth/device-approve?session_id=${result.cli_session_id}`;
394
+ const manualUrl = result.verification_url || `${WEB_URL}/auth/cli`;
395
+
391
396
  cliAuthPending = {
392
397
  sessionId: result.cli_session_id,
393
398
  userCode: result.user_code,
394
- verificationUrl: result.verification_url || `${WEB_URL}/auth/cli`,
399
+ autoApproveUrl,
400
+ manualUrl,
395
401
  pollInterval,
396
402
  expiresAt: Date.now() + (result.expires_in || 600) * 1000,
397
403
  };
398
404
 
399
- return `Not authenticated. Open ${cliAuthPending.verificationUrl} and enter code: ${cliAuthPending.userCode}`;
405
+ return `Not authenticated. Open this URL to sign in:\n${autoApproveUrl}\n\nIf that doesn't work, open ${manualUrl} and enter code: ${cliAuthPending.userCode}`;
400
406
  }
401
407
 
402
408
  // ── Project Identification ──────────────────────────────────────────
403
409
 
404
- function hashString(str) {
405
- let hash = 0;
406
- for (let i = 0; i < str.length; i++) {
407
- const char = str.charCodeAt(i);
408
- hash = ((hash << 5) - hash) + char;
409
- hash |= 0;
410
- }
411
- return Math.abs(hash).toString(36);
412
- }
413
-
414
410
  // Detect project root: env override > .git traversal > cwd fallback
415
411
  function detectProjectRoot() {
416
412
  // Explicit override via env (set in MCP config)
@@ -435,10 +431,98 @@ function detectProjectRoot() {
435
431
  return process.cwd();
436
432
  }
437
433
 
438
- const detectedRoot = detectProjectRoot();
439
- const projectHash = hashString(detectedRoot);
440
- const projectName = detectedRoot.replace(/\\/g, "/").split("/").pop() || detectedRoot;
441
- const projectPath = detectedRoot;
434
+ /**
435
+ * Read or create the project identifier from .vplex/project-id.
436
+ *
437
+ * This file is the single source of truth for project identity.
438
+ * It survives folder renames, git remote changes, moves, and re-clones
439
+ * (if committed to the repo).
440
+ *
441
+ * Priority:
442
+ * 1. .vplex/project-id file exists → read and use it
443
+ * 2. File doesn't exist → generate new ID, create .vplex/ dir + file
444
+ */
445
+ function resolveProjectId(projectRoot) {
446
+ const vplexDir = join(projectRoot, ".vplex");
447
+ const idFile = join(vplexDir, "project-id");
448
+
449
+ // Try to read existing ID
450
+ try {
451
+ const id = readFileSync(idFile, "utf-8").trim();
452
+ if (id.length >= 4 && id.length <= 64) {
453
+ return id;
454
+ }
455
+ // Invalid content — regenerate
456
+ } catch {
457
+ // File doesn't exist — create it
458
+ }
459
+
460
+ // Generate new project ID
461
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
462
+ let id = "";
463
+ for (let i = 0; i < 12; i++) {
464
+ id += chars[Math.floor(Math.random() * chars.length)];
465
+ }
466
+
467
+ try {
468
+ mkdirSync(vplexDir, { recursive: true });
469
+ writeFileSync(idFile, id + "\n", { encoding: "utf-8" });
470
+ process.stderr.write(`[vplex-mcp] Created .vplex/project-id: ${id}\n`);
471
+ } catch (err) {
472
+ process.stderr.write(`[vplex-mcp] Warning: could not write .vplex/project-id: ${err.message}\n`);
473
+ }
474
+
475
+ return id;
476
+ }
477
+
478
+ let detectedRoot = detectProjectRoot();
479
+ let projectHash = resolveProjectId(detectedRoot);
480
+ let projectName = detectedRoot.replace(/\\/g, "/").split("/").pop() || detectedRoot;
481
+ let projectPath = detectedRoot;
482
+ process.stderr.write(`[vplex-mcp] Project: ${projectName} | id: ${projectHash} | root: ${detectedRoot}\n`);
483
+
484
+ /**
485
+ * Re-detect project from a new root path (e.g. from MCP client roots).
486
+ * Walks up from the given path looking for .git, then resolves project ID.
487
+ */
488
+ function reinitProject(rootUri) {
489
+ let newRoot = rootUri;
490
+ // Convert file:// URI to path
491
+ if (newRoot.startsWith("file:///")) {
492
+ newRoot = newRoot.slice(8); // file:///C:/foo → C:/foo
493
+ // On Windows, file:///C:/foo → C:/foo (keep drive letter)
494
+ // On Unix, file:///home/user → /home/user
495
+ if (!/^\//.test(newRoot) && /^[a-zA-Z]:/.test(newRoot)) {
496
+ // Windows path — already correct
497
+ } else if (!newRoot.startsWith("/")) {
498
+ newRoot = "/" + newRoot;
499
+ }
500
+ } else if (newRoot.startsWith("file://")) {
501
+ newRoot = newRoot.slice(7);
502
+ }
503
+ // Decode URI components (spaces, special chars)
504
+ newRoot = decodeURIComponent(newRoot);
505
+
506
+ // Walk up from new root looking for .git
507
+ let dir = newRoot;
508
+ const rootDir = parse(dir).root;
509
+ while (dir !== rootDir) {
510
+ try {
511
+ statSync(join(dir, ".git"));
512
+ newRoot = dir;
513
+ break;
514
+ } catch { /* continue */ }
515
+ const parent = dirname(dir);
516
+ if (parent === dir) break;
517
+ dir = parent;
518
+ }
519
+
520
+ detectedRoot = newRoot;
521
+ projectHash = resolveProjectId(detectedRoot);
522
+ projectName = detectedRoot.replace(/\\/g, "/").split("/").pop() || detectedRoot;
523
+ projectPath = detectedRoot;
524
+ process.stderr.write(`[vplex-mcp] Project updated: ${projectName} | id: ${projectHash} | root: ${detectedRoot}\n`);
525
+ }
442
526
 
443
527
  // ── HTTP Helper ─────────────────────────────────────────────────────
444
528
 
@@ -684,7 +768,7 @@ const TOOLS = [
684
768
  },
685
769
  {
686
770
  name: "memory_signin",
687
- description: "Sign in to VPLEX Memory. Starts the browser-based auth flow and returns a URL + code to enter.",
771
+ description: "Sign in to VPLEX Memory. Opens the browser-based auth flow if already logged in via browser, approval is automatic (no code needed). Returns a URL to open.",
688
772
  inputSchema: { type: "object", properties: {} },
689
773
  },
690
774
  {
@@ -1160,9 +1244,14 @@ async function handleToolCall(name, args) {
1160
1244
  const user = session?.user?.email || session?.user?.name || "unknown";
1161
1245
  return { content: [{ type: "text", text: `Already signed in as ${user}. Use memory_logout first to switch accounts.` }] };
1162
1246
  }
1163
- // Start the CLI auth flow
1247
+ // Start the CLI auth flow with auto-approve URL
1164
1248
  const authMessage = await startCliAuthFlow();
1165
- return { content: [{ type: "text", text: authMessage }] };
1249
+ return {
1250
+ content: [{
1251
+ type: "text",
1252
+ text: `${authMessage}\n\nOnce you sign in via the browser, this session will be automatically authenticated. The polling runs in the background — just try any memory tool again after signing in.`,
1253
+ }],
1254
+ };
1166
1255
  }
1167
1256
 
1168
1257
  case "memory_auth_status": {
@@ -1218,22 +1307,45 @@ async function handleRequest(request) {
1218
1307
 
1219
1308
  // MCP notifications (no id, or notifications/* prefix) — never respond
1220
1309
  if (id === undefined || id === null || method.startsWith("notifications/")) {
1310
+ // Handle roots/list_changed — client workspace changed
1311
+ if (method === "notifications/roots/list_changed" && params?.roots?.length > 0) {
1312
+ const rootUri = params.roots[0].uri || params.roots[0];
1313
+ if (typeof rootUri === "string" && rootUri.length > 0) {
1314
+ reinitProject(rootUri);
1315
+ }
1316
+ }
1221
1317
  return null;
1222
1318
  }
1223
1319
 
1224
1320
  try {
1225
1321
  switch (method) {
1226
1322
  case "initialize":
1323
+ // Read workspace roots from client (e.g. VS Code workspace folders)
1324
+ if (params?.roots?.length > 0) {
1325
+ const rootUri = params.roots[0].uri || params.roots[0];
1326
+ process.stderr.write(`[vplex-mcp] Client root: ${typeof rootUri === "string" ? rootUri : JSON.stringify(rootUri)}\n`);
1327
+ if (typeof rootUri === "string" && rootUri.length > 0) {
1328
+ reinitProject(rootUri);
1329
+ }
1330
+ } else if (params?.workspaceFolders?.length > 0) {
1331
+ // Some clients send workspaceFolders instead of roots
1332
+ const folder = params.workspaceFolders[0].uri || params.workspaceFolders[0];
1333
+ if (typeof folder === "string" && folder.length > 0) {
1334
+ reinitProject(folder);
1335
+ }
1336
+ }
1227
1337
  return {
1228
1338
  jsonrpc: "2.0", id,
1229
1339
  result: {
1230
1340
  protocolVersion: "2024-11-05",
1231
- capabilities: { tools: {} },
1341
+ capabilities: { tools: {}, roots: { listChanged: false } },
1232
1342
  serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
1233
1343
  },
1234
1344
  };
1235
1345
 
1236
1346
  case "initialized":
1347
+ // After initialization, request workspace roots from the client
1348
+ requestRootsFromClient();
1237
1349
  return null;
1238
1350
 
1239
1351
  case "tools/list":
@@ -1257,6 +1369,38 @@ async function handleRequest(request) {
1257
1369
  }
1258
1370
  }
1259
1371
 
1372
+ // ── Server-to-Client Requests ───────────────────────────────────────
1373
+
1374
+ let nextRequestId = 1;
1375
+ const pendingRequests = new Map(); // id → { resolve, reject, timer }
1376
+
1377
+ function sendClientRequest(method, params = {}) {
1378
+ return new Promise((resolve, reject) => {
1379
+ const id = `srv-${nextRequestId++}`;
1380
+ const timer = setTimeout(() => {
1381
+ pendingRequests.delete(id);
1382
+ reject(new Error(`Client request ${method} timed out`));
1383
+ }, 5000);
1384
+ pendingRequests.set(id, { resolve, reject, timer });
1385
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
1386
+ });
1387
+ }
1388
+
1389
+ async function requestRootsFromClient() {
1390
+ try {
1391
+ const result = await sendClientRequest("roots/list");
1392
+ if (result?.roots?.length > 0) {
1393
+ const rootUri = result.roots[0].uri || result.roots[0];
1394
+ process.stderr.write(`[vplex-mcp] Got root from client: ${typeof rootUri === "string" ? rootUri : JSON.stringify(rootUri)}\n`);
1395
+ if (typeof rootUri === "string" && rootUri.length > 0) {
1396
+ reinitProject(rootUri);
1397
+ }
1398
+ }
1399
+ } catch (err) {
1400
+ process.stderr.write(`[vplex-mcp] roots/list not supported by client: ${err.message}\n`);
1401
+ }
1402
+ }
1403
+
1260
1404
  // ── stdio Communication Loop ────────────────────────────────────────
1261
1405
 
1262
1406
  const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
@@ -1265,8 +1409,18 @@ rl.on("line", async (line) => {
1265
1409
  if (!line.trim()) return;
1266
1410
 
1267
1411
  try {
1268
- const request = JSON.parse(line);
1269
- const response = await handleRequest(request);
1412
+ const msg = JSON.parse(line);
1413
+
1414
+ // Check if this is a response to a server-initiated request
1415
+ if (msg.id && pendingRequests.has(msg.id)) {
1416
+ const { resolve, timer } = pendingRequests.get(msg.id);
1417
+ clearTimeout(timer);
1418
+ pendingRequests.delete(msg.id);
1419
+ resolve(msg.result ?? null);
1420
+ return;
1421
+ }
1422
+
1423
+ const response = await handleRequest(msg);
1270
1424
  if (response !== null) {
1271
1425
  // Write to stdout only — stderr is reserved for diagnostics
1272
1426
  process.stdout.write(JSON.stringify(response) + "\n");