From 04ba66bc1eace6825c4fc1b38f6810de5b95554d Mon Sep 17 00:00:00 2001 From: decolua Date: Sat, 28 Feb 2026 11:30:21 +0700 Subject: [PATCH] chore: Refactor CursorAuthModal to handle manual instructions for Windows users. --- open-sse/config/constants.js | 9 +- open-sse/executors/default.js | 7 +- open-sse/services/model.js | 3 + package.json | 2 +- src/app/(dashboard)/dashboard/profile/page.js | 171 +++++++----------- src/app/api/oauth/cursor/auto-import/route.js | 122 +++++++++---- src/lib/oauth/providers.js | 14 +- src/shared/components/CursorAuthModal.js | 77 +++++--- src/shared/constants/providers.js | 1 - 9 files changed, 225 insertions(+), 181 deletions(-) diff --git a/open-sse/config/constants.js b/open-sse/config/constants.js index eced4346..e22c1b7d 100644 --- a/open-sse/config/constants.js +++ b/open-sse/config/constants.js @@ -284,16 +284,15 @@ export const PROVIDERS = { }, kilocode: { baseUrl: "https://api.kilo.ai/api/openrouter/chat/completions", - format: "openrouter", + format: "openai", headers: {} }, cline: { - baseUrl: "https://api.cline.bot/api/v1/messages", - format: "claude", + baseUrl: "https://api.cline.bot/api/v1/chat/completions", + format: "openai", headers: { "HTTP-Referer": "https://cline.bot", - "X-Title": "Cline", - "Anthropic-Version": "2023-06-01" + "X-Title": "Cline" }, tokenUrl: "https://api.cline.bot/api/v1/auth/token", refreshUrl: "https://api.cline.bot/api/v1/auth/refresh" diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index 97ea4bc7..a47da862 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -23,7 +23,6 @@ export class DefaultExecutor extends BaseExecutor { case "glm": case "kimi": case "kimi-coding": - case "cline": case "minimax": case "minimax-cn": return `${this.config.baseUrl}?beta=true`; @@ -47,7 +46,6 @@ export class DefaultExecutor extends BaseExecutor { case "glm": case "kimi": case "kimi-coding": - case "cline": case "minimax": case "minimax-cn": headers["x-api-key"] = credentials.apiKey || credentials.accessToken; @@ -62,6 +60,11 @@ export class DefaultExecutor extends BaseExecutor { if (!headers["anthropic-version"]) { 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 { headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; } diff --git a/open-sse/services/model.js b/open-sse/services/model.js index a712f36c..6b2f7f3e 100644 --- a/open-sse/services/model.js +++ b/open-sse/services/model.js @@ -9,6 +9,9 @@ const ALIAS_TO_PROVIDER_ID = { gh: "github", kr: "kiro", cu: "cursor", + kc: "kilocode", + kmc: "kimi-coding", + cl: "cline", // API Key providers openai: "openai", anthropic: "anthropic", diff --git a/package.json b/package.json index 3cc0796a..e5c082ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.16", + "version": "0.3.17", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index 0ee1e3cc..60e991d8 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -339,19 +339,74 @@ export default function ProfilePage() {
{/* Local Mode Info */} -
-
- computer +
+
+
+ computer +
+
+

Local Mode

+

Running on your machine

+
-
-

Local Mode

-

Running on your machine

+
+ {["light", "dark", "system"].map((option) => ( + + ))}
-
-

- All data is stored locally in the ~/.9router/db.json file. -

+
+
+
+

Database Location

+

~/.9router/db.json

+
+
+
+ + + +
+ {dbStatus.message && ( +

+ {dbStatus.message} +

+ )}
@@ -560,102 +615,6 @@ export default function ProfilePage() {
- {/* Theme Preferences */} - -
-
- palette -
-

Appearance

-
-
-
-
-

Dark Mode

-

- Switch between light and dark themes -

-
- setTheme(isDark ? "light" : "dark")} - /> -
- - {/* Theme Options */} -
-
- {["light", "dark", "system"].map((option) => ( - - ))} -
-
-
-
- - {/* Data Management */} - -
-
- database -
-

Data

-
-
-
-
-

Database Location

-

~/.9router/db.json

-
-
-
- - - -
- {dbStatus.message && ( -

- {dbStatus.message} -

- )} -
-
- {/* Observability Settings */}
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" }, };