From 057ff4755a61c3953d996bd038f32a2bf5da7bf2 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Mon, 24 Aug 2026 12:34:13 +0530 Subject: [PATCH 1/3] feat: add organization switching --- README.md | 12 ++- commands/supermemory-setup.md | 6 +- commands/supermemory-switch-org.md | 16 ++++ src/auth.test.ts | 71 ++++++++++++++++ src/auth.ts | 125 +++++++++++++++++++++++++---- src/cli.ts | 92 +++++++++++++++++++-- src/config.ts | 22 +++++ 7 files changed, 319 insertions(+), 25 deletions(-) create mode 100644 commands/supermemory-switch-org.md create mode 100644 src/auth.test.ts diff --git a/README.md b/README.md index 70ca4ca..b486cfb 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,16 @@ If you only need to authenticate an existing install, run: bunx cursor-supermemory@latest login ``` +To connect Cursor to a different Supermemory organization, run: + +```bash +bunx cursor-supermemory@latest switch-org +``` + +Choose the organization in your browser, then restart Cursor or run +**Developer: Reload Window**. A failed or cancelled switch keeps the previously +saved browser credential. + ## What it does - **Session hooks** — injects relevant memories at session start; saves conversation highlights at session end @@ -53,7 +63,7 @@ explicit project knowledge when an agent requests one scope. | Variable | Description | |---|---| -| `SUPERMEMORY_API_KEY` | API key (overrides all other sources) | +| `SUPERMEMORY_API_KEY` | API key (overrides browser-selected organizations and all other sources) | | `SUPERMEMORY_API_URL` | Override the Supermemory API base URL | | `SUPERMEMORY_REPO_TAG` | Override the unified repository container tag | | `SUPERMEMORY_USER_TAG` | Legacy Cursor personal container to continue reading | diff --git a/commands/supermemory-setup.md b/commands/supermemory-setup.md index 37b22d6..deaa3ed 100644 --- a/commands/supermemory-setup.md +++ b/commands/supermemory-setup.md @@ -11,4 +11,8 @@ bunx cursor-supermemory@latest login This opens your browser to connect your Supermemory account to Cursor. Once connected, the AI will have persistent memory across all your coding sessions. -If the browser doesn't open automatically, visit: https://console.supermemory.ai/auth/connect?client=cursor +To choose a different organization later, run: + +```bash +bunx cursor-supermemory@latest switch-org +``` diff --git a/commands/supermemory-switch-org.md b/commands/supermemory-switch-org.md new file mode 100644 index 0000000..d68bda1 --- /dev/null +++ b/commands/supermemory-switch-org.md @@ -0,0 +1,16 @@ +--- +name: supermemory-switch-org +description: Choose which Supermemory organization Cursor should use +--- + +Run the following command in the terminal: + +```bash +bunx cursor-supermemory@latest switch-org +``` + +This opens Supermemory in your browser, where you can choose an organization. +Your existing saved credential is kept if authentication is cancelled or fails. + +After a successful switch, restart Cursor or run **Developer: Reload Window** +so the MCP server uses the new organization. diff --git a/src/auth.test.ts b/src/auth.test.ts new file mode 100644 index 0000000..0b04510 --- /dev/null +++ b/src/auth.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "bun:test"; +import { parseAuthCallback, verifyApiKey } from "./auth.ts"; + +describe("parseAuthCallback", () => { + test("accepts a state-bound plugin credential", () => { + const url = new URL( + "http://127.0.0.1/callback?state=expected&apikey=sm_org_secret", + ); + expect(parseAuthCallback(url, "expected")).toBe("sm_org_secret"); + }); + + test("rejects a callback with the wrong state", () => { + const url = new URL( + "http://127.0.0.1/callback?state=wrong&apikey=sm_org_secret", + ); + expect(() => parseAuthCallback(url, "expected")).toThrow( + "Invalid callback state", + ); + }); + + test("rejects a callback without a plugin credential", () => { + const url = new URL( + "http://127.0.0.1/callback?state=expected&apikey=invalid", + ); + expect(() => parseAuthCallback(url, "expected")).toThrow( + "Invalid API key", + ); + }); +}); + +describe("verifyApiKey", () => { + test("returns the organization bound to the candidate credential", async () => { + const identity = await verifyApiKey( + "sm_org_secret", + "https://api.supermemory.ai/", + (async (url, init) => { + expect(url).toBe("https://api.supermemory.ai/v3/session"); + expect(init?.headers).toEqual({ + Authorization: "Bearer sm_org_secret", + "x-sm-source": "cursor", + }); + return new Response( + JSON.stringify({ + org: { id: "org_1", name: "Engineering" }, + user: { email: "person@example.com" }, + }), + { status: 200 }, + ); + }) as typeof fetch, + ); + + expect(identity).toEqual({ + organizationId: "org_1", + organizationName: "Engineering", + userEmail: "person@example.com", + }); + }); + + test("rejects a candidate credential without an organization", async () => { + await expect( + verifyApiKey( + "sm_unknown_secret", + "https://api.supermemory.ai", + (async () => + new Response(JSON.stringify({ user: { id: "user_1" } }), { + status: 200, + })) as unknown as typeof fetch, + ), + ).rejects.toThrow("did not resolve to an organization"); + }); +}); diff --git a/src/auth.ts b/src/auth.ts index e854081..ebab07c 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,11 +1,13 @@ import path from "node:path"; import os from "node:os"; import fs from "node:fs"; +import { randomBytes } from "node:crypto"; const CREDENTIALS_DIR = path.join(os.homedir(), ".supermemory-cursor"); const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json"); -const AUTH_PORT = 19878; -const AUTH_URL = "https://console.supermemory.ai/auth/connect"; +const AUTH_URL = "https://app.supermemory.ai/auth/connect"; +export const DEFAULT_API_URL = "https://api.supermemory.ai"; +const SESSION_TIMEOUT_MS = 10_000; const SUCCESS_HTML = `