chore: Refactor CursorAuthModal to handle manual instructions for Windows users.
This commit is contained in:
@@ -284,16 +284,15 @@ export const PROVIDERS = {
|
|||||||
},
|
},
|
||||||
kilocode: {
|
kilocode: {
|
||||||
baseUrl: "https://api.kilo.ai/api/openrouter/chat/completions",
|
baseUrl: "https://api.kilo.ai/api/openrouter/chat/completions",
|
||||||
format: "openrouter",
|
format: "openai",
|
||||||
headers: {}
|
headers: {}
|
||||||
},
|
},
|
||||||
cline: {
|
cline: {
|
||||||
baseUrl: "https://api.cline.bot/api/v1/messages",
|
baseUrl: "https://api.cline.bot/api/v1/chat/completions",
|
||||||
format: "claude",
|
format: "openai",
|
||||||
headers: {
|
headers: {
|
||||||
"HTTP-Referer": "https://cline.bot",
|
"HTTP-Referer": "https://cline.bot",
|
||||||
"X-Title": "Cline",
|
"X-Title": "Cline"
|
||||||
"Anthropic-Version": "2023-06-01"
|
|
||||||
},
|
},
|
||||||
tokenUrl: "https://api.cline.bot/api/v1/auth/token",
|
tokenUrl: "https://api.cline.bot/api/v1/auth/token",
|
||||||
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh"
|
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh"
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export class DefaultExecutor extends BaseExecutor {
|
|||||||
case "glm":
|
case "glm":
|
||||||
case "kimi":
|
case "kimi":
|
||||||
case "kimi-coding":
|
case "kimi-coding":
|
||||||
case "cline":
|
|
||||||
case "minimax":
|
case "minimax":
|
||||||
case "minimax-cn":
|
case "minimax-cn":
|
||||||
return `${this.config.baseUrl}?beta=true`;
|
return `${this.config.baseUrl}?beta=true`;
|
||||||
@@ -47,7 +46,6 @@ export class DefaultExecutor extends BaseExecutor {
|
|||||||
case "glm":
|
case "glm":
|
||||||
case "kimi":
|
case "kimi":
|
||||||
case "kimi-coding":
|
case "kimi-coding":
|
||||||
case "cline":
|
|
||||||
case "minimax":
|
case "minimax":
|
||||||
case "minimax-cn":
|
case "minimax-cn":
|
||||||
headers["x-api-key"] = credentials.apiKey || credentials.accessToken;
|
headers["x-api-key"] = credentials.apiKey || credentials.accessToken;
|
||||||
@@ -62,6 +60,11 @@ export class DefaultExecutor extends BaseExecutor {
|
|||||||
if (!headers["anthropic-version"]) {
|
if (!headers["anthropic-version"]) {
|
||||||
headers["anthropic-version"] = "2023-06-01";
|
headers["anthropic-version"] = "2023-06-01";
|
||||||
}
|
}
|
||||||
|
} else if (this.provider === "kilocode") {
|
||||||
|
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
|
||||||
|
if (credentials.providerSpecificData?.orgId) {
|
||||||
|
headers["X-Kilocode-OrganizationID"] = credentials.providerSpecificData.orgId;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
|
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ const ALIAS_TO_PROVIDER_ID = {
|
|||||||
gh: "github",
|
gh: "github",
|
||||||
kr: "kiro",
|
kr: "kiro",
|
||||||
cu: "cursor",
|
cu: "cursor",
|
||||||
|
kc: "kilocode",
|
||||||
|
kmc: "kimi-coding",
|
||||||
|
cl: "cline",
|
||||||
// API Key providers
|
// API Key providers
|
||||||
openai: "openai",
|
openai: "openai",
|
||||||
anthropic: "anthropic",
|
anthropic: "anthropic",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router-app",
|
"name": "9router-app",
|
||||||
"version": "0.3.16",
|
"version": "0.3.17",
|
||||||
"description": "9Router web dashboard",
|
"description": "9Router web dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -339,19 +339,74 @@ export default function ProfilePage() {
|
|||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Local Mode Info */}
|
{/* Local Mode Info */}
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="size-12 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center">
|
<div className="flex items-center gap-4">
|
||||||
<span className="material-symbols-outlined text-2xl">computer</span>
|
<div className="size-12 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center">
|
||||||
|
<span className="material-symbols-outlined text-2xl">computer</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold">Local Mode</h2>
|
||||||
|
<p className="text-text-muted">Running on your machine</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="inline-flex p-1 rounded-lg bg-black/5 dark:bg-white/5">
|
||||||
<h2 className="text-xl font-semibold">Local Mode</h2>
|
{["light", "dark", "system"].map((option) => (
|
||||||
<p className="text-text-muted">Running on your machine</p>
|
<button
|
||||||
|
key={option}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTheme(option)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1.5 px-3 py-1.5 rounded-md font-medium transition-all",
|
||||||
|
theme === option
|
||||||
|
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
||||||
|
: "text-text-muted hover:text-text-main"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[18px]">
|
||||||
|
{option === "light" ? "light_mode" : option === "dark" ? "dark_mode" : "contrast"}
|
||||||
|
</span>
|
||||||
|
<span className="capitalize text-sm">{option}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4 border-t border-border">
|
<div className="flex flex-col gap-3 pt-4 border-t border-border">
|
||||||
<p className="text-sm text-text-muted">
|
<div className="flex items-center justify-between p-3 rounded-lg bg-bg border border-border">
|
||||||
All data is stored locally in the <code className="bg-sidebar px-1 rounded">~/.9router/db.json</code> file.
|
<div>
|
||||||
</p>
|
<p className="font-medium">Database Location</p>
|
||||||
|
<p className="text-sm text-text-muted font-mono">~/.9router/db.json</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
icon="download"
|
||||||
|
onClick={handleExportDatabase}
|
||||||
|
loading={dbLoading}
|
||||||
|
>
|
||||||
|
Download Backup
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
icon="upload"
|
||||||
|
onClick={() => importFileRef.current?.click()}
|
||||||
|
disabled={dbLoading}
|
||||||
|
>
|
||||||
|
Import Backup
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={importFileRef}
|
||||||
|
type="file"
|
||||||
|
accept="application/json,.json"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleImportDatabase}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{dbStatus.message && (
|
||||||
|
<p className={`text-sm ${dbStatus.type === "error" ? "text-red-500" : "text-green-600 dark:text-green-400"}`}>
|
||||||
|
{dbStatus.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -560,102 +615,6 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Theme Preferences */}
|
|
||||||
<Card>
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500">
|
|
||||||
<span className="material-symbols-outlined text-[20px]">palette</span>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold">Appearance</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Dark Mode</p>
|
|
||||||
<p className="text-sm text-text-muted">
|
|
||||||
Switch between light and dark themes
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Toggle
|
|
||||||
checked={isDark}
|
|
||||||
onChange={() => setTheme(isDark ? "light" : "dark")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Theme Options */}
|
|
||||||
<div className="pt-4 border-t border-border">
|
|
||||||
<div className="inline-flex p-1 rounded-lg bg-black/5 dark:bg-white/5">
|
|
||||||
{["light", "dark", "system"].map((option) => (
|
|
||||||
<button
|
|
||||||
key={option}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setTheme(option)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all",
|
|
||||||
theme === option
|
|
||||||
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
|
||||||
: "text-text-muted hover:text-text-main"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-outlined text-[20px]">
|
|
||||||
{option === "light" ? "light_mode" : option === "dark" ? "dark_mode" : "contrast"}
|
|
||||||
</span>
|
|
||||||
<span className="capitalize">{option}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Data Management */}
|
|
||||||
<Card>
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<div className="p-2 rounded-lg bg-green-500/10 text-green-500">
|
|
||||||
<span className="material-symbols-outlined text-[20px]">database</span>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold">Data</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-bg border border-border">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Database Location</p>
|
|
||||||
<p className="text-sm text-text-muted font-mono">~/.9router/db.json</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
icon="download"
|
|
||||||
onClick={handleExportDatabase}
|
|
||||||
loading={dbLoading}
|
|
||||||
>
|
|
||||||
Download Backup
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
icon="upload"
|
|
||||||
onClick={() => importFileRef.current?.click()}
|
|
||||||
disabled={dbLoading}
|
|
||||||
>
|
|
||||||
Import Backup
|
|
||||||
</Button>
|
|
||||||
<input
|
|
||||||
ref={importFileRef}
|
|
||||||
type="file"
|
|
||||||
accept="application/json,.json"
|
|
||||||
className="hidden"
|
|
||||||
onChange={handleImportDatabase}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{dbStatus.message && (
|
|
||||||
<p className={`text-sm ${dbStatus.type === "error" ? "text-red-500" : "text-green-600 dark:text-green-400"}`}>
|
|
||||||
{dbStatus.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Observability Settings */}
|
{/* Observability Settings */}
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { NextResponse } from "next/server";
|
|||||||
import { access, constants } from "fs/promises";
|
import { access, constants } from "fs/promises";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { join } from "path";
|
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 ACCESS_TOKEN_KEYS = ["cursorAuth/accessToken", "cursorAuth/token"];
|
||||||
const MACHINE_ID_KEYS = ["storage.serviceMachineId", "storage.machineId", "telemetry.machineId"];
|
const MACHINE_ID_KEYS = ["storage.serviceMachineId", "storage.machineId", "telemetry.machineId"];
|
||||||
@@ -29,15 +33,14 @@ function getCandidatePaths(platform) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux
|
|
||||||
return [
|
return [
|
||||||
join(home, ".config/Cursor/User/globalStorage/state.vscdb"),
|
join(home, ".config/Cursor/User/globalStorage/state.vscdb"),
|
||||||
join(home, ".config/cursor/User/globalStorage/state.vscdb"),
|
join(home, ".config/cursor/User/globalStorage/state.vscdb"),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Extract tokens from open db, with fuzzy fallback */
|
/** Extract tokens using better-sqlite3 (stream-based, no RAM limit) */
|
||||||
function extractTokens(db, platform) {
|
function extractTokens(db) {
|
||||||
const desiredKeys = [...ACCESS_TOKEN_KEYS, ...MACHINE_ID_KEYS];
|
const desiredKeys = [...ACCESS_TOKEN_KEYS, ...MACHINE_ID_KEYS];
|
||||||
const rows = db.prepare(
|
const rows = db.prepare(
|
||||||
`SELECT key, value FROM itemTable WHERE key IN (${desiredKeys.map(() => "?").join(",")})`
|
`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) {
|
if (!tokens.accessToken || !tokens.machineId) {
|
||||||
const fallbackRows = db.prepare(
|
const fallbackRows = db.prepare(
|
||||||
"SELECT key, value FROM itemTable WHERE key LIKE '%cursorAuth/%' OR key LIKE '%machineId%' OR key LIKE '%serviceMachineId%'"
|
"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;
|
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
|
* 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() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
const candidates = getCandidatePaths(platform);
|
const candidates = getCandidatePaths(platform);
|
||||||
|
|
||||||
// Find first readable db path
|
|
||||||
let dbPath = null;
|
let dbPath = null;
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
try {
|
try {
|
||||||
@@ -111,44 +154,47 @@ export async function GET() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let db;
|
// Strategy 1: better-sqlite3 bundled → then global install fallback
|
||||||
|
let Database = null;
|
||||||
try {
|
try {
|
||||||
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
const mod = await import("better-sqlite3");
|
||||||
} catch (error) {
|
Database = mod.default;
|
||||||
return NextResponse.json({
|
} catch {
|
||||||
found: false,
|
// Try loading from global node_modules (user ran: npm i better-sqlite3 -g)
|
||||||
error: `Found Cursor database at:\n${dbPath}\n\nBut could not open it: ${error.message}`,
|
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 {
|
if (Database) {
|
||||||
const tokens = extractTokens(db, platform);
|
let db;
|
||||||
db.close();
|
try {
|
||||||
|
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
||||||
|
const tokens = extractTokens(db);
|
||||||
|
db.close();
|
||||||
|
|
||||||
if (!tokens.accessToken || !tokens.machineId) {
|
if (tokens.accessToken && tokens.machineId) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({ found: true, accessToken: tokens.accessToken, machineId: tokens.machineId });
|
||||||
found: false,
|
}
|
||||||
error: "Tokens not found in database. Please login to Cursor IDE first.",
|
} 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) {
|
} catch (error) {
|
||||||
console.log("Cursor auto-import error:", error);
|
console.log("Cursor auto-import error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json({ found: false, error: error.message }, { status: 500 });
|
||||||
{ found: false, error: error.message },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -761,7 +761,18 @@ const PROVIDERS = {
|
|||||||
if (!response.ok) return { ok: false, data: { error: "poll_failed", error_description: `Poll failed: ${response.status}` } };
|
if (!response.ok) return { ok: false, data: { error: "poll_failed", error_description: `Poll failed: ${response.status}` } };
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.status === "approved" && data.token) {
|
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" } };
|
return { ok: false, data: { error: "authorization_pending" } };
|
||||||
},
|
},
|
||||||
@@ -770,6 +781,7 @@ const PROVIDERS = {
|
|||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
expiresIn: null,
|
expiresIn: null,
|
||||||
email: tokens._userEmail,
|
email: tokens._userEmail,
|
||||||
|
...(tokens._orgId ? { providerSpecificData: { orgId: tokens._orgId } } : {}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -15,35 +15,38 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) {
|
|||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const [autoDetecting, setAutoDetecting] = useState(false);
|
const [autoDetecting, setAutoDetecting] = useState(false);
|
||||||
const [autoDetected, setAutoDetected] = 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
|
// Auto-detect tokens when modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
runAutoDetect();
|
||||||
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();
|
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleImportToken = async () => {
|
const handleImportToken = async () => {
|
||||||
@@ -76,7 +79,6 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) {
|
|||||||
throw new Error(data.error || "Import failed");
|
throw new Error(data.error || "Import failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success - close modal and trigger refresh
|
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -119,8 +121,29 @@ export default function CursorAuthModal({ isOpen, onSuccess, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Windows manual instructions */}
|
||||||
|
{windowsManual && (
|
||||||
|
<div className="bg-amber-50 dark:bg-amber-900/20 p-3 rounded-lg border border-amber-200 dark:border-amber-800 flex flex-col gap-2">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<span className="material-symbols-outlined text-amber-600 dark:text-amber-400">info</span>
|
||||||
|
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||||
|
Could not read Cursor database automatically.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-amber-700 dark:text-amber-300">
|
||||||
|
Run this command in your terminal, then click <strong>Retry</strong>:
|
||||||
|
</p>
|
||||||
|
<pre className="text-xs bg-black/10 dark:bg-white/10 rounded p-2 font-mono select-all">
|
||||||
|
npm i better-sqlite3 -g
|
||||||
|
</pre>
|
||||||
|
<Button onClick={runAutoDetect} variant="outline" fullWidth>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Info message if not auto-detected */}
|
{/* Info message if not auto-detected */}
|
||||||
{!autoDetected && !error && (
|
{!autoDetected && !windowsManual && !error && (
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
|
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400">info</span>
|
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400">info</span>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const OAUTH_PROVIDERS = {
|
|||||||
codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6" },
|
codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6" },
|
||||||
github: { id: "github", alias: "gh", name: "GitHub Copilot", icon: "code", color: "#333333" },
|
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" },
|
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" },
|
// 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" },
|
// cline: { id: "cline", alias: "cl", name: "Cline", icon: "smart_toy", color: "#5B9BD5", textIcon: "CL" },
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user