diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index ca3061d6..96a230b3 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -226,6 +226,7 @@ export class DefaultExecutor extends BaseExecutor { gemini: () => this.refreshFromGrant(credentials, proxyOptions), kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions), cline: () => this.refreshCline(credentials.refreshToken, proxyOptions), + clinepass: () => this.refreshCline(credentials.refreshToken, proxyOptions), "kimi-coding": () => this.refreshKimiCoding(credentials.refreshToken, proxyOptions), kilocode: () => this.refreshKilocode(credentials.refreshToken, proxyOptions) }; @@ -299,7 +300,11 @@ export class DefaultExecutor extends BaseExecutor { const data = payload?.data || payload; const expiresAtIso = data?.expiresAt; const expiresIn = expiresAtIso ? Math.max(1, Math.floor((new Date(expiresAtIso).getTime() - Date.now()) / 1000)) : undefined; - return { accessToken: data?.accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn }; + let accessToken = data?.accessToken; + if (accessToken && !accessToken.startsWith("workos:")) { + accessToken = `workos:${accessToken}`; + } + return { accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn }; } async refreshKimiCoding(refreshToken, proxyOptions = null) { diff --git a/open-sse/providers/registry/clinepass.js b/open-sse/providers/registry/clinepass.js new file mode 100644 index 00000000..702054ac --- /dev/null +++ b/open-sse/providers/registry/clinepass.js @@ -0,0 +1,57 @@ +export default { + id: "clinepass", + priority: 85, + alias: "clinepass", + uiAlias: "clinepass", + display: { + name: "ClinePass", + icon: "vpn_key", + color: "#5B9BD5", + textIcon: "CP", + website: "https://cline.bot", + notice: { + signupUrl: "https://app.cline.bot", + }, + }, + category: "oauth", + authModes: ["oauth", "apikey"], + hasOAuth: true, + transport: { + baseUrl: "https://api.cline.bot/api/v1/chat/completions", + headers: { + "HTTP-Referer": "https://cline.bot", + "X-Title": "Cline", + }, + auth: { + combined: true, + header: "Authorization", + scheme: "bearer", + hooks: [ + "clineHeaders", + ], + }, + }, + models: [ + { id: "cline-pass/glm-5.2", name: "GLM-5.2 (ClinePass)" }, + { id: "cline-pass/kimi-k2.7-code", name: "Kimi K2.7 Code (ClinePass)" }, + { id: "cline-pass/kimi-k2.6", name: "Kimi K2.6 (ClinePass)" }, + { id: "cline-pass/deepseek-v4-pro", name: "DeepSeek V4 Pro (ClinePass)" }, + { id: "cline-pass/deepseek-v4-flash", name: "DeepSeek V4 Flash (ClinePass)" }, + { id: "cline-pass/mimo-v2.5", name: "MiMo-V2.5 (ClinePass)" }, + { id: "cline-pass/mimo-v2.5-pro", name: "MiMo-V2.5-Pro (ClinePass)" }, + { id: "cline-pass/minimax-m3", name: "MiniMax M3 (ClinePass)" }, + { id: "cline-pass/qwen3.7-max", name: "Qwen3.7 Max (ClinePass)" }, + { id: "cline-pass/qwen3.7-plus", name: "Qwen3.7 Plus (ClinePass)" }, + ], + oauth: { + appBaseUrl: "https://app.cline.bot", + apiBaseUrl: "https://api.cline.bot", + authorizeUrl: "https://api.cline.bot/api/v1/auth/authorize", + tokenUrl: "https://api.cline.bot/api/v1/auth/token", + refreshUrl: "https://api.cline.bot/api/v1/auth/refresh", + }, + thinkingConfig: { + options: ["auto", "on", "off"], + defaultMode: "auto", + }, +}; diff --git a/open-sse/providers/registry/index.js b/open-sse/providers/registry/index.js index 64edcf32..6f0f6826 100644 --- a/open-sse/providers/registry/index.js +++ b/open-sse/providers/registry/index.js @@ -15,86 +15,87 @@ import p12 from "./cerebras.js"; import p13 from "./chutes.js"; import p14 from "./claude.js"; import p15 from "./cline.js"; -import p16 from "./cloudflare-ai.js"; -import p17 from "./codebuddy-cn.js"; -import p18 from "./codex.js"; -import p19 from "./cohere.js"; -import p20 from "./comfyui.js"; -import p21 from "./commandcode.js"; -import p22 from "./coqui.js"; -import p23 from "./cursor.js"; -import p24 from "./deepgram.js"; -import p25 from "./deepseek.js"; -import p26 from "./edge-tts.js"; -import p27 from "./elevenlabs.js"; -import p28 from "./exa.js"; -import p29 from "./fal-ai.js"; -import p30 from "./firecrawl.js"; -import p31 from "./fireworks.js"; -import p32 from "./gemini-cli.js"; -import p33 from "./gemini.js"; -import p34 from "./github.js"; -import p35 from "./gitlab.js"; -import p36 from "./glm-cn.js"; -import p37 from "./glm.js"; -import p38 from "./google-pse.js"; -import p39 from "./google-tts.js"; -import p40 from "./grok-web.js"; -import p41 from "./groq.js"; -import p42 from "./huggingface.js"; -import p43 from "./hyperbolic.js"; -import p44 from "./iflow.js"; -import p45 from "./inworld.js"; -import p46 from "./jina-ai.js"; -import p47 from "./jina-reader.js"; -import p48 from "./kilocode.js"; -import p49 from "./kimchi.js"; -import p50 from "./kimi-coding.js"; -import p51 from "./kimi.js"; -import p52 from "./kiro.js"; -import p53 from "./linkup.js"; -import p54 from "./local-device.js"; -import p55 from "./mimo-free.js"; -import p56 from "./minimax-cn.js"; -import p57 from "./minimax.js"; -import p58 from "./mistral.js"; -import p59 from "./mmf.js"; -import p60 from "./nanobanana.js"; -import p61 from "./nebius.js"; -import p62 from "./nvidia.js"; -import p63 from "./ollama-local.js"; -import p64 from "./ollama.js"; -import p65 from "./openai.js"; -import p66 from "./opencode-go.js"; -import p67 from "./opencode.js"; -import p68 from "./openrouter.js"; -import p69 from "./perplexity-web.js"; -import p70 from "./perplexity.js"; -import p71 from "./playht.js"; -import p72 from "./qoder.js"; -import p73 from "./qwen.js"; -import p74 from "./recraft.js"; -import p75 from "./runwayml.js"; -import p76 from "./sdwebui.js"; -import p77 from "./searchapi.js"; -import p78 from "./searxng.js"; -import p79 from "./serper.js"; -import p80 from "./siliconflow.js"; -import p81 from "./stability-ai.js"; -import p82 from "./tavily.js"; -import p83 from "./together.js"; -import p84 from "./topaz.js"; -import p85 from "./tortoise.js"; -import p86 from "./venice.js"; -import p87 from "./vercel-ai-gateway.js"; -import p88 from "./vertex-partner.js"; -import p89 from "./vertex.js"; -import p90 from "./volcengine-ark.js"; -import p91 from "./voyage-ai.js"; -import p92 from "./xai.js"; -import p93 from "./xiaomi-mimo.js"; -import p94 from "./xiaomi-tokenplan.js"; -import p95 from "./youcom.js"; +import p16 from "./clinepass.js"; +import p17 from "./cloudflare-ai.js"; +import p18 from "./codebuddy-cn.js"; +import p19 from "./codex.js"; +import p20 from "./cohere.js"; +import p21 from "./comfyui.js"; +import p22 from "./commandcode.js"; +import p23 from "./coqui.js"; +import p24 from "./cursor.js"; +import p25 from "./deepgram.js"; +import p26 from "./deepseek.js"; +import p27 from "./edge-tts.js"; +import p28 from "./elevenlabs.js"; +import p29 from "./exa.js"; +import p30 from "./fal-ai.js"; +import p31 from "./firecrawl.js"; +import p32 from "./fireworks.js"; +import p33 from "./gemini-cli.js"; +import p34 from "./gemini.js"; +import p35 from "./github.js"; +import p36 from "./gitlab.js"; +import p37 from "./glm-cn.js"; +import p38 from "./glm.js"; +import p39 from "./google-pse.js"; +import p40 from "./google-tts.js"; +import p41 from "./grok-web.js"; +import p42 from "./groq.js"; +import p43 from "./huggingface.js"; +import p44 from "./hyperbolic.js"; +import p45 from "./iflow.js"; +import p46 from "./inworld.js"; +import p47 from "./jina-ai.js"; +import p48 from "./jina-reader.js"; +import p49 from "./kilocode.js"; +import p50 from "./kimchi.js"; +import p51 from "./kimi-coding.js"; +import p52 from "./kimi.js"; +import p53 from "./kiro.js"; +import p54 from "./linkup.js"; +import p55 from "./local-device.js"; +import p56 from "./mimo-free.js"; +import p57 from "./minimax-cn.js"; +import p58 from "./minimax.js"; +import p59 from "./mistral.js"; +import p60 from "./mmf.js"; +import p61 from "./nanobanana.js"; +import p62 from "./nebius.js"; +import p63 from "./nvidia.js"; +import p64 from "./ollama-local.js"; +import p65 from "./ollama.js"; +import p66 from "./openai.js"; +import p67 from "./opencode-go.js"; +import p68 from "./opencode.js"; +import p69 from "./openrouter.js"; +import p70 from "./perplexity-web.js"; +import p71 from "./perplexity.js"; +import p72 from "./playht.js"; +import p73 from "./qoder.js"; +import p74 from "./qwen.js"; +import p75 from "./recraft.js"; +import p76 from "./runwayml.js"; +import p77 from "./sdwebui.js"; +import p78 from "./searchapi.js"; +import p79 from "./searxng.js"; +import p80 from "./serper.js"; +import p81 from "./siliconflow.js"; +import p82 from "./stability-ai.js"; +import p83 from "./tavily.js"; +import p84 from "./together.js"; +import p85 from "./topaz.js"; +import p86 from "./tortoise.js"; +import p87 from "./venice.js"; +import p88 from "./vercel-ai-gateway.js"; +import p89 from "./vertex-partner.js"; +import p90 from "./vertex.js"; +import p91 from "./volcengine-ark.js"; +import p92 from "./voyage-ai.js"; +import p93 from "./xai.js"; +import p94 from "./xiaomi-mimo.js"; +import p95 from "./xiaomi-tokenplan.js"; +import p96 from "./youcom.js"; export default [ p0, @@ -192,5 +193,6 @@ export default [ p92, p93, p94, - p95 + p95, + p96 ]; diff --git a/open-sse/services/clinepassModels.js b/open-sse/services/clinepassModels.js new file mode 100644 index 00000000..4208a4b6 --- /dev/null +++ b/open-sse/services/clinepassModels.js @@ -0,0 +1,63 @@ +import { buildClineHeaders } from "../shared/clineAuth.js"; + +const CLINEPASS_MODELS_ENDPOINT = "https://api.cline.bot/api/v1/models"; +const FETCH_TIMEOUT_MS = 5000; + +/** + * Build request headers for the ClinePass /models endpoint (Cline's upstream API). + * - API keys are sent as plain Bearer tokens. + * - OAuth access tokens must carry the WorkOS `workos:` prefix (handled by buildClineHeaders). + */ +function buildModelListHeaders(token, isApiKey) { + if (isApiKey) { + return { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }; + } + return buildClineHeaders(token, { Accept: "application/json" }); +} + +/** + * Fetch ClinePass live model catalog from Cline's /models endpoint. + * + * @param {object} credentials - Connection credentials ({ accessToken, apiKey }) + * @returns {Promise<{ models: { id: string, name: string }[] } | null>} + */ +export async function resolveClinepassModels(credentials) { + const isApiKey = Boolean(credentials?.apiKey); + const token = isApiKey ? credentials.apiKey : credentials?.accessToken; + if (!token) return null; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + + try { + const headers = buildModelListHeaders(token, isApiKey); + + const response = await fetch(CLINEPASS_MODELS_ENDPOINT, { + method: "GET", + headers, + signal: controller.signal, + }); + + if (!response.ok) return null; + + const json = await response.json(); + const rawList = Array.isArray(json) ? json : json?.data; + if (!Array.isArray(rawList)) return null; + + const models = rawList + .filter((m) => typeof m?.id === "string" && m.id.startsWith("cline-pass/")) + .map((m) => ({ + id: m.id, + name: m.name || m.id, + })); + + return models.length ? { models } : null; + } catch { + return null; + } finally { + clearTimeout(timer); + } +} diff --git a/src/app/api/oauth/[provider]/[action]/route.js b/src/app/api/oauth/[provider]/[action]/route.js index 01c442bc..ce7fdbc5 100644 --- a/src/app/api/oauth/[provider]/[action]/route.js +++ b/src/app/api/oauth/[provider]/[action]/route.js @@ -232,8 +232,8 @@ export async function POST(request, { params }) { }); } - // Cline uses authorization_code without PKCE. Kimchi returns a browser token. - const noPkceExchangeProviders = ["cline", "kimchi"]; + // Cline and ClinePass use authorization_code without PKCE. Kimchi returns a browser token. + const noPkceExchangeProviders = ["cline", "clinepass", "kimchi"]; if (!code || !redirectUri || (!codeVerifier && !noPkceExchangeProviders.includes(provider))) { return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); } diff --git a/src/app/api/v1/models/route.js b/src/app/api/v1/models/route.js index 8b1c720b..32647304 100644 --- a/src/app/api/v1/models/route.js +++ b/src/app/api/v1/models/route.js @@ -11,6 +11,7 @@ import { resolveKiroModels } from "open-sse/services/kiroModels.js"; import { resolveKimchiModels } from "open-sse/services/kimchiModels.js"; import { resolveQoderModels } from "open-sse/services/qoderModels.js"; import { resolveCopilotModels } from "open-sse/services/copilotModels.js"; +import { resolveClinepassModels } from "open-sse/services/clinepassModels.js"; import { updateProviderCredentials } from "@/sse/services/tokenRefresh"; import { capabilitiesFromServiceKind } from "open-sse/providers/capabilities.js"; @@ -63,6 +64,13 @@ const LIVE_MODEL_RESOLVERS = { }, }); return result?.models?.length ? { models: result.models } : null; + }, + clinepass: async (conn) => { + const result = await resolveClinepassModels({ + accessToken: conn.accessToken, + apiKey: conn.apiKey, + }); + return result?.models?.length ? { models: result.models } : null; } }; diff --git a/src/lib/oauth/constants/oauth.js b/src/lib/oauth/constants/oauth.js index 1354d5d9..2f4715cf 100644 --- a/src/lib/oauth/constants/oauth.js +++ b/src/lib/oauth/constants/oauth.js @@ -102,6 +102,9 @@ export const KILOCODE_CONFIG = { ...PROVIDER_OAUTH["kilocode"] }; // Cline OAuth Configuration (Local Callback Flow via app.cline.bot) export const CLINE_CONFIG = { ...PROVIDER_OAUTH["cline"] }; +// ClinePass OAuth Configuration (shares Cline's OAuth endpoints) +export const CLINEPASS_CONFIG = { ...PROVIDER_OAUTH["clinepass"] }; + // GitLab Duo OAuth Configuration (Authorization Code Flow with PKCE) export const GITLAB_CONFIG = { ...PROVIDER_OAUTH["gitlab"] }; @@ -130,6 +133,7 @@ export const PROVIDERS = { KIMI_CODING: "kimi-coding", KILOCODE: "kilocode", CLINE: "cline", + CLINEPASS: "clinepass", GITLAB: "gitlab", CODEBUDDY: "codebuddy-cn", KIMCHI: "kimchi", diff --git a/src/lib/oauth/providers.js b/src/lib/oauth/providers.js index bb543b85..361f384d 100644 --- a/src/lib/oauth/providers.js +++ b/src/lib/oauth/providers.js @@ -23,6 +23,7 @@ import { KIMI_CODING_CONFIG, KILOCODE_CONFIG, CLINE_CONFIG, + CLINEPASS_CONFIG, GITLAB_CONFIG, CODEBUDDY_CONFIG, KIMCHI_CONFIG, @@ -1116,6 +1117,64 @@ const PROVIDERS = { providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName }, }), }, + clinepass: { + config: CLINEPASS_CONFIG, + flowType: "authorization_code", + buildAuthUrl: (config, redirectUri) => { + const params = new URLSearchParams({ + client_type: "extension", + callback_url: redirectUri, + redirect_uri: redirectUri, + }); + return `${config.authorizeUrl}?${params.toString()}`; + }, + exchangeToken: async (config, code, redirectUri) => { + try { + // Cline encodes token data as base64 in the code param + let base64 = code; + const padding = 4 - (base64.length % 4); + if (padding !== 4) base64 += "=".repeat(padding); + const decoded = Buffer.from(base64, "base64").toString("utf-8"); + const lastBrace = decoded.lastIndexOf("}"); + if (lastBrace === -1) throw new Error("No JSON found in decoded code"); + const tokenData = JSON.parse(decoded.substring(0, lastBrace + 1)); + return { + access_token: tokenData.accessToken, + refresh_token: tokenData.refreshToken, + email: tokenData.email, + firstName: tokenData.firstName, + lastName: tokenData.lastName, + expires_at: tokenData.expiresAt, + }; + } catch (e) { + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ grant_type: "authorization_code", code, client_type: "extension", redirect_uri: redirectUri }), + }); + if (!response.ok) { + const error = await response.text(); + throw new Error(`ClinePass token exchange failed: ${error}`); + } + const data = await response.json(); + return { + access_token: data.data?.accessToken || data.accessToken, + refresh_token: data.data?.refreshToken || data.refreshToken, + email: data.data?.userInfo?.email || "", + expires_at: data.data?.expiresAt || data.expiresAt, + }; + } + }, + mapTokens: (tokens) => ({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_at + ? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000) + : 3600, + email: tokens.email, + providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName }, + }), + }, // GitLab Duo - Authorization Code Flow with PKCE // Supports two login modes via loginMode metadata: "oauth" (default) or "pat" gitlab: {