diff --git a/src/app/api/oauth/cursor/auto-import/route.js b/src/app/api/oauth/cursor/auto-import/route.js
index 29ed8d6c..91f9fb65 100644
--- a/src/app/api/oauth/cursor/auto-import/route.js
+++ b/src/app/api/oauth/cursor/auto-import/route.js
@@ -2,7 +2,11 @@ import { NextResponse } from "next/server";
import { access, constants } from "fs/promises";
import { homedir } from "os";
import { join } from "path";
-import Database from "better-sqlite3";
+import { execFile, execSync } from "child_process";
+import { promisify } from "util";
+import { createRequire } from "module";
+
+const execFileAsync = promisify(execFile);
const ACCESS_TOKEN_KEYS = ["cursorAuth/accessToken", "cursorAuth/token"];
const MACHINE_ID_KEYS = ["storage.serviceMachineId", "storage.machineId", "telemetry.machineId"];
@@ -29,15 +33,14 @@ function getCandidatePaths(platform) {
];
}
- // Linux
return [
join(home, ".config/Cursor/User/globalStorage/state.vscdb"),
join(home, ".config/cursor/User/globalStorage/state.vscdb"),
];
}
-/** Extract tokens from open db, with fuzzy fallback */
-function extractTokens(db, platform) {
+/** Extract tokens using better-sqlite3 (stream-based, no RAM limit) */
+function extractTokens(db) {
const desiredKeys = [...ACCESS_TOKEN_KEYS, ...MACHINE_ID_KEYS];
const rows = db.prepare(
`SELECT key, value FROM itemTable WHERE key IN (${desiredKeys.map(() => "?").join(",")})`
@@ -62,7 +65,7 @@ function extractTokens(db, platform) {
}
}
- // Fuzzy fallback for all platforms when exact keys miss
+ // Fuzzy fallback
if (!tokens.accessToken || !tokens.machineId) {
const fallbackRows = db.prepare(
"SELECT key, value FROM itemTable WHERE key LIKE '%cursorAuth/%' OR key LIKE '%machineId%' OR key LIKE '%serviceMachineId%'"
@@ -83,16 +86,56 @@ function extractTokens(db, platform) {
return tokens;
}
+/**
+ * Extract tokens via sqlite3 CLI (fallback for Windows when native addon fails)
+ * Queries each key individually and parses output
+ */
+async function extractTokensViaCLI(dbPath) {
+ const normalize = (raw) => {
+ const value = raw.trim();
+ try {
+ const parsed = JSON.parse(value);
+ return typeof parsed === "string" ? parsed : value;
+ } catch {
+ return value;
+ }
+ };
+
+ const query = async (sql) => {
+ const { stdout } = await execFileAsync("sqlite3", [dbPath, sql], { timeout: 10000 });
+ return stdout.trim();
+ };
+
+ // Try each key in priority order
+ let accessToken = null;
+ for (const key of ACCESS_TOKEN_KEYS) {
+ try {
+ const raw = await query(`SELECT value FROM itemTable WHERE key='${key}' LIMIT 1`);
+ if (raw) { accessToken = normalize(raw); break; }
+ } catch { /* try next */ }
+ }
+
+ let machineId = null;
+ for (const key of MACHINE_ID_KEYS) {
+ try {
+ const raw = await query(`SELECT value FROM itemTable WHERE key='${key}' LIMIT 1`);
+ if (raw) { machineId = normalize(raw); break; }
+ } catch { /* try next */ }
+ }
+
+ return { accessToken, machineId };
+}
+
/**
* GET /api/oauth/cursor/auto-import
- * Auto-detect and extract Cursor tokens from local SQLite database
+ * Auto-detect and extract Cursor tokens from local SQLite database.
+ * Strategy: better-sqlite3 (native, fast) → sqlite3 CLI (fallback) → windowsManual
*/
export async function GET() {
try {
const platform = process.platform;
const candidates = getCandidatePaths(platform);
- // Find first readable db path
let dbPath = null;
for (const candidate of candidates) {
try {
@@ -111,44 +154,47 @@ export async function GET() {
});
}
- let db;
+ // Strategy 1: better-sqlite3 bundled → then global install fallback
+ let Database = null;
try {
- db = new Database(dbPath, { readonly: true, fileMustExist: true });
- } catch (error) {
- return NextResponse.json({
- found: false,
- error: `Found Cursor database at:\n${dbPath}\n\nBut could not open it: ${error.message}`,
- });
+ const mod = await import("better-sqlite3");
+ Database = mod.default;
+ } catch {
+ // Try loading from global node_modules (user ran: npm i better-sqlite3 -g)
+ try {
+ const globalRoot = execSync("npm root -g", { timeout: 5000 }).toString().trim();
+ const requireGlobal = createRequire(join(globalRoot, "better-sqlite3", "package.json"));
+ Database = requireGlobal("better-sqlite3");
+ } catch { /* fall through to sqlite3 CLI strategy */ }
}
- try {
- const tokens = extractTokens(db, platform);
- db.close();
+ if (Database) {
+ let db;
+ try {
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
+ const tokens = extractTokens(db);
+ db.close();
- if (!tokens.accessToken || !tokens.machineId) {
- return NextResponse.json({
- found: false,
- error: "Tokens not found in database. Please login to Cursor IDE first.",
- });
+ if (tokens.accessToken && tokens.machineId) {
+ return NextResponse.json({ found: true, accessToken: tokens.accessToken, machineId: tokens.machineId });
+ }
+ } catch {
+ db?.close();
}
-
- return NextResponse.json({
- found: true,
- accessToken: tokens.accessToken,
- machineId: tokens.machineId,
- });
- } catch (error) {
- db?.close();
- return NextResponse.json({
- found: false,
- error: `Failed to read database: ${error.message}`,
- });
}
+
+ // Strategy 2: sqlite3 CLI (works on Windows if sqlite3 is installed)
+ try {
+ const tokens = await extractTokensViaCLI(dbPath);
+ if (tokens.accessToken && tokens.machineId) {
+ return NextResponse.json({ found: true, accessToken: tokens.accessToken, machineId: tokens.machineId });
+ }
+ } catch { /* sqlite3 CLI not available */ }
+
+ // Strategy 3: ask user to paste manually
+ return NextResponse.json({ found: false, windowsManual: true, dbPath });
} catch (error) {
console.log("Cursor auto-import error:", error);
- return NextResponse.json(
- { found: false, error: error.message },
- { status: 500 }
- );
+ return NextResponse.json({ found: false, error: error.message }, { status: 500 });
}
}
diff --git a/src/lib/oauth/providers.js b/src/lib/oauth/providers.js
index 214102f7..0765b587 100644
--- a/src/lib/oauth/providers.js
+++ b/src/lib/oauth/providers.js
@@ -761,7 +761,18 @@ const PROVIDERS = {
if (!response.ok) return { ok: false, data: { error: "poll_failed", error_description: `Poll failed: ${response.status}` } };
const data = await response.json();
if (data.status === "approved" && data.token) {
- return { ok: true, data: { access_token: data.token, _userEmail: data.userEmail } };
+ // Fetch profile to get orgId for X-Kilocode-OrganizationID header
+ let orgId = null;
+ try {
+ const profileRes = await fetch(`${config.apiBaseUrl}/api/profile`, {
+ headers: { "Authorization": `Bearer ${data.token}` }
+ });
+ if (profileRes.ok) {
+ const profile = await profileRes.json();
+ orgId = profile.organizations?.[0]?.id || null;
+ }
+ } catch {}
+ return { ok: true, data: { access_token: data.token, _userEmail: data.userEmail, _orgId: orgId } };
}
return { ok: false, data: { error: "authorization_pending" } };
},
@@ -770,6 +781,7 @@ const PROVIDERS = {
refreshToken: null,
expiresIn: null,
email: tokens._userEmail,
+ ...(tokens._orgId ? { providerSpecificData: { orgId: tokens._orgId } } : {}),
}),
},
diff --git a/src/shared/components/CursorAuthModal.js b/src/shared/components/CursorAuthModal.js
index 04da8f7e..c8966784 100644
--- a/src/shared/components/CursorAuthModal.js
+++ b/src/shared/components/CursorAuthModal.js
@@ -15,35 +15,38 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) {
const [importing, setImporting] = useState(false);
const [autoDetecting, setAutoDetecting] = useState(false);
const [autoDetected, setAutoDetected] = useState(false);
+ const [windowsManual, setWindowsManual] = useState(false);
+
+ const runAutoDetect = async () => {
+ setAutoDetecting(true);
+ setError(null);
+ setAutoDetected(false);
+ setWindowsManual(false);
+
+ try {
+ const res = await fetch("/api/oauth/cursor/auto-import");
+ const data = await res.json();
+
+ if (data.found) {
+ setAccessToken(data.accessToken);
+ setMachineId(data.machineId);
+ setAutoDetected(true);
+ } else if (data.windowsManual) {
+ setWindowsManual(true);
+ } else {
+ setError(data.error || "Could not auto-detect tokens");
+ }
+ } catch (err) {
+ setError("Failed to auto-detect tokens");
+ } finally {
+ setAutoDetecting(false);
+ }
+ };
// Auto-detect tokens when modal opens
useEffect(() => {
if (!isOpen) return;
-
- const autoDetect = async () => {
- setAutoDetecting(true);
- setError(null);
- setAutoDetected(false);
-
- try {
- const res = await fetch("/api/oauth/cursor/auto-import");
- const data = await res.json();
-
- if (data.found) {
- setAccessToken(data.accessToken);
- setMachineId(data.machineId);
- setAutoDetected(true);
- } else {
- setError(data.error || "Could not auto-detect tokens");
- }
- } catch (err) {
- setError("Failed to auto-detect tokens");
- } finally {
- setAutoDetecting(false);
- }
- };
-
- autoDetect();
+ runAutoDetect();
}, [isOpen]);
const handleImportToken = async () => {
@@ -76,7 +79,6 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) {
throw new Error(data.error || "Import failed");
}
- // Success - close modal and trigger refresh
onSuccess?.();
onClose();
} catch (err) {
@@ -119,8 +121,29 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) {
)}
+ {/* Windows manual instructions */}
+ {windowsManual && (
+
+
+
info
+
+ Could not read Cursor database automatically.
+
+
+
+ Run this command in your terminal, then click Retry:
+
+
+ npm i better-sqlite3 -g
+
+
+
+ )}
+
{/* Info message if not auto-detected */}
- {!autoDetected && !error && (
+ {!autoDetected && !windowsManual && !error && (
info
diff --git a/src/shared/constants/providers.js b/src/shared/constants/providers.js
index c9562b45..de55e221 100644
--- a/src/shared/constants/providers.js
+++ b/src/shared/constants/providers.js
@@ -15,7 +15,6 @@ export const OAUTH_PROVIDERS = {
codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6" },
github: { id: "github", alias: "gh", name: "GitHub Copilot", icon: "code", color: "#333333" },
cursor: { id: "cursor", alias: "cu", name: "Cursor IDE", icon: "edit_note", color: "#00D4AA" },
- // "kimi-coding": { id: "kimi-coding", alias: "kmc", name: "Kimi Coding", icon: "psychology", color: "#1E40AF", textIcon: "KC" },
// kilocode: { id: "kilocode", alias: "kc", name: "Kilo Code", icon: "code", color: "#FF6B35", textIcon: "KC" },
// cline: { id: "cline", alias: "cl", name: "Cline", icon: "smart_toy", color: "#5B9BD5", textIcon: "CL" },
};