vplex-memory 2.4.1 → 2.4.3

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.0"]
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.0"]
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.0"]
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.0"]
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.0"]
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.1",
3
+ "version": "2.4.3",
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
 
@@ -682,6 +766,11 @@ const TOOLS = [
682
766
  description: "Check authentication status: whether logged in, token expiry, and user info.",
683
767
  inputSchema: { type: "object", properties: {} },
684
768
  },
769
+ {
770
+ name: "memory_signin",
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.",
772
+ inputSchema: { type: "object", properties: {} },
773
+ },
685
774
  {
686
775
  name: "memory_logout",
687
776
  description: "Log out by deleting the cached session. Next tool call will require re-authentication.",
@@ -1147,6 +1236,24 @@ async function handleToolCall(name, args) {
1147
1236
  };
1148
1237
  }
1149
1238
 
1239
+ case "memory_signin": {
1240
+ // Check if already authenticated
1241
+ const existingToken = await getToken();
1242
+ if (existingToken) {
1243
+ const session = readSession();
1244
+ const user = session?.user?.email || session?.user?.name || "unknown";
1245
+ return { content: [{ type: "text", text: `Already signed in as ${user}. Use memory_logout first to switch accounts.` }] };
1246
+ }
1247
+ // Start the CLI auth flow with auto-approve URL
1248
+ const authMessage = await startCliAuthFlow();
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
+ };
1255
+ }
1256
+
1150
1257
  case "memory_auth_status": {
1151
1258
  const session = readSession();
1152
1259
  if (!session?.token) {
@@ -1200,17 +1307,38 @@ async function handleRequest(request) {
1200
1307
 
1201
1308
  // MCP notifications (no id, or notifications/* prefix) — never respond
1202
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
+ }
1203
1317
  return null;
1204
1318
  }
1205
1319
 
1206
1320
  try {
1207
1321
  switch (method) {
1208
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
+ }
1209
1337
  return {
1210
1338
  jsonrpc: "2.0", id,
1211
1339
  result: {
1212
1340
  protocolVersion: "2024-11-05",
1213
- capabilities: { tools: {} },
1341
+ capabilities: { tools: {}, roots: { listChanged: false } },
1214
1342
  serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
1215
1343
  },
1216
1344
  };