diff --git a/assets/cursor.png b/assets/cursor.png new file mode 100644 index 0000000..e256217 Binary files /dev/null and b/assets/cursor.png differ diff --git a/package.json b/package.json index 50c5574..2a838df 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@types/bun": "latest", "typescript": "^5" }, - "files": ["dist", "rules", "skills", "commands", "hooks", ".cursor-plugin", ".mcp.json"], + "files": ["dist", "rules", "skills", "commands", "hooks", ".cursor-plugin", ".mcp.json", "assets"], "keywords": ["cursor", "supermemory", "mcp", "memory", "ai", "plugin"], "license": "MIT", "repository": { diff --git a/src/auth.ts b/src/auth.ts index e854081..1a72b56 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,17 +1,137 @@ 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 = process.env.SUPERMEMORY_AUTH_URL || "https://app.supermemory.ai/auth/connect"; +const CURSOR_LOGO_FILE = path.join(import.meta.dir, "..", "assets", "cursor.png"); const SUCCESS_HTML = ` -

Connected to Cursor!

`; + + + Connected - Supermemory + + + + +
+
Connected
+
+
+ + +
+ +
+
+

Cursor is connected

+

Supermemory is ready to provide persistent context inside Cursor.

+
You can close this tab and return to Cursor.
+
+ +`; export function loadCredentials(): { apiKey: string; createdAt: string } | null { try { @@ -47,16 +167,31 @@ export async function startAuthFlow( ): Promise<{ success: boolean; apiKey?: string; error?: string }> { return new Promise((resolve) => { let settled = false; + const stateToken = randomBytes(16).toString("hex"); const server = Bun.serve({ - port: AUTH_PORT, + port: 0, hostname: "127.0.0.1", fetch(req) { const url = new URL(req.url); + + if (url.pathname === "/cursor.png") { + return new Response(Bun.file(CURSOR_LOGO_FILE), { + headers: { + "Content-Type": "image/png", + "Cache-Control": "no-store", + }, + }); + } + if (url.pathname !== "/callback") { return new Response("Not found", { status: 404 }); } + if (url.searchParams.get("state") !== stateToken) { + return new Response("Invalid state", { status: 403 }); + } + const apiKey = url.searchParams.get("apikey") || url.searchParams.get("api_key"); if (!apiKey?.startsWith("sm_")) { return new Response("Invalid API key", { status: 400 }); @@ -74,12 +209,13 @@ export async function startAuthFlow( }, }); - const callbackUrl = `http://localhost:${AUTH_PORT}/callback`; + const callbackUrl = `http://127.0.0.1:${server.port}/callback?state=${stateToken}`; const authUrl = `${AUTH_URL}?callback=${encodeURIComponent(callbackUrl)}&client=cursor`; process.stderr.write(`\nOpen this URL to connect Supermemory to Cursor:\n\n ${authUrl}\n\nWaiting...\n`); - const opener = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open"; - Bun.$`${opener} ${authUrl}`.quiet().nothrow(); + openUrl(authUrl).catch((error) => { + process.stderr.write(`Failed to open browser automatically: ${error.message}\n`); + }); const timer = setTimeout(() => { if (!settled) { @@ -89,3 +225,15 @@ export async function startAuthFlow( }, timeoutMs); }); } + +async function openUrl(url: string): Promise { + if (process.platform === "win32") { + const result = await Bun.spawn(["rundll32.exe", "url.dll,FileProtocolHandler", url]).exited; + if (result !== 0) throw new Error(`browser opener exited with code ${result}`); + return; + } + + const opener = process.platform === "darwin" ? "open" : "xdg-open"; + const result = await Bun.spawn([opener, url]).exited; + if (result !== 0) throw new Error(`browser opener exited with code ${result}`); +} diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts index 6ac5dbf..af501c9 100644 --- a/src/hooks/session-start.ts +++ b/src/hooks/session-start.ts @@ -48,7 +48,7 @@ async function main() { process.stdout.write(JSON.stringify({ continue: true, hookSpecificOutput: { - hookEventName: "sessionStart", + hookEventName: "SessionStart", additionalContext: context, }, }));