feature/codex-cli

This commit is contained in:
DhanushSantosh
2026-01-06 04:52:25 +05:30
parent 4d4025ca06
commit a57dcc170d
54 changed files with 5562 additions and 91 deletions

View File

@@ -11,8 +11,12 @@ import { createDeleteApiKeyHandler } from './routes/delete-api-key.js';
import { createApiKeysHandler } from './routes/api-keys.js';
import { createPlatformHandler } from './routes/platform.js';
import { createVerifyClaudeAuthHandler } from './routes/verify-claude-auth.js';
import { createVerifyCodexAuthHandler } from './routes/verify-codex-auth.js';
import { createGhStatusHandler } from './routes/gh-status.js';
import { createCursorStatusHandler } from './routes/cursor-status.js';
import { createCodexStatusHandler } from './routes/codex-status.js';
import { createInstallCodexHandler } from './routes/install-codex.js';
import { createAuthCodexHandler } from './routes/auth-codex.js';
import {
createGetCursorConfigHandler,
createSetCursorDefaultModelHandler,
@@ -35,10 +39,16 @@ export function createSetupRoutes(): Router {
router.get('/api-keys', createApiKeysHandler());
router.get('/platform', createPlatformHandler());
router.post('/verify-claude-auth', createVerifyClaudeAuthHandler());
router.post('/verify-codex-auth', createVerifyCodexAuthHandler());
router.get('/gh-status', createGhStatusHandler());
// Cursor CLI routes
router.get('/cursor-status', createCursorStatusHandler());
// Codex CLI routes
router.get('/codex-status', createCodexStatusHandler());
router.post('/install-codex', createInstallCodexHandler());
router.post('/auth-codex', createAuthCodexHandler());
router.get('/cursor-config', createGetCursorConfigHandler());
router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler());
router.post('/cursor-config/models', createSetCursorModelsHandler());

View File

@@ -0,0 +1,31 @@
/**
* POST /auth-codex endpoint - Authenticate Codex CLI
*/
import type { Request, Response } from 'express';
import { logError, getErrorMessage } from '../common.js';
/**
* Creates handler for POST /api/setup/auth-codex
* Returns instructions for manual Codex CLI authentication
*/
export function createAuthCodexHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
const loginCommand = 'codex login';
res.json({
success: true,
requiresManualAuth: true,
command: loginCommand,
message: `Please authenticate Codex CLI manually by running: ${loginCommand}`,
});
} catch (error) {
logError(error, 'Auth Codex failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,43 @@
/**
* GET /codex-status endpoint - Get Codex CLI installation and auth status
*/
import type { Request, Response } from 'express';
import { CodexProvider } from '../../../providers/codex-provider.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Creates handler for GET /api/setup/codex-status
* Returns Codex CLI installation and authentication status
*/
export function createCodexStatusHandler() {
const installCommand = 'npm install -g @openai/codex';
const loginCommand = 'codex login';
return async (_req: Request, res: Response): Promise<void> => {
try {
const provider = new CodexProvider();
const status = await provider.detectInstallation();
res.json({
success: true,
installed: status.installed,
version: status.version || null,
path: status.path || null,
auth: {
authenticated: status.authenticated || false,
method: status.method || 'cli',
hasApiKey: status.hasApiKey || false,
},
installCommand,
loginCommand,
});
} catch (error) {
logError(error, 'Get Codex status failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,33 @@
/**
* POST /install-codex endpoint - Install Codex CLI
*/
import type { Request, Response } from 'express';
import { logError, getErrorMessage } from '../common.js';
/**
* Creates handler for POST /api/setup/install-codex
* Installs Codex CLI (currently returns instructions for manual install)
*/
export function createInstallCodexHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
// For now, return manual installation instructions
// In the future, this could potentially trigger npm global install
const installCommand = 'npm install -g @openai/codex';
res.json({
success: true,
message: `Please install Codex CLI manually by running: ${installCommand}`,
requiresManualInstall: true,
installCommand,
});
} catch (error) {
logError(error, 'Install Codex failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,232 @@
/**
* POST /verify-codex-auth endpoint - Verify Codex authentication
*/
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import { CODEX_MODEL_MAP } from '@automaker/types';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import { getApiKey } from '../common.js';
import { getCodexAuthIndicators } from '@automaker/platform';
const logger = createLogger('Setup');
const OPENAI_API_KEY_ENV = 'OPENAI_API_KEY';
const AUTH_PROMPT = "Reply with only the word 'ok'";
const AUTH_TIMEOUT_MS = 30000;
const ERROR_BILLING_MESSAGE =
'Credit balance is too low. Please add credits to your OpenAI account.';
const ERROR_RATE_LIMIT_MESSAGE =
'Rate limit reached. Please wait a while before trying again or upgrade your plan.';
const ERROR_CLI_AUTH_REQUIRED =
"CLI authentication failed. Please run 'codex login' to authenticate.";
const ERROR_API_KEY_REQUIRED = 'No API key configured. Please enter an API key first.';
const AUTH_ERROR_PATTERNS = [
'authentication',
'unauthorized',
'invalid_api_key',
'invalid api key',
'api key is invalid',
'not authenticated',
'login',
'auth(',
'token refresh',
'tokenrefresh',
'failed to parse server response',
'transport channel closed',
];
const BILLING_ERROR_PATTERNS = [
'credit balance is too low',
'credit balance too low',
'insufficient credits',
'insufficient balance',
'no credits',
'out of credits',
'billing',
'payment required',
'add credits',
];
const RATE_LIMIT_PATTERNS = [
'limit reached',
'rate limit',
'rate_limit',
'too many requests',
'resets',
'429',
];
function containsAuthError(text: string): boolean {
const lowerText = text.toLowerCase();
return AUTH_ERROR_PATTERNS.some((pattern) => lowerText.includes(pattern));
}
function isBillingError(text: string): boolean {
const lowerText = text.toLowerCase();
return BILLING_ERROR_PATTERNS.some((pattern) => lowerText.includes(pattern));
}
function isRateLimitError(text: string): boolean {
if (isBillingError(text)) {
return false;
}
const lowerText = text.toLowerCase();
return RATE_LIMIT_PATTERNS.some((pattern) => lowerText.includes(pattern));
}
export function createVerifyCodexAuthHandler() {
return async (req: Request, res: Response): Promise<void> => {
const { authMethod } = req.body as { authMethod?: 'cli' | 'api_key' };
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), AUTH_TIMEOUT_MS);
const originalKey = process.env[OPENAI_API_KEY_ENV];
try {
if (authMethod === 'cli') {
delete process.env[OPENAI_API_KEY_ENV];
} else if (authMethod === 'api_key') {
const storedApiKey = getApiKey('openai');
if (storedApiKey) {
process.env[OPENAI_API_KEY_ENV] = storedApiKey;
} else if (!process.env[OPENAI_API_KEY_ENV]) {
res.json({ success: true, authenticated: false, error: ERROR_API_KEY_REQUIRED });
return;
}
}
if (authMethod === 'cli') {
const authIndicators = await getCodexAuthIndicators();
if (!authIndicators.hasOAuthToken && !authIndicators.hasApiKey) {
res.json({
success: true,
authenticated: false,
error: ERROR_CLI_AUTH_REQUIRED,
});
return;
}
}
// Use Codex provider explicitly (not ProviderFactory.getProviderForModel)
// because Cursor also supports GPT models and has higher priority
const provider = ProviderFactory.getProviderByName('codex');
if (!provider) {
throw new Error('Codex provider not available');
}
const stream = provider.executeQuery({
prompt: AUTH_PROMPT,
model: CODEX_MODEL_MAP.gpt52Codex,
cwd: process.cwd(),
maxTurns: 1,
allowedTools: [],
abortController,
});
let receivedAnyContent = false;
let errorMessage = '';
for await (const msg of stream) {
if (msg.type === 'error' && msg.error) {
if (isBillingError(msg.error)) {
errorMessage = ERROR_BILLING_MESSAGE;
} else if (isRateLimitError(msg.error)) {
errorMessage = ERROR_RATE_LIMIT_MESSAGE;
} else {
errorMessage = msg.error;
}
break;
}
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text' && block.text) {
receivedAnyContent = true;
if (isBillingError(block.text)) {
errorMessage = ERROR_BILLING_MESSAGE;
break;
}
if (isRateLimitError(block.text)) {
errorMessage = ERROR_RATE_LIMIT_MESSAGE;
break;
}
if (containsAuthError(block.text)) {
errorMessage = block.text;
break;
}
}
}
}
if (msg.type === 'result' && msg.result) {
receivedAnyContent = true;
if (isBillingError(msg.result)) {
errorMessage = ERROR_BILLING_MESSAGE;
} else if (isRateLimitError(msg.result)) {
errorMessage = ERROR_RATE_LIMIT_MESSAGE;
} else if (containsAuthError(msg.result)) {
errorMessage = msg.result;
break;
}
}
}
if (errorMessage) {
// Rate limit and billing errors mean auth succeeded but usage is limited
const isUsageLimitError =
errorMessage === ERROR_BILLING_MESSAGE || errorMessage === ERROR_RATE_LIMIT_MESSAGE;
const response: {
success: boolean;
authenticated: boolean;
error: string;
details?: string;
} = {
success: true,
authenticated: isUsageLimitError ? true : false,
error: isUsageLimitError
? errorMessage
: authMethod === 'cli'
? ERROR_CLI_AUTH_REQUIRED
: 'API key is invalid or has been revoked.',
};
// Include detailed error for auth failures so users can debug
if (!isUsageLimitError && errorMessage !== response.error) {
response.details = errorMessage;
}
res.json(response);
return;
}
if (!receivedAnyContent) {
res.json({
success: true,
authenticated: false,
error: 'No response received from Codex. Please check your authentication.',
});
return;
}
res.json({ success: true, authenticated: true });
} catch (error: unknown) {
const errMessage = error instanceof Error ? error.message : String(error);
logger.error('[Setup] Codex auth verification error:', errMessage);
const normalizedError = isBillingError(errMessage)
? ERROR_BILLING_MESSAGE
: isRateLimitError(errMessage)
? ERROR_RATE_LIMIT_MESSAGE
: errMessage;
res.json({
success: true,
authenticated: false,
error: normalizedError,
});
} finally {
clearTimeout(timeoutId);
if (originalKey !== undefined) {
process.env[OPENAI_API_KEY_ENV] = originalKey;
} else {
delete process.env[OPENAI_API_KEY_ENV];
}
}
};
}