mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
Merge branch 'main' into various-improvements
This commit is contained in:
@@ -1,71 +1,185 @@
|
||||
const { execSync } = require('child_process');
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* Claude CLI Detector
|
||||
*
|
||||
* Authentication options:
|
||||
* 1. OAuth Token (Subscription): User runs `claude setup-token` and provides the token to the app
|
||||
* 2. API Key (Pay-per-use): User provides their Anthropic API key directly
|
||||
*/
|
||||
class ClaudeCliDetector {
|
||||
/**
|
||||
* Check if Claude Code CLI is installed and accessible
|
||||
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'sdk'|'none' }
|
||||
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'none' }
|
||||
*/
|
||||
static detectClaudeInstallation() {
|
||||
try {
|
||||
// Method 1: Check if 'claude' command is in PATH
|
||||
try {
|
||||
const claudePath = execSync('which claude', { encoding: 'utf-8' }).trim();
|
||||
const version = execSync('claude --version', { encoding: 'utf-8' }).trim();
|
||||
return {
|
||||
installed: true,
|
||||
path: claudePath,
|
||||
version: version,
|
||||
method: 'cli'
|
||||
};
|
||||
} catch (error) {
|
||||
// CLI not in PATH, check local installation
|
||||
}
|
||||
|
||||
// Method 2: Check for local installation
|
||||
const localClaudePath = path.join(os.homedir(), '.claude', 'local', 'claude');
|
||||
if (fs.existsSync(localClaudePath)) {
|
||||
/**
|
||||
* Try to get updated PATH from shell config files
|
||||
* This helps detect CLI installations that modify shell config but haven't updated the current process PATH
|
||||
*/
|
||||
static getUpdatedPathFromShellConfig() {
|
||||
const homeDir = os.homedir();
|
||||
const shell = process.env.SHELL || '/bin/bash';
|
||||
const shellName = path.basename(shell);
|
||||
|
||||
// Common shell config files
|
||||
const configFiles = [];
|
||||
if (shellName.includes('zsh')) {
|
||||
configFiles.push(path.join(homeDir, '.zshrc'));
|
||||
configFiles.push(path.join(homeDir, '.zshenv'));
|
||||
configFiles.push(path.join(homeDir, '.zprofile'));
|
||||
} else if (shellName.includes('bash')) {
|
||||
configFiles.push(path.join(homeDir, '.bashrc'));
|
||||
configFiles.push(path.join(homeDir, '.bash_profile'));
|
||||
configFiles.push(path.join(homeDir, '.profile'));
|
||||
}
|
||||
|
||||
// Also check common locations
|
||||
const commonPaths = [
|
||||
path.join(homeDir, '.local', 'bin'),
|
||||
path.join(homeDir, '.cargo', 'bin'),
|
||||
'/usr/local/bin',
|
||||
'/opt/homebrew/bin',
|
||||
path.join(homeDir, 'bin'),
|
||||
];
|
||||
|
||||
// Try to extract PATH additions from config files
|
||||
for (const configFile of configFiles) {
|
||||
if (fs.existsSync(configFile)) {
|
||||
try {
|
||||
const version = execSync(`${localClaudePath} --version`, { encoding: 'utf-8' }).trim();
|
||||
return {
|
||||
installed: true,
|
||||
path: localClaudePath,
|
||||
version: version,
|
||||
method: 'cli-local'
|
||||
};
|
||||
const content = fs.readFileSync(configFile, 'utf-8');
|
||||
// Look for PATH exports that might include claude installation paths
|
||||
const pathMatches = content.match(/export\s+PATH=["']?([^"'\n]+)["']?/g);
|
||||
if (pathMatches) {
|
||||
for (const match of pathMatches) {
|
||||
const pathValue = match.replace(/export\s+PATH=["']?/, '').replace(/["']?$/, '');
|
||||
const paths = pathValue.split(':').filter(p => p && !p.includes('$'));
|
||||
commonPaths.push(...paths);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Local CLI exists but may not be executable
|
||||
// Ignore errors reading config files
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(commonPaths)]; // Remove duplicates
|
||||
}
|
||||
|
||||
static detectClaudeInstallation() {
|
||||
console.log('[ClaudeCliDetector] Detecting Claude installation...');
|
||||
|
||||
try {
|
||||
// Method 1: Check if 'claude' command is in PATH (Unix)
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
const claudePath = execSync('which claude 2>/dev/null', { encoding: 'utf-8' }).trim();
|
||||
if (claudePath) {
|
||||
const version = this.getClaudeVersion(claudePath);
|
||||
console.log('[ClaudeCliDetector] Found claude at:', claudePath, 'version:', version);
|
||||
return {
|
||||
installed: true,
|
||||
path: claudePath,
|
||||
version: version,
|
||||
method: 'cli'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// CLI not in PATH, continue checking other locations
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Check Windows path
|
||||
// Method 2: Check Windows path
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
const claudePath = execSync('where claude', { encoding: 'utf-8' }).trim();
|
||||
const version = execSync('claude --version', { encoding: 'utf-8' }).trim();
|
||||
return {
|
||||
installed: true,
|
||||
path: claudePath,
|
||||
version: version,
|
||||
method: 'cli'
|
||||
};
|
||||
const claudePath = execSync('where claude 2>nul', { encoding: 'utf-8' }).trim().split('\n')[0];
|
||||
if (claudePath) {
|
||||
const version = this.getClaudeVersion(claudePath);
|
||||
console.log('[ClaudeCliDetector] Found claude at:', claudePath, 'version:', version);
|
||||
return {
|
||||
installed: true,
|
||||
path: claudePath,
|
||||
version: version,
|
||||
method: 'cli'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Not found
|
||||
// Not found on Windows
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: SDK mode (using OAuth token)
|
||||
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||
// Method 3: Check for local installation
|
||||
const localClaudePath = path.join(os.homedir(), '.claude', 'local', 'claude');
|
||||
if (fs.existsSync(localClaudePath)) {
|
||||
const version = this.getClaudeVersion(localClaudePath);
|
||||
console.log('[ClaudeCliDetector] Found local claude at:', localClaudePath, 'version:', version);
|
||||
return {
|
||||
installed: true,
|
||||
path: null,
|
||||
version: 'SDK Mode',
|
||||
method: 'sdk'
|
||||
path: localClaudePath,
|
||||
version: version,
|
||||
method: 'cli-local'
|
||||
};
|
||||
}
|
||||
|
||||
// Method 4: Check common installation locations (including those from shell config)
|
||||
const commonPaths = this.getUpdatedPathFromShellConfig();
|
||||
const binaryNames = ['claude', 'claude-code'];
|
||||
|
||||
for (const basePath of commonPaths) {
|
||||
for (const binaryName of binaryNames) {
|
||||
const claudePath = path.join(basePath, binaryName);
|
||||
if (fs.existsSync(claudePath)) {
|
||||
try {
|
||||
const version = this.getClaudeVersion(claudePath);
|
||||
console.log('[ClaudeCliDetector] Found claude at:', claudePath, 'version:', version);
|
||||
return {
|
||||
installed: true,
|
||||
path: claudePath,
|
||||
version: version,
|
||||
method: 'cli'
|
||||
};
|
||||
} catch (error) {
|
||||
// File exists but can't get version, might not be executable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 5: Try to source shell config and check PATH again (for Unix)
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
const shell = process.env.SHELL || '/bin/bash';
|
||||
const shellName = path.basename(shell);
|
||||
const homeDir = os.homedir();
|
||||
|
||||
let sourceCmd = '';
|
||||
if (shellName.includes('zsh')) {
|
||||
sourceCmd = `source ${homeDir}/.zshrc 2>/dev/null && which claude`;
|
||||
} else if (shellName.includes('bash')) {
|
||||
sourceCmd = `source ${homeDir}/.bashrc 2>/dev/null && which claude`;
|
||||
}
|
||||
|
||||
if (sourceCmd) {
|
||||
const claudePath = execSync(`bash -c "${sourceCmd}"`, { encoding: 'utf-8', timeout: 2000 }).trim();
|
||||
if (claudePath && claudePath.startsWith('/')) {
|
||||
const version = this.getClaudeVersion(claudePath);
|
||||
console.log('[ClaudeCliDetector] Found claude via shell config at:', claudePath, 'version:', version);
|
||||
return {
|
||||
installed: true,
|
||||
path: claudePath,
|
||||
version: version,
|
||||
method: 'cli'
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Failed to source shell config or find claude
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[ClaudeCliDetector] Claude CLI not found');
|
||||
return {
|
||||
installed: false,
|
||||
path: null,
|
||||
@@ -85,35 +199,223 @@ class ClaudeCliDetector {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installation recommendations
|
||||
* Get Claude CLI version
|
||||
* @param {string} claudePath Path to claude executable
|
||||
* @returns {string|null} Version string or null
|
||||
*/
|
||||
static getInstallationInfo() {
|
||||
static getClaudeVersion(claudePath) {
|
||||
try {
|
||||
const version = execSync(`"${claudePath}" --version 2>/dev/null`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000
|
||||
}).trim();
|
||||
return version || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authentication status
|
||||
* Checks for:
|
||||
* 1. OAuth token stored in app's credentials (from `claude setup-token`)
|
||||
* 2. API key stored in app's credentials
|
||||
* 3. API key in environment variable
|
||||
*
|
||||
* @param {string} appCredentialsPath Path to app's credentials.json
|
||||
* @returns {Object} Authentication status
|
||||
*/
|
||||
static getAuthStatus(appCredentialsPath) {
|
||||
console.log('[ClaudeCliDetector] Checking auth status...');
|
||||
|
||||
const envApiKey = process.env.ANTHROPIC_API_KEY;
|
||||
console.log('[ClaudeCliDetector] Env ANTHROPIC_API_KEY:', !!envApiKey);
|
||||
|
||||
// Check app's stored credentials
|
||||
let storedOAuthToken = null;
|
||||
let storedApiKey = null;
|
||||
|
||||
if (appCredentialsPath && fs.existsSync(appCredentialsPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(appCredentialsPath, 'utf-8');
|
||||
const credentials = JSON.parse(content);
|
||||
storedOAuthToken = credentials.anthropic_oauth_token || null;
|
||||
storedApiKey = credentials.anthropic || credentials.anthropic_api_key || null;
|
||||
console.log('[ClaudeCliDetector] App credentials:', {
|
||||
hasOAuthToken: !!storedOAuthToken,
|
||||
hasApiKey: !!storedApiKey
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ClaudeCliDetector] Error reading app credentials:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine authentication method
|
||||
// Priority: Stored OAuth Token > Stored API Key > Env API Key
|
||||
let authenticated = false;
|
||||
let method = 'none';
|
||||
|
||||
if (storedOAuthToken) {
|
||||
authenticated = true;
|
||||
method = 'oauth_token';
|
||||
console.log('[ClaudeCliDetector] Using stored OAuth token (subscription)');
|
||||
} else if (storedApiKey) {
|
||||
authenticated = true;
|
||||
method = 'api_key';
|
||||
console.log('[ClaudeCliDetector] Using stored API key');
|
||||
} else if (envApiKey) {
|
||||
authenticated = true;
|
||||
method = 'api_key_env';
|
||||
console.log('[ClaudeCliDetector] Using environment API key');
|
||||
} else {
|
||||
console.log('[ClaudeCliDetector] No authentication found');
|
||||
}
|
||||
|
||||
const result = {
|
||||
authenticated,
|
||||
method,
|
||||
hasStoredOAuthToken: !!storedOAuthToken,
|
||||
hasStoredApiKey: !!storedApiKey,
|
||||
hasEnvApiKey: !!envApiKey
|
||||
};
|
||||
|
||||
console.log('[ClaudeCliDetector] Auth status result:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full status including installation and auth
|
||||
* @param {string} appCredentialsPath Path to app's credentials.json
|
||||
* @returns {Object} Full status
|
||||
*/
|
||||
static getFullStatus(appCredentialsPath) {
|
||||
const installation = this.detectClaudeInstallation();
|
||||
const auth = this.getAuthStatus(appCredentialsPath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: installation.installed ? 'installed' : 'not_installed',
|
||||
installed: installation.installed,
|
||||
path: installation.path,
|
||||
version: installation.version,
|
||||
method: installation.method,
|
||||
auth
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installation commands for different platforms
|
||||
* @returns {Object} Installation commands
|
||||
*/
|
||||
static getInstallCommands() {
|
||||
return {
|
||||
macos: 'curl -fsSL https://claude.ai/install.sh | bash',
|
||||
windows: 'irm https://claude.ai/install.ps1 | iex',
|
||||
linux: 'curl -fsSL https://claude.ai/install.sh | bash'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Claude CLI using the official script
|
||||
* @param {Function} onProgress Callback for progress updates
|
||||
* @returns {Promise<Object>} Installation result
|
||||
*/
|
||||
static async installCli(onProgress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const platform = process.platform;
|
||||
let command, args;
|
||||
|
||||
if (platform === 'win32') {
|
||||
command = 'powershell';
|
||||
args = ['-Command', 'irm https://claude.ai/install.ps1 | iex'];
|
||||
} else {
|
||||
command = 'bash';
|
||||
args = ['-c', 'curl -fsSL https://claude.ai/install.sh | bash'];
|
||||
}
|
||||
|
||||
console.log('[ClaudeCliDetector] Installing Claude CLI...');
|
||||
|
||||
const proc = spawn(command, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: false
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
if (onProgress) {
|
||||
onProgress({ type: 'stdout', data: text });
|
||||
}
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
errorOutput += text;
|
||||
if (onProgress) {
|
||||
onProgress({ type: 'stderr', data: text });
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('[ClaudeCliDetector] Installation completed successfully');
|
||||
resolve({
|
||||
success: true,
|
||||
output,
|
||||
message: 'Claude CLI installed successfully'
|
||||
});
|
||||
} else {
|
||||
console.error('[ClaudeCliDetector] Installation failed with code:', code);
|
||||
reject({
|
||||
success: false,
|
||||
error: errorOutput || `Installation failed with code ${code}`,
|
||||
output
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (error) => {
|
||||
console.error('[ClaudeCliDetector] Installation error:', error);
|
||||
reject({
|
||||
success: false,
|
||||
error: error.message,
|
||||
output
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instructions for setup-token command
|
||||
* @returns {Object} Setup token instructions
|
||||
*/
|
||||
static getSetupTokenInstructions() {
|
||||
const detection = this.detectClaudeInstallation();
|
||||
|
||||
if (detection.installed) {
|
||||
|
||||
if (!detection.installed) {
|
||||
return {
|
||||
status: 'installed',
|
||||
method: detection.method,
|
||||
version: detection.version,
|
||||
path: detection.path,
|
||||
recommendation: detection.method === 'cli'
|
||||
? 'Using Claude Code CLI - optimal for long-running tasks'
|
||||
: 'Using SDK mode - works well but CLI may provide better performance'
|
||||
success: false,
|
||||
error: 'Claude CLI is not installed. Please install it first.',
|
||||
installCommands: this.getInstallCommands()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'not_installed',
|
||||
recommendation: 'Consider installing Claude Code CLI for better performance with ultrathink',
|
||||
installCommands: {
|
||||
macos: 'curl -fsSL claude.ai/install.sh | bash',
|
||||
windows: 'irm https://claude.ai/install.ps1 | iex',
|
||||
linux: 'curl -fsSL claude.ai/install.sh | bash',
|
||||
npm: 'npm install -g @anthropic-ai/claude-code'
|
||||
}
|
||||
success: true,
|
||||
command: 'claude setup-token',
|
||||
instructions: [
|
||||
'1. Open your terminal',
|
||||
'2. Run: claude setup-token',
|
||||
'3. Follow the prompts to authenticate',
|
||||
'4. Copy the token that is displayed',
|
||||
'5. Paste the token in the field below'
|
||||
],
|
||||
note: 'This token is from your Claude subscription and allows you to use Claude without API charges.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClaudeCliDetector;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { execSync } = require('child_process');
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
@@ -11,6 +11,205 @@ const os = require('os');
|
||||
* for code generation and agentic tasks.
|
||||
*/
|
||||
class CodexCliDetector {
|
||||
/**
|
||||
* Get the path to Codex config directory
|
||||
* @returns {string} Path to .codex directory
|
||||
*/
|
||||
static getConfigDir() {
|
||||
return path.join(os.homedir(), '.codex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to Codex auth file
|
||||
* @returns {string} Path to auth.json
|
||||
*/
|
||||
static getAuthPath() {
|
||||
return path.join(this.getConfigDir(), 'auth.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Codex authentication status
|
||||
* @returns {Object} Authentication status
|
||||
*/
|
||||
static checkAuth() {
|
||||
console.log('[CodexCliDetector] Checking auth status...');
|
||||
try {
|
||||
const authPath = this.getAuthPath();
|
||||
const envApiKey = process.env.OPENAI_API_KEY;
|
||||
console.log('[CodexCliDetector] Auth path:', authPath);
|
||||
console.log('[CodexCliDetector] Has env API key:', !!envApiKey);
|
||||
|
||||
// First, try to verify authentication using codex CLI command if available
|
||||
try {
|
||||
const detection = this.detectCodexInstallation();
|
||||
if (detection.installed) {
|
||||
try {
|
||||
// Use 'codex login status' to verify authentication
|
||||
const statusOutput = execSync(`"${detection.path || 'codex'}" login status 2>/dev/null`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
// If command succeeds and shows logged in status
|
||||
if (statusOutput && (statusOutput.includes('Logged in') || statusOutput.includes('Authenticated'))) {
|
||||
const result = {
|
||||
authenticated: true,
|
||||
method: 'cli_verified',
|
||||
hasAuthFile: fs.existsSync(authPath),
|
||||
hasEnvKey: !!envApiKey,
|
||||
authPath
|
||||
};
|
||||
console.log('[CodexCliDetector] Auth result (cli_verified):', result);
|
||||
return result;
|
||||
}
|
||||
} catch (statusError) {
|
||||
// status command failed, continue with file-based check
|
||||
}
|
||||
}
|
||||
} catch (verifyError) {
|
||||
// CLI verification failed, continue with file-based check
|
||||
}
|
||||
|
||||
// Check if auth file exists
|
||||
if (fs.existsSync(authPath)) {
|
||||
const content = fs.readFileSync(authPath, 'utf-8');
|
||||
const auth = JSON.parse(content);
|
||||
|
||||
// Check for token object structure (from codex auth login)
|
||||
// Structure: { token: { Id_token, access_token, refresh_token }, last_refresh: ... }
|
||||
if (auth.token && typeof auth.token === 'object') {
|
||||
const token = auth.token;
|
||||
if (token.Id_token || token.access_token || token.refresh_token || token.id_token) {
|
||||
return {
|
||||
authenticated: true,
|
||||
method: 'auth_file',
|
||||
hasAuthFile: true,
|
||||
hasEnvKey: !!envApiKey,
|
||||
authPath
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for various possible auth fields that codex might use
|
||||
if (auth.api_key || auth.openai_api_key || auth.access_token || auth.apiKey) {
|
||||
return {
|
||||
authenticated: true,
|
||||
method: 'auth_file',
|
||||
hasAuthFile: true,
|
||||
hasEnvKey: !!envApiKey,
|
||||
authPath
|
||||
};
|
||||
}
|
||||
|
||||
// Also check if the file has any meaningful content (non-empty object)
|
||||
const keys = Object.keys(auth);
|
||||
if (keys.length > 0) {
|
||||
// File exists and has content, likely authenticated
|
||||
// Try to verify by checking if codex command works
|
||||
try {
|
||||
const detection = this.detectCodexInstallation();
|
||||
if (detection.installed) {
|
||||
// Try to verify auth by running a simple command
|
||||
try {
|
||||
execSync(`"${detection.path || 'codex'}" --version 2>/dev/null`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 3000
|
||||
});
|
||||
// If command succeeds, assume authenticated
|
||||
return {
|
||||
authenticated: true,
|
||||
method: 'auth_file',
|
||||
hasAuthFile: true,
|
||||
hasEnvKey: !!envApiKey,
|
||||
authPath
|
||||
};
|
||||
} catch (cmdError) {
|
||||
// Command failed, but file exists - might still be authenticated
|
||||
// Return authenticated if file has content
|
||||
return {
|
||||
authenticated: true,
|
||||
method: 'auth_file',
|
||||
hasAuthFile: true,
|
||||
hasEnvKey: !!envApiKey,
|
||||
authPath
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (verifyError) {
|
||||
// Verification failed, but file exists with content
|
||||
return {
|
||||
authenticated: true,
|
||||
method: 'auth_file',
|
||||
hasAuthFile: true,
|
||||
hasEnvKey: !!envApiKey,
|
||||
authPath
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check environment variable
|
||||
if (envApiKey) {
|
||||
const result = {
|
||||
authenticated: true,
|
||||
method: 'env_var',
|
||||
hasAuthFile: false,
|
||||
hasEnvKey: true,
|
||||
authPath
|
||||
};
|
||||
console.log('[CodexCliDetector] Auth result (env_var):', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// If auth file exists but we didn't find standard keys,
|
||||
// check if codex CLI is installed and try to verify auth
|
||||
if (fs.existsSync(authPath)) {
|
||||
try {
|
||||
const detection = this.detectCodexInstallation();
|
||||
if (detection.installed) {
|
||||
// Auth file exists and CLI is installed - likely authenticated
|
||||
// The file existing is a good indicator that login was successful
|
||||
return {
|
||||
authenticated: true,
|
||||
method: 'auth_file',
|
||||
hasAuthFile: true,
|
||||
hasEnvKey: !!envApiKey,
|
||||
authPath
|
||||
};
|
||||
}
|
||||
} catch (verifyError) {
|
||||
// Verification attempt failed, but file exists
|
||||
// Assume authenticated if file exists
|
||||
return {
|
||||
authenticated: true,
|
||||
method: 'auth_file',
|
||||
hasAuthFile: true,
|
||||
hasEnvKey: !!envApiKey,
|
||||
authPath
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
authenticated: false,
|
||||
method: 'none',
|
||||
hasAuthFile: false,
|
||||
hasEnvKey: false,
|
||||
authPath
|
||||
};
|
||||
console.log('[CodexCliDetector] Auth result (not authenticated):', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[CodexCliDetector] Error checking auth:', error);
|
||||
const result = {
|
||||
authenticated: false,
|
||||
method: 'none',
|
||||
error: error.message
|
||||
};
|
||||
console.log('[CodexCliDetector] Auth result (error):', result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if Codex CLI is installed and accessible
|
||||
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'npm'|'brew'|'none' }
|
||||
@@ -224,6 +423,171 @@ class CodexCliDetector {
|
||||
static getDefaultModel() {
|
||||
return 'gpt-5.1-codex-max';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive installation info including auth status
|
||||
* @returns {Object} Full status object
|
||||
*/
|
||||
static getFullStatus() {
|
||||
const installation = this.detectCodexInstallation();
|
||||
const auth = this.checkAuth();
|
||||
const info = this.getInstallationInfo();
|
||||
|
||||
return {
|
||||
...info,
|
||||
auth,
|
||||
installation
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Codex CLI using npm
|
||||
* @param {Function} onProgress Callback for progress updates
|
||||
* @returns {Promise<Object>} Installation result
|
||||
*/
|
||||
static async installCli(onProgress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const command = 'npm';
|
||||
const args = ['install', '-g', '@openai/codex@latest'];
|
||||
|
||||
const proc = spawn(command, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: true
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
if (onProgress) {
|
||||
onProgress({ type: 'stdout', data: text });
|
||||
}
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
errorOutput += text;
|
||||
// npm often outputs progress to stderr
|
||||
if (onProgress) {
|
||||
onProgress({ type: 'stderr', data: text });
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({
|
||||
success: true,
|
||||
output,
|
||||
message: 'Codex CLI installed successfully'
|
||||
});
|
||||
} else {
|
||||
reject({
|
||||
success: false,
|
||||
error: errorOutput || `Installation failed with code ${code}`,
|
||||
output
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (error) => {
|
||||
reject({
|
||||
success: false,
|
||||
error: error.message,
|
||||
output
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate Codex CLI - opens browser for OAuth or stores API key
|
||||
* @param {string} apiKey Optional API key to store
|
||||
* @param {Function} onProgress Callback for progress updates
|
||||
* @returns {Promise<Object>} Authentication result
|
||||
*/
|
||||
static async authenticate(apiKey, onProgress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const detection = this.detectCodexInstallation();
|
||||
|
||||
if (!detection.installed) {
|
||||
reject({
|
||||
success: false,
|
||||
error: 'Codex CLI is not installed'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const codexPath = detection.path || 'codex';
|
||||
|
||||
if (apiKey) {
|
||||
// Store API key directly using codex auth command
|
||||
const proc = spawn(codexPath, ['auth', 'login', '--api-key', apiKey], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: false
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
if (onProgress) {
|
||||
onProgress({ type: 'stdout', data: text });
|
||||
}
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
errorOutput += text;
|
||||
if (onProgress) {
|
||||
onProgress({ type: 'stderr', data: text });
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({
|
||||
success: true,
|
||||
output,
|
||||
message: 'Codex CLI authenticated successfully'
|
||||
});
|
||||
} else {
|
||||
reject({
|
||||
success: false,
|
||||
error: errorOutput || `Authentication failed with code ${code}`,
|
||||
output
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (error) => {
|
||||
reject({
|
||||
success: false,
|
||||
error: error.message,
|
||||
output
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Require manual authentication
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
type: 'info',
|
||||
data: 'Please run the following command in your terminal to authenticate:\n\ncodex auth login\n\nThen return here to continue setup.'
|
||||
});
|
||||
}
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
command: `${codexPath} auth login`,
|
||||
message: 'Please authenticate Codex CLI manually'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodexCliDetector;
|
||||
|
||||
@@ -412,16 +412,16 @@ class FeatureExecutor {
|
||||
if (provider?.ensureAuthEnv && !provider.ensureAuthEnv()) {
|
||||
// Check if CLI is installed to provide better error message
|
||||
let authMsg =
|
||||
"Missing Anthropic auth. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.";
|
||||
"Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication.";
|
||||
try {
|
||||
const claudeCliDetector = require("./claude-cli-detector");
|
||||
const detection = claudeCliDetector.detectClaudeInstallation();
|
||||
if (detection.installed && detection.method === "cli") {
|
||||
authMsg =
|
||||
"Claude CLI is installed but not authenticated. Run `claude login` to authenticate, or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.";
|
||||
"Claude CLI is installed but not authenticated. Go to Settings > Setup to provide your subscription token (from `claude setup-token`) or API key.";
|
||||
} else {
|
||||
authMsg =
|
||||
"Missing Anthropic auth. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN, or install Claude CLI and run `claude login`.";
|
||||
"Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication, or set ANTHROPIC_API_KEY environment variable.";
|
||||
}
|
||||
} catch (err) {
|
||||
// Fallback to default message
|
||||
|
||||
@@ -94,9 +94,42 @@ class ClaudeProvider extends ModelProvider {
|
||||
this.sdk = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to load credentials from the app's own credentials.json file.
|
||||
* This is where we store OAuth tokens and API keys that users enter in the setup wizard.
|
||||
* Returns { oauthToken, apiKey } or null values if not found.
|
||||
*/
|
||||
loadTokenFromAppCredentials() {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { app } = require('electron');
|
||||
const credentialsPath = path.join(app.getPath('userData'), 'credentials.json');
|
||||
|
||||
if (!fs.existsSync(credentialsPath)) {
|
||||
console.log('[ClaudeProvider] App credentials file does not exist:', credentialsPath);
|
||||
return { oauthToken: null, apiKey: null };
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(credentialsPath, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
// Check for OAuth token first (from claude setup-token), then API key
|
||||
const oauthToken = parsed.anthropic_oauth_token || null;
|
||||
const apiKey = parsed.anthropic || parsed.anthropic_api_key || null;
|
||||
|
||||
console.log('[ClaudeProvider] App credentials check - OAuth token:', !!oauthToken, ', API key:', !!apiKey);
|
||||
return { oauthToken, apiKey };
|
||||
} catch (err) {
|
||||
console.warn('[ClaudeProvider] Failed to read app credentials:', err?.message);
|
||||
return { oauthToken: null, apiKey: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to load a Claude OAuth token from the local CLI config (~/.claude/config.json).
|
||||
* Returns the token string or null if not found.
|
||||
* NOTE: Claude's credentials.json is encrypted, so we only try config.json
|
||||
*/
|
||||
loadTokenFromCliConfig() {
|
||||
try {
|
||||
@@ -117,30 +150,44 @@ class ClaudeProvider extends ModelProvider {
|
||||
}
|
||||
|
||||
ensureAuthEnv() {
|
||||
// If API key or token already present, keep as-is.
|
||||
// If API key or token already present in environment, keep as-is.
|
||||
if (process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||
console.log('[ClaudeProvider] Auth already present in environment');
|
||||
return true;
|
||||
}
|
||||
// Try to hydrate from CLI login config
|
||||
|
||||
// Priority 1: Try to load from app's own credentials (setup wizard)
|
||||
const appCredentials = this.loadTokenFromAppCredentials();
|
||||
if (appCredentials.oauthToken) {
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKEN = appCredentials.oauthToken;
|
||||
console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from app credentials');
|
||||
return true;
|
||||
}
|
||||
if (appCredentials.apiKey) {
|
||||
process.env.ANTHROPIC_API_KEY = appCredentials.apiKey;
|
||||
console.log('[ClaudeProvider] Loaded ANTHROPIC_API_KEY from app credentials');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Priority 2: Try to hydrate from CLI login config (legacy)
|
||||
const token = this.loadTokenFromCliConfig();
|
||||
if (token) {
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
||||
console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from ~/.claude/config.json');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check if CLI is installed but not logged in
|
||||
try {
|
||||
const claudeCliDetector = require('./claude-cli-detector');
|
||||
const detection = claudeCliDetector.detectClaudeInstallation();
|
||||
if (detection.installed && detection.method === 'cli') {
|
||||
console.error('[ClaudeProvider] Claude CLI is installed but not logged in. Run `claude login` to authenticate.');
|
||||
console.error('[ClaudeProvider] Claude CLI is installed but not authenticated. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.');
|
||||
} else {
|
||||
console.error('[ClaudeProvider] No Anthropic auth found (env empty, ~/.claude/config.json missing token)');
|
||||
console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ClaudeProvider] No Anthropic auth found (env empty, ~/.claude/config.json missing token)');
|
||||
console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -156,17 +203,17 @@ class ClaudeProvider extends ModelProvider {
|
||||
}
|
||||
|
||||
async *executeQuery(options) {
|
||||
// Ensure we have auth; fall back to CLI login token if available.
|
||||
// Ensure we have auth; fall back to app credentials or CLI login token if available.
|
||||
if (!this.ensureAuthEnv()) {
|
||||
// Check if CLI is installed to provide better error message
|
||||
let msg = 'Missing Anthropic auth. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.';
|
||||
let msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication.';
|
||||
try {
|
||||
const claudeCliDetector = require('./claude-cli-detector');
|
||||
const detection = claudeCliDetector.detectClaudeInstallation();
|
||||
if (detection.installed && detection.method === 'cli') {
|
||||
msg = 'Claude CLI is installed but not authenticated. Run `claude login` to authenticate, or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.';
|
||||
msg = 'Claude CLI is installed but not authenticated. Go to Settings > Setup to provide your subscription token (from `claude setup-token`) or API key.';
|
||||
} else {
|
||||
msg = 'Missing Anthropic auth. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN, or install Claude CLI and run `claude login`.';
|
||||
msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication, or set ANTHROPIC_API_KEY environment variable.';
|
||||
}
|
||||
} catch (err) {
|
||||
// Fallback to default message
|
||||
@@ -239,11 +286,11 @@ class ClaudeProvider extends ModelProvider {
|
||||
validateConfig() {
|
||||
const errors = [];
|
||||
|
||||
// Ensure auth is available (try to auto-load from CLI config)
|
||||
// Ensure auth is available (try to auto-load from app credentials or CLI config)
|
||||
this.ensureAuthEnv();
|
||||
|
||||
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN && !process.env.ANTHROPIC_API_KEY) {
|
||||
errors.push('No Claude authentication found. Set CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY, or run `claude login` to populate ~/.claude/config.json.');
|
||||
errors.push('No Claude authentication found. Go to Settings > Setup to configure your subscription token or API key.');
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user