mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-17 00:14:34 +00:00
feat(cliproxy): integrate missing provider support
This commit is contained in:
1 parent
2589c02937
commit
2d9f8c9695
34 files changed
+863
-34
No files matched your search
@@ -253,9 +253,8 @@ class ProfileDetector {
|
||||
* Detect profile type and return routing information
|
||||
*
|
||||
* Priority order:
|
||||
* 0. Hardcoded CLIProxy profiles (gemini, codex, agy, qwen)
|
||||
* 0.5. Copilot profile (if enabled in config)
|
||||
* 0.75. Cursor profile (if enabled in config)
|
||||
* 0. Hardcoded special runtime profiles (copilot, cursor)
|
||||
* 0.5. Hardcoded CLIProxy profiles (gemini, codex, agy, qwen, ...)
|
||||
* 1. Unified config profiles (if config.yaml exists or CCS_UNIFIED_CONFIG=1)
|
||||
* 2. User-defined CLIProxy variants (config.cliproxy section) [legacy]
|
||||
* 3. Settings-based profiles (config.profiles section) [legacy]
|
||||
@@ -267,16 +266,7 @@ class ProfileDetector {
|
||||
return this.resolveDefaultProfile();
|
||||
}
|
||||
|
||||
// Priority 0: Check CLIProxy profiles (gemini, codex, agy, qwen) - OAuth-based, zero config
|
||||
if (isCLIProxyProvider(profileName)) {
|
||||
return {
|
||||
type: 'cliproxy',
|
||||
name: profileName,
|
||||
provider: profileName,
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 0.5: Check Copilot profile - GitHub Copilot subscription via copilot-api
|
||||
// Priority 0: Check Copilot profile - GitHub Copilot subscription via copilot-api
|
||||
if (profileName === 'copilot') {
|
||||
const unifiedConfig = this.readUnifiedConfig();
|
||||
const copilotConfig = unifiedConfig?.copilot;
|
||||
@@ -306,7 +296,10 @@ class ProfileDetector {
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 0.75: Check Cursor profile - local Cursor daemon runtime
|
||||
// Priority 0.25: Check Cursor profile - local Cursor daemon runtime.
|
||||
// This keeps bare `ccs cursor` and `ccs cursor <subcommand>` bound to the
|
||||
// existing runtime/admin surface even though CLIProxy also exposes a
|
||||
// distinct provider named "cursor".
|
||||
if (profileName === 'cursor') {
|
||||
const cursorConfig = getCursorConfig();
|
||||
|
||||
@@ -334,6 +327,15 @@ class ProfileDetector {
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 0.5: Check CLIProxy profiles (gemini, codex, agy, qwen, ...)
|
||||
if (isCLIProxyProvider(profileName)) {
|
||||
return {
|
||||
type: 'cliproxy',
|
||||
name: profileName,
|
||||
provider: profileName,
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 1: Try unified config if available
|
||||
const unifiedConfig = this.readUnifiedConfig();
|
||||
if (unifiedConfig) {
|
||||
|
||||
+20
-2
@@ -77,6 +77,7 @@ import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils
|
||||
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
|
||||
import { createLogger } from './services/logging';
|
||||
import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides';
|
||||
import type { ProfileDetectionResult } from './auth/profile-detector';
|
||||
|
||||
// Import target adapter system
|
||||
import {
|
||||
@@ -122,6 +123,7 @@ interface RuntimeReasoningResolution {
|
||||
sourceDisplay: string | undefined;
|
||||
}
|
||||
|
||||
const CURSOR_CLIPROXY_SHORTCUT_FLAGS = new Set(['--auth', '--logout', '--config', '--accounts']);
|
||||
const CODEX_RUNTIME_REASONING_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
|
||||
const CODEX_NATIVE_PASSTHROUGH_FLAGS = new Set(['--help', '-h', '--version', '-v']);
|
||||
|
||||
@@ -147,6 +149,13 @@ function detectProfile(args: string[]): DetectedProfile {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUseCursorCliproxyShortcut(args: string[]): boolean {
|
||||
return (
|
||||
args[0] === 'cursor' &&
|
||||
args.some((arg, index) => index > 0 && CURSOR_CLIPROXY_SHORTCUT_FLAGS.has(arg))
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRuntimeReasoningFlags(
|
||||
args: string[],
|
||||
envThinkingValue: string | undefined
|
||||
@@ -537,8 +546,17 @@ async function main(): Promise<void> {
|
||||
try {
|
||||
// Detect profile (strip --target flags before profile detection)
|
||||
const cleanArgs = stripTargetFlag(args);
|
||||
const { profile, remainingArgs } = detectProfile(cleanArgs);
|
||||
const profileInfo = detector.detectProfileType(profile);
|
||||
const useCursorCliproxyShortcut = shouldUseCursorCliproxyShortcut(cleanArgs);
|
||||
const { profile, remainingArgs } = useCursorCliproxyShortcut
|
||||
? { profile: 'cursor', remainingArgs: cleanArgs.slice(1) }
|
||||
: detectProfile(cleanArgs);
|
||||
const profileInfo: ProfileDetectionResult = useCursorCliproxyShortcut
|
||||
? {
|
||||
type: 'cliproxy',
|
||||
name: 'cursor',
|
||||
provider: 'cursor',
|
||||
}
|
||||
: detector.detectProfileType(profile);
|
||||
let resolvedTarget: ReturnType<typeof resolveTargetType>;
|
||||
try {
|
||||
resolvedTarget = resolveTargetType(
|
||||
|
||||
@@ -155,6 +155,10 @@ export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google'
|
||||
* - Qwen: Device Code Flow (polling-based, NO callback port needed)
|
||||
* - GHCP: Device Code Flow (polling-based, NO callback port needed)
|
||||
* - Kimi: Device Code Flow (polling-based, NO callback port needed)
|
||||
* - Cursor: Device-style browser polling (NO callback port needed)
|
||||
* - GitLab: Authorization Code Flow with callback server on port 17171
|
||||
* - CodeBuddy: Device-style browser polling (NO callback port needed)
|
||||
* - Kilo: Device Code Flow (polling-based, NO callback port needed)
|
||||
*/
|
||||
export const OAUTH_CALLBACK_PORTS: Partial<Record<CLIProxyProvider, number>> =
|
||||
CLIPROXY_PROVIDER_IDS.reduce(
|
||||
@@ -274,6 +278,34 @@ export const OAUTH_CONFIGS: Record<CLIProxyProvider, ProviderOAuthConfig> = {
|
||||
scopes: ['api'],
|
||||
authFlag: '--kimi-login',
|
||||
},
|
||||
cursor: {
|
||||
provider: 'cursor',
|
||||
displayName: 'Cursor',
|
||||
authUrl: 'https://cursor.com/loginDeepControl',
|
||||
scopes: [],
|
||||
authFlag: '--cursor-login',
|
||||
},
|
||||
gitlab: {
|
||||
provider: 'gitlab',
|
||||
displayName: 'GitLab Duo',
|
||||
authUrl: 'https://gitlab.com/oauth/authorize',
|
||||
scopes: ['api', 'read_user'],
|
||||
authFlag: '--gitlab-login',
|
||||
},
|
||||
codebuddy: {
|
||||
provider: 'codebuddy',
|
||||
displayName: 'CodeBuddy (Tencent)',
|
||||
authUrl: 'https://copilot.tencent.com/v2/plugin/auth/state',
|
||||
scopes: [],
|
||||
authFlag: '--codebuddy-login',
|
||||
},
|
||||
kilo: {
|
||||
provider: 'kilo',
|
||||
displayName: 'Kilo AI',
|
||||
authUrl: 'https://api.kilo.ai/api/device-auth/codes',
|
||||
scopes: [],
|
||||
authFlag: '--kilo-login',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -373,6 +405,12 @@ export interface OAuthOptions {
|
||||
noIncognito?: boolean;
|
||||
/** If true, skip OAuth and import token from Kiro IDE directly (Kiro only) */
|
||||
import?: boolean;
|
||||
/** GitLab auth mode override. */
|
||||
gitlabAuthMode?: 'oauth' | 'pat';
|
||||
/** GitLab self-hosted base URL override. */
|
||||
gitlabBaseUrl?: string;
|
||||
/** GitLab personal access token for PAT login. */
|
||||
gitlabPersonalAccessToken?: string;
|
||||
/** Enable paste-callback mode: show auth URL and prompt for callback paste */
|
||||
pasteCallback?: boolean;
|
||||
/** If true, use port-forwarding mode (skip interactive prompt in headless) */
|
||||
|
||||
@@ -83,9 +83,12 @@ const POLLED_AUTH_LOCAL_TOKEN_GRACE_MS = 15 * 1000;
|
||||
export async function requestPasteCallbackStart(
|
||||
provider: CLIProxyProvider,
|
||||
target: ProxyTarget,
|
||||
options?: { kiroMethod?: OAuthOptions['kiroMethod'] }
|
||||
options?: {
|
||||
kiroMethod?: OAuthOptions['kiroMethod'];
|
||||
gitlabBaseUrl?: OAuthOptions['gitlabBaseUrl'];
|
||||
}
|
||||
): Promise<PasteCallbackStartData> {
|
||||
const startPath = getPasteCallbackStartPath(provider, {
|
||||
let startPath = getPasteCallbackStartPath(provider, {
|
||||
kiroMethod: options?.kiroMethod,
|
||||
});
|
||||
if (!startPath) {
|
||||
@@ -93,6 +96,9 @@ export async function requestPasteCallbackStart(
|
||||
`Paste-callback start is not available for ${provider} with the selected method`
|
||||
);
|
||||
}
|
||||
if (provider === 'gitlab' && options?.gitlabBaseUrl?.trim()) {
|
||||
startPath += `&base_url=${encodeURIComponent(options.gitlabBaseUrl.trim())}`;
|
||||
}
|
||||
const response = await fetch(buildProxyUrl(target, startPath), {
|
||||
headers: buildManagementHeaders(target),
|
||||
});
|
||||
@@ -142,6 +148,27 @@ function parseAuthUrlState(url: string | null | undefined): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGitLabBaseUrl(baseUrl: string | undefined): string | undefined {
|
||||
const normalized = baseUrl?.trim();
|
||||
return normalized ? normalized : undefined;
|
||||
}
|
||||
|
||||
async function promptGitLabPersonalAccessToken(): Promise<string | null> {
|
||||
const readline = await import('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise<string | null>((resolve) => {
|
||||
rl.question('GitLab Personal Access Token: ', (answer) => {
|
||||
rl.close();
|
||||
const token = answer.trim();
|
||||
resolve(token.length > 0 ? token : null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function findNewTokenSnapshotForManualAuth(
|
||||
provider: CLIProxyProvider,
|
||||
tokenDir: string,
|
||||
@@ -458,7 +485,10 @@ async function handlePasteCallbackMode(
|
||||
tokenDir: string,
|
||||
nickname?: string,
|
||||
expectedAccountId?: string,
|
||||
options?: { kiroMethod?: OAuthOptions['kiroMethod'] }
|
||||
options?: {
|
||||
kiroMethod?: OAuthOptions['kiroMethod'];
|
||||
gitlabBaseUrl?: OAuthOptions['gitlabBaseUrl'];
|
||||
}
|
||||
): Promise<AccountInfo | null> {
|
||||
// Resolve CLIProxyAPI target (local or remote based on config)
|
||||
const target = getProxyTarget();
|
||||
@@ -647,6 +677,97 @@ async function handlePasteCallbackMode(
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGitLabPatLogin(
|
||||
provider: CLIProxyProvider,
|
||||
oauthConfig: ProviderOAuthConfig,
|
||||
verbose: boolean,
|
||||
tokenDir: string,
|
||||
nickname?: string,
|
||||
expectedAccountId?: string,
|
||||
options?: {
|
||||
gitlabBaseUrl?: OAuthOptions['gitlabBaseUrl'];
|
||||
gitlabPersonalAccessToken?: OAuthOptions['gitlabPersonalAccessToken'];
|
||||
}
|
||||
): Promise<AccountInfo | null> {
|
||||
const target = getProxyTarget();
|
||||
const baseUrl = normalizeGitLabBaseUrl(options?.gitlabBaseUrl);
|
||||
const knownTokenFiles = listProviderTokenSnapshots(provider, tokenDir);
|
||||
const suppliedToken = options?.gitlabPersonalAccessToken?.trim();
|
||||
const personalAccessToken =
|
||||
suppliedToken || process.env['GITLAB_PERSONAL_ACCESS_TOKEN']?.trim() || undefined;
|
||||
|
||||
let token = personalAccessToken;
|
||||
if (!token) {
|
||||
console.log('');
|
||||
console.log(info(`Starting ${oauthConfig.displayName} PAT login...`));
|
||||
console.log('Paste a Personal Access Token with api and read_user scopes.');
|
||||
token = (await promptGitLabPersonalAccessToken()) || undefined;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.log(info('Cancelled'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(buildProxyUrl(target, '/v0/management/gitlab-auth-url'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...buildManagementHeaders(target),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(baseUrl ? { base_url: baseUrl } : {}),
|
||||
personal_access_token: token,
|
||||
}),
|
||||
});
|
||||
|
||||
const responseBody = (await response.text()).trim();
|
||||
let payload: Record<string, unknown> = {};
|
||||
if (responseBody) {
|
||||
try {
|
||||
payload = JSON.parse(responseBody) as Record<string, unknown>;
|
||||
} catch {
|
||||
payload = { error: responseBody };
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok || payload.status !== 'ok') {
|
||||
const errorMessage =
|
||||
(typeof payload.error === 'string' && payload.error) ||
|
||||
responseBody ||
|
||||
`GitLab PAT login failed with status ${response.status}`;
|
||||
console.log(fail(errorMessage));
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenSnapshot = findNewTokenSnapshotForAuthAttempt(
|
||||
provider,
|
||||
tokenDir,
|
||||
knownTokenFiles,
|
||||
expectedAccountId
|
||||
);
|
||||
if (!tokenSnapshot) {
|
||||
console.log(fail('GitLab PAT login completed, but CCS could not find the saved token file.'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = registerAccountFromToken(
|
||||
provider,
|
||||
tokenDir,
|
||||
nickname,
|
||||
verbose,
|
||||
expectedAccountId || tokenSnapshot.file
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
console.log(fail('Authenticated GitLab token could not be registered as a CCS account.'));
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(ok('Authentication successful!'));
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger OAuth flow for provider
|
||||
* Auto-detects headless environment and uses --no-browser flag accordingly
|
||||
@@ -666,6 +787,10 @@ export async function triggerOAuth(
|
||||
provider === 'kiro' ? normalizeKiroAuthMethod(options.kiroMethod) : DEFAULT_KIRO_AUTH_METHOD;
|
||||
const resolvedKiroIDCFlow =
|
||||
provider === 'kiro' ? normalizeKiroIDCFlow(options.kiroIDCFlow) : DEFAULT_KIRO_IDC_FLOW;
|
||||
const resolvedGitLabAuthMode =
|
||||
provider === 'gitlab' && options.gitlabAuthMode === 'pat' ? 'pat' : 'oauth';
|
||||
const resolvedGitLabBaseUrl =
|
||||
provider === 'gitlab' ? normalizeGitLabBaseUrl(options.gitlabBaseUrl) : undefined;
|
||||
|
||||
if (provider === 'agy') {
|
||||
if (fromUI && !acceptAgyRisk) {
|
||||
@@ -744,6 +869,14 @@ export async function triggerOAuth(
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && resolvedGitLabBaseUrl && !selectedPasteCallback) {
|
||||
selectedPasteCallback = true;
|
||||
console.log('');
|
||||
console.log(
|
||||
info('GitLab custom base URL selected. Switching to paste-callback mode for OAuth.')
|
||||
);
|
||||
}
|
||||
|
||||
const useSelectedKiroLocalPasteCallback =
|
||||
selectedPasteCallback &&
|
||||
provider === 'kiro' &&
|
||||
@@ -765,6 +898,22 @@ export async function triggerOAuth(
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && resolvedGitLabAuthMode === 'pat') {
|
||||
const tokenDir = getProviderTokenDir(provider);
|
||||
return handleGitLabPatLogin(
|
||||
provider,
|
||||
oauthConfig,
|
||||
verbose,
|
||||
tokenDir,
|
||||
nickname,
|
||||
existingNameMatch?.id,
|
||||
{
|
||||
gitlabBaseUrl: resolvedGitLabBaseUrl,
|
||||
gitlabPersonalAccessToken: options.gitlabPersonalAccessToken,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedPasteCallback && !useSelectedKiroDirectCliFlow) {
|
||||
const tokenDir = getProviderTokenDir(provider);
|
||||
return handlePasteCallbackMode(
|
||||
@@ -774,7 +923,10 @@ export async function triggerOAuth(
|
||||
tokenDir,
|
||||
nickname,
|
||||
existingNameMatch?.id,
|
||||
{ kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined }
|
||||
{
|
||||
kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined,
|
||||
gitlabBaseUrl: provider === 'gitlab' ? resolvedGitLabBaseUrl : undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -353,6 +353,7 @@ export async function execClaudeWithCLIProxy(
|
||||
const addAccount = argsWithoutProxy.includes('--add');
|
||||
const showAccounts = argsWithoutProxy.includes('--accounts');
|
||||
const forceImport = argsWithoutProxy.includes('--import');
|
||||
const gitlabTokenLogin = argsWithoutProxy.includes('--token-login');
|
||||
const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy);
|
||||
|
||||
const incognitoFlag = argsWithoutProxy.includes('--incognito');
|
||||
@@ -444,6 +445,16 @@ export async function execClaudeWithCLIProxy(
|
||||
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
|
||||
}
|
||||
|
||||
let gitlabBaseUrl: string | undefined;
|
||||
const gitlabBaseUrlValue = readOptionValue(argsWithoutProxy, '--gitlab-url');
|
||||
if (gitlabBaseUrlValue.present && gitlabBaseUrlValue.value) {
|
||||
gitlabBaseUrl = gitlabBaseUrlValue.value.trim();
|
||||
} else if (gitlabBaseUrlValue.present) {
|
||||
console.error(fail('--gitlab-url requires a value'));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
|
||||
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
|
||||
process.exitCode = 1;
|
||||
@@ -491,6 +502,13 @@ export async function execClaudeWithCLIProxy(
|
||||
return;
|
||||
}
|
||||
|
||||
if ((gitlabTokenLogin || gitlabBaseUrl) && provider !== 'gitlab') {
|
||||
const flagName = gitlabTokenLogin ? '--token-login' : '--gitlab-url';
|
||||
console.error(fail(`${flagName} is only valid for ccs gitlab`));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse --thinking / --effort flags (aliases; first occurrence wins)
|
||||
const thinkingParse = parseThinkingOverride(argsWithoutProxy);
|
||||
if (thinkingParse.error) {
|
||||
@@ -730,6 +748,8 @@ export async function execClaudeWithCLIProxy(
|
||||
...(kiroIDCStartUrl && p === 'kiro' ? { kiroIDCStartUrl } : {}),
|
||||
...(kiroIDCRegion && p === 'kiro' ? { kiroIDCRegion } : {}),
|
||||
...(kiroIDCFlow && p === 'kiro' ? { kiroIDCFlow } : {}),
|
||||
...(gitlabTokenLogin && p === 'gitlab' ? { gitlabAuthMode: 'pat' as const } : {}),
|
||||
...(gitlabBaseUrl && p === 'gitlab' ? { gitlabBaseUrl } : {}),
|
||||
...(forceHeadless ? { headless: true } : {}),
|
||||
...(setNickname ? { nickname: setNickname } : {}),
|
||||
...(noIncognito ? { noIncognito: true } : {}),
|
||||
@@ -775,6 +795,8 @@ export async function execClaudeWithCLIProxy(
|
||||
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
|
||||
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
|
||||
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
|
||||
...(gitlabTokenLogin ? { gitlabAuthMode: 'pat' as const } : {}),
|
||||
...(gitlabBaseUrl ? { gitlabBaseUrl } : {}),
|
||||
...(forceHeadless ? { headless: true } : {}),
|
||||
...(setNickname ? { nickname: setNickname } : {}),
|
||||
...(noIncognito ? { noIncognito: true } : {}),
|
||||
|
||||
@@ -134,6 +134,54 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
tokenTypeValues: ['kimi'],
|
||||
aliases: ['moonshot'],
|
||||
},
|
||||
cursor: {
|
||||
displayName: 'Cursor',
|
||||
description: 'Cursor browser-authenticated provider',
|
||||
oauthFlow: 'device_code',
|
||||
callbackPort: null,
|
||||
callbackProviderName: 'cursor',
|
||||
authUrlProviderName: 'cursor',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authFilePrefixes: ['cursor.', 'cursor-'],
|
||||
tokenTypeValues: ['cursor'],
|
||||
aliases: [],
|
||||
},
|
||||
gitlab: {
|
||||
displayName: 'GitLab Duo',
|
||||
description: 'GitLab Duo with OAuth or PAT auth',
|
||||
oauthFlow: 'authorization_code',
|
||||
callbackPort: 17171,
|
||||
callbackProviderName: 'gitlab',
|
||||
authUrlProviderName: 'gitlab',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authFilePrefixes: ['gitlab-'],
|
||||
tokenTypeValues: ['gitlab'],
|
||||
aliases: ['gitlab-duo'],
|
||||
},
|
||||
codebuddy: {
|
||||
displayName: 'CodeBuddy (Tencent)',
|
||||
description: 'Tencent CodeBuddy AI assistant',
|
||||
oauthFlow: 'device_code',
|
||||
callbackPort: null,
|
||||
callbackProviderName: 'codebuddy',
|
||||
authUrlProviderName: 'codebuddy',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authFilePrefixes: ['codebuddy-'],
|
||||
tokenTypeValues: ['codebuddy'],
|
||||
aliases: ['tencent'],
|
||||
},
|
||||
kilo: {
|
||||
displayName: 'Kilo AI',
|
||||
description: 'Kilo AI coding assistant',
|
||||
oauthFlow: 'device_code',
|
||||
callbackPort: null,
|
||||
callbackProviderName: 'kilo',
|
||||
authUrlProviderName: 'kilo',
|
||||
refreshOwnership: 'unsupported',
|
||||
authFilePrefixes: ['kilo-'],
|
||||
tokenTypeValues: ['kilo'],
|
||||
aliases: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const CLIPROXY_PROVIDER_IDS = Object.freeze(
|
||||
|
||||
+19
-4
@@ -122,6 +122,10 @@ export interface DownloadResult {
|
||||
* - ghcp: GitHub Copilot via Device Code (OAuth through CLIProxyAPIPlus)
|
||||
* - claude: Claude (Anthropic) via OAuth
|
||||
* - kimi: Kimi (Moonshot AI) via Device Code OAuth
|
||||
* - cursor: Cursor via PKCE browser polling
|
||||
* - gitlab: GitLab Duo via OAuth or PAT
|
||||
* - codebuddy: Tencent CodeBuddy via browser polling
|
||||
* - kilo: Kilo AI via device flow
|
||||
*/
|
||||
export type CLIProxyProvider =
|
||||
| 'gemini'
|
||||
@@ -132,12 +136,16 @@ export type CLIProxyProvider =
|
||||
| 'kiro'
|
||||
| 'ghcp'
|
||||
| 'claude'
|
||||
| 'kimi';
|
||||
| 'kimi'
|
||||
| 'cursor'
|
||||
| 'gitlab'
|
||||
| 'codebuddy'
|
||||
| 'kilo';
|
||||
|
||||
/**
|
||||
* CLIProxy backend selection
|
||||
* - original: CLIProxyAPI (no Kiro/ghcp support)
|
||||
* - plus: CLIProxyAPIPlus (Kiro/ghcp support, default)
|
||||
* - original: CLIProxyAPI (legacy provider subset)
|
||||
* - plus: CLIProxyAPIPlus (expanded provider support, default)
|
||||
*/
|
||||
export type CLIProxyBackend = 'original' | 'plus';
|
||||
|
||||
@@ -149,7 +157,14 @@ export type CliproxyRoutingStrategy = 'round-robin' | 'fill-first';
|
||||
/**
|
||||
* Providers that require CLIProxyAPIPlus backend
|
||||
*/
|
||||
export const PLUS_ONLY_PROVIDERS: CLIProxyProvider[] = ['kiro', 'ghcp'];
|
||||
export const PLUS_ONLY_PROVIDERS: CLIProxyProvider[] = [
|
||||
'kiro',
|
||||
'ghcp',
|
||||
'cursor',
|
||||
'gitlab',
|
||||
'codebuddy',
|
||||
'kilo',
|
||||
];
|
||||
|
||||
/**
|
||||
* CLIProxy config.yaml structure (minimal)
|
||||
|
||||
@@ -183,6 +183,10 @@ export const BUILTIN_PROVIDER_SHORTCUTS: readonly ShortcutEntry[] = CLIPROXY_PRO
|
||||
ghcp: 'GitHub Copilot via CLIProxy OAuth',
|
||||
claude: 'Claude via CLIProxy OAuth',
|
||||
kimi: 'Kimi via CLIProxy OAuth',
|
||||
cursor: 'Cursor via CLIProxy OAuth',
|
||||
gitlab: 'GitLab Duo via CLIProxy OAuth',
|
||||
codebuddy: 'CodeBuddy via CLIProxy OAuth',
|
||||
kilo: 'Kilo AI via CLIProxy OAuth',
|
||||
}[name] || 'CLIProxy OAuth provider',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -9,6 +9,13 @@ export const RESERVED_PROFILE_NAMES = [
|
||||
'agy',
|
||||
'qwen',
|
||||
'iflow',
|
||||
'kiro',
|
||||
'ghcp',
|
||||
'claude',
|
||||
'kimi',
|
||||
'gitlab',
|
||||
'codebuddy',
|
||||
'kilo',
|
||||
// Copilot API (GitHub Copilot proxy)
|
||||
'copilot',
|
||||
// Cursor IDE (Cursor proxy daemon)
|
||||
|
||||
@@ -121,7 +121,7 @@ export function listClaudeExtensionProfiles(): ClaudeExtensionProfileOption[] {
|
||||
'default',
|
||||
...all.accounts,
|
||||
...all.settings,
|
||||
...all.cliproxy,
|
||||
...all.cliproxy.filter((profileName) => profileName !== 'cursor'),
|
||||
...all.cliproxyVariants,
|
||||
];
|
||||
const deduped = [...new Set(orderedNames)];
|
||||
|
||||
@@ -229,6 +229,20 @@ function parseKiroIDCFlow(raw: unknown): { flow: KiroIDCFlow; invalid: boolean }
|
||||
return { flow: normalizeKiroIDCFlow(normalized), invalid: false };
|
||||
}
|
||||
|
||||
function parseGitLabAuthMode(raw: unknown): { mode: 'oauth' | 'pat'; invalid: boolean } {
|
||||
if (raw === undefined || raw === null || raw === '') {
|
||||
return { mode: 'oauth', invalid: false };
|
||||
}
|
||||
if (typeof raw !== 'string') {
|
||||
return { mode: 'oauth', invalid: true };
|
||||
}
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
if (normalized === 'oauth' || normalized === 'pat') {
|
||||
return { mode: normalized, invalid: false };
|
||||
}
|
||||
return { mode: 'oauth', invalid: true };
|
||||
}
|
||||
|
||||
export function getKiroStartIDCValidationError(options: {
|
||||
kiroMethod: KiroAuthMethod;
|
||||
kiroIDCStartUrl?: string;
|
||||
@@ -603,6 +617,13 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
const kiroIDCRegion =
|
||||
typeof requestBody.kiroIDCRegion === 'string' ? requestBody.kiroIDCRegion.trim() : undefined;
|
||||
const kiroIDCFlowRaw = requestBody.kiroIDCFlow;
|
||||
const gitlabAuthModeRaw = requestBody.gitlabAuthMode;
|
||||
const gitlabBaseUrl =
|
||||
typeof requestBody.gitlabBaseUrl === 'string' ? requestBody.gitlabBaseUrl.trim() : undefined;
|
||||
const gitlabPersonalAccessToken =
|
||||
typeof requestBody.gitlabPersonalAccessToken === 'string'
|
||||
? requestBody.gitlabPersonalAccessToken.trim()
|
||||
: undefined;
|
||||
const riskAcknowledgement = requestBody.riskAcknowledgement;
|
||||
const target = getProxyTarget();
|
||||
if (target.isRemote) {
|
||||
@@ -613,6 +634,8 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
const nickname = nicknameRaw?.trim();
|
||||
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
||||
const { flow: kiroIDCFlow, invalid: invalidKiroIDCFlow } = parseKiroIDCFlow(kiroIDCFlowRaw);
|
||||
const { mode: gitlabAuthMode, invalid: invalidGitLabAuthMode } =
|
||||
parseGitLabAuthMode(gitlabAuthModeRaw);
|
||||
|
||||
// Validate provider
|
||||
if (!validProviders.includes(provider as CLIProxyProvider)) {
|
||||
@@ -628,6 +651,14 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && invalidGitLabAuthMode) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid gitlabAuthMode. Supported: oauth, pat',
|
||||
code: 'INVALID_GITLAB_AUTH_MODE',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'kiro') {
|
||||
const kiroIDCValidationError = getKiroStartIDCValidationError({
|
||||
kiroMethod,
|
||||
@@ -651,6 +682,89 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && gitlabAuthMode === 'pat') {
|
||||
if (!gitlabPersonalAccessToken) {
|
||||
res.status(400).json({
|
||||
error: 'gitlabPersonalAccessToken is required when gitlabAuthMode=pat',
|
||||
code: 'MISSING_GITLAB_PAT',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const localProvider = provider as CLIProxyProvider;
|
||||
const knownTokenFiles = listProviderTokenSnapshots(localProvider);
|
||||
const response = await fetch(buildProxyUrl(target, '/v0/management/gitlab-auth-url'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...buildManagementHeaders(target),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(gitlabBaseUrl ? { base_url: gitlabBaseUrl } : {}),
|
||||
personal_access_token: gitlabPersonalAccessToken,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = (await response.json().catch(() => ({}))) as {
|
||||
status?: string;
|
||||
error?: string;
|
||||
};
|
||||
if (!response.ok || data.status !== 'ok') {
|
||||
res.status(response.ok ? 400 : response.status).json({
|
||||
error: data.error || 'GitLab PAT authentication failed',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenSnapshot = findNewTokenSnapshot(
|
||||
listProviderTokenSnapshots(localProvider),
|
||||
knownTokenFiles
|
||||
);
|
||||
if (!tokenSnapshot) {
|
||||
res.status(409).json({
|
||||
error: 'GitLab PAT authentication completed, but CCS could not find the saved token.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const account = registerAccountFromToken(
|
||||
localProvider,
|
||||
getProviderTokenDir(localProvider),
|
||||
nickname,
|
||||
false,
|
||||
tokenSnapshot.file
|
||||
);
|
||||
if (!account) {
|
||||
res.status(409).json({
|
||||
error: 'GitLab PAT authentication succeeded, but account registration failed.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureManagedModelPrefixes([account.provider]);
|
||||
} catch {
|
||||
// Keep auth success path non-fatal when prefix repair cannot run.
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
account: {
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
nickname: account.nickname,
|
||||
provider: account.provider,
|
||||
isDefault: account.isDefault,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
respondInternalError(res, error, 'Failed to start GitLab PAT flow.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const existingAccounts = getProviderAccounts(provider as CLIProxyProvider);
|
||||
const nicknameError = getStartAuthNicknameError(
|
||||
provider as CLIProxyProvider,
|
||||
@@ -681,6 +795,8 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
kiroIDCStartUrl: provider === 'kiro' ? kiroIDCStartUrl : undefined,
|
||||
kiroIDCRegion: provider === 'kiro' ? kiroIDCRegion : undefined,
|
||||
kiroIDCFlow: provider === 'kiro' && kiroMethod === 'idc' ? kiroIDCFlow : undefined,
|
||||
gitlabAuthMode: provider === 'gitlab' ? gitlabAuthMode : undefined,
|
||||
gitlabBaseUrl: provider === 'gitlab' ? gitlabBaseUrl : undefined,
|
||||
fromUI: true, // Enable project selection prompt in UI
|
||||
noIncognito, // Kiro: use normal browser if enabled
|
||||
});
|
||||
@@ -837,9 +953,14 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
||||
req.body && typeof req.body === 'object' ? (req.body as Record<string, unknown>) : {};
|
||||
const nicknameRaw = typeof requestBody.nickname === 'string' ? requestBody.nickname : undefined;
|
||||
const kiroMethodRaw = requestBody.kiroMethod;
|
||||
const gitlabAuthModeRaw = requestBody.gitlabAuthMode;
|
||||
const gitlabBaseUrl =
|
||||
typeof requestBody.gitlabBaseUrl === 'string' ? requestBody.gitlabBaseUrl.trim() : undefined;
|
||||
const riskAcknowledgement = requestBody.riskAcknowledgement;
|
||||
const nickname = nicknameRaw?.trim();
|
||||
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
||||
const { mode: gitlabAuthMode, invalid: invalidGitLabAuthMode } =
|
||||
parseGitLabAuthMode(gitlabAuthModeRaw);
|
||||
|
||||
// Check remote mode
|
||||
const target = getProxyTarget();
|
||||
@@ -862,6 +983,22 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && invalidGitLabAuthMode) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid gitlabAuthMode. Supported: oauth, pat',
|
||||
code: 'INVALID_GITLAB_AUTH_MODE',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && gitlabAuthMode === 'pat') {
|
||||
res.status(400).json({
|
||||
error: 'GitLab PAT login must use /api/cliproxy/auth/gitlab/start',
|
||||
code: 'GITLAB_PAT_REQUIRES_START',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'agy' && !isAntigravityResponsibilityBypassEnabled()) {
|
||||
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
|
||||
if (!validation.valid) {
|
||||
@@ -900,11 +1037,18 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
||||
provider === 'kiro' && kiroManagementMethod
|
||||
? `&method=${encodeURIComponent(kiroManagementMethod)}`
|
||||
: '';
|
||||
const gitlabQuery =
|
||||
provider === 'gitlab' && gitlabBaseUrl
|
||||
? `&base_url=${encodeURIComponent(gitlabBaseUrl)}`
|
||||
: '';
|
||||
|
||||
// Call CLIProxyAPI to start OAuth and get auth URL
|
||||
// CLIProxyAPI management routes are under /v0/management prefix
|
||||
const response = await fetch(
|
||||
buildProxyUrl(target, `/v0/management/${authUrlProvider}-auth-url?is_webui=true${kiroQuery}`),
|
||||
buildProxyUrl(
|
||||
target,
|
||||
`/v0/management/${authUrlProvider}-auth-url?is_webui=true${kiroQuery}${gitlabQuery}`
|
||||
),
|
||||
{ headers: buildManagementHeaders(target) }
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user