feat(setup): implement setup wizard for CLI tools configuration

- Added a new SetupView component to guide users through the installation and authentication of Claude and Codex CLIs.
- Integrated IPC handlers for checking CLI status, installing, and authenticating both CLIs.
- Enhanced the app store to manage setup state, including first run detection and progress tracking.
- Updated the main application view to redirect to the setup wizard on first run.
- Improved user experience by providing clear instructions and feedback during the setup process.

These changes streamline the initial configuration of CLI tools, ensuring users can easily set up their development environment.
This commit is contained in:
Kacper
2025-12-10 19:15:29 +01:00
parent 2afb5ced90
commit 3bd28d3084
12 changed files with 3191 additions and 83 deletions

View File

@@ -818,8 +818,23 @@ ipcMain.handle(
ipcMain.handle("claude:check-cli", async () => {
try {
const claudeCliDetector = require("./services/claude-cli-detector");
const info = claudeCliDetector.getInstallationInfo();
return { success: true, ...info };
const path = require("path");
const credentialsPath = path.join(app.getPath("userData"), "credentials.json");
const fullStatus = claudeCliDetector.getFullStatus(credentialsPath);
// Return in format expected by settings view (status: "installed" | "not_installed")
return {
success: true,
status: fullStatus.installed ? "installed" : "not_installed",
method: fullStatus.auth?.method || null,
version: fullStatus.version || null,
path: fullStatus.path || null,
authenticated: fullStatus.auth?.authenticated || false,
recommendation: fullStatus.installed
? null
: "Install Claude Code CLI for optimal performance with ultrathink.",
installCommands: fullStatus.installed ? null : claudeCliDetector.getInstallCommands(),
};
} catch (error) {
console.error("[IPC] claude:check-cli error:", error);
return { success: false, error: error.message };
@@ -1363,3 +1378,233 @@ ipcMain.handle("git:get-file-diff", async (_, { projectPath, filePath }) => {
return { success: false, error: error.message };
}
});
// ============================================================================
// Setup & CLI Management IPC Handlers
// ============================================================================
/**
* Get comprehensive Claude CLI status including auth
*/
ipcMain.handle("setup:claude-status", async () => {
try {
const claudeCliDetector = require("./services/claude-cli-detector");
const credentialsPath = path.join(app.getPath("userData"), "credentials.json");
const result = claudeCliDetector.getFullStatus(credentialsPath);
console.log("[IPC] setup:claude-status result:", result);
return result;
} catch (error) {
console.error("[IPC] setup:claude-status error:", error);
return { success: false, error: error.message };
}
});
/**
* Get comprehensive Codex CLI status including auth
*/
ipcMain.handle("setup:codex-status", async () => {
try {
const codexCliDetector = require("./services/codex-cli-detector");
const info = codexCliDetector.getFullStatus();
return { success: true, ...info };
} catch (error) {
console.error("[IPC] setup:codex-status error:", error);
return { success: false, error: error.message };
}
});
/**
* Install Claude CLI
*/
ipcMain.handle("setup:install-claude", async (event) => {
try {
const claudeCliDetector = require("./services/claude-cli-detector");
const sendProgress = (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("setup:install-progress", {
cli: "claude",
...progress
});
}
};
const result = await claudeCliDetector.installCli(sendProgress);
return { success: true, ...result };
} catch (error) {
console.error("[IPC] setup:install-claude error:", error);
return { success: false, error: error.message || error.error };
}
});
/**
* Install Codex CLI
*/
ipcMain.handle("setup:install-codex", async (event) => {
try {
const codexCliDetector = require("./services/codex-cli-detector");
const sendProgress = (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("setup:install-progress", {
cli: "codex",
...progress
});
}
};
const result = await codexCliDetector.installCli(sendProgress);
return { success: true, ...result };
} catch (error) {
console.error("[IPC] setup:install-codex error:", error);
return { success: false, error: error.message || error.error };
}
});
/**
* Authenticate Claude CLI (manual auth required)
*/
ipcMain.handle("setup:auth-claude", async (event) => {
try {
const claudeCliDetector = require("./services/claude-cli-detector");
const sendProgress = (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("setup:auth-progress", {
cli: "claude",
...progress
});
}
};
const result = await claudeCliDetector.runSetupToken(sendProgress);
return { success: true, ...result };
} catch (error) {
console.error("[IPC] setup:auth-claude error:", error);
return { success: false, error: error.message || error.error };
}
});
/**
* Authenticate Codex CLI with optional API key
*/
ipcMain.handle("setup:auth-codex", async (event, { apiKey }) => {
try {
const codexCliDetector = require("./services/codex-cli-detector");
const sendProgress = (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("setup:auth-progress", {
cli: "codex",
...progress
});
}
};
const result = await codexCliDetector.authenticate(apiKey, sendProgress);
return { success: true, ...result };
} catch (error) {
console.error("[IPC] setup:auth-codex error:", error);
return { success: false, error: error.message || error.error };
}
});
/**
* Store API key or OAuth token securely (using app's userData)
* @param {string} provider - Provider name (anthropic, openai, google, anthropic_oauth_token)
* @param {string} apiKey - The API key or OAuth token to store
*/
ipcMain.handle("setup:store-api-key", async (_, { provider, apiKey }) => {
try {
console.log("[IPC] setup:store-api-key called for provider:", provider);
const configPath = path.join(app.getPath("userData"), "credentials.json");
let credentials = {};
// Read existing credentials
try {
const content = await fs.readFile(configPath, "utf-8");
credentials = JSON.parse(content);
} catch (e) {
// File doesn't exist, start fresh
}
// Store the new key/token
credentials[provider] = apiKey;
// Write back
await fs.writeFile(configPath, JSON.stringify(credentials, null, 2), "utf-8");
console.log("[IPC] setup:store-api-key stored successfully for:", provider);
return { success: true };
} catch (error) {
console.error("[IPC] setup:store-api-key error:", error);
return { success: false, error: error.message };
}
});
/**
* Get stored API keys and tokens
*/
ipcMain.handle("setup:get-api-keys", async () => {
try {
const configPath = path.join(app.getPath("userData"), "credentials.json");
try {
const content = await fs.readFile(configPath, "utf-8");
const credentials = JSON.parse(content);
// Return which keys/tokens exist (not the actual values for security)
return {
success: true,
hasAnthropicKey: !!credentials.anthropic,
hasAnthropicOAuthToken: !!credentials.anthropic_oauth_token,
hasOpenAIKey: !!credentials.openai,
hasGoogleKey: !!credentials.google
};
} catch (e) {
return {
success: true,
hasAnthropicKey: false,
hasAnthropicOAuthToken: false,
hasOpenAIKey: false,
hasGoogleKey: false
};
}
} catch (error) {
console.error("[IPC] setup:get-api-keys error:", error);
return { success: false, error: error.message };
}
});
/**
* Configure Codex MCP server for a project
*/
ipcMain.handle("setup:configure-codex-mcp", async (_, { projectPath }) => {
try {
const codexConfigManager = require("./services/codex-config-manager");
const mcpServerPath = path.join(__dirname, "services", "mcp-server-factory.js");
const configPath = await codexConfigManager.configureMcpServer(projectPath, mcpServerPath);
return { success: true, configPath };
} catch (error) {
console.error("[IPC] setup:configure-codex-mcp error:", error);
return { success: false, error: error.message };
}
});
/**
* Get platform information
*/
ipcMain.handle("setup:get-platform", async () => {
const os = require("os");
return {
success: true,
platform: process.platform,
arch: process.arch,
homeDir: os.homedir(),
isWindows: process.platform === "win32",
isMac: process.platform === "darwin",
isLinux: process.platform === "linux"
};
});

View File

@@ -252,6 +252,59 @@ contextBridge.exposeInMainWorld("electronAPI", {
};
},
},
// Setup & CLI Management API
setup: {
// Get comprehensive Claude CLI status
getClaudeStatus: () => ipcRenderer.invoke("setup:claude-status"),
// Get comprehensive Codex CLI status
getCodexStatus: () => ipcRenderer.invoke("setup:codex-status"),
// Install Claude CLI
installClaude: () => ipcRenderer.invoke("setup:install-claude"),
// Install Codex CLI
installCodex: () => ipcRenderer.invoke("setup:install-codex"),
// Authenticate Claude CLI
authClaude: () => ipcRenderer.invoke("setup:auth-claude"),
// Authenticate Codex CLI with optional API key
authCodex: (apiKey) => ipcRenderer.invoke("setup:auth-codex", { apiKey }),
// Store API key securely
storeApiKey: (provider, apiKey) =>
ipcRenderer.invoke("setup:store-api-key", { provider, apiKey }),
// Get stored API keys status
getApiKeys: () => ipcRenderer.invoke("setup:get-api-keys"),
// Configure Codex MCP server for a project
configureCodexMcp: (projectPath) =>
ipcRenderer.invoke("setup:configure-codex-mcp", { projectPath }),
// Get platform information
getPlatform: () => ipcRenderer.invoke("setup:get-platform"),
// Listen for installation progress
onInstallProgress: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("setup:install-progress", subscription);
return () => {
ipcRenderer.removeListener("setup:install-progress", subscription);
};
},
// Listen for auth progress
onAuthProgress: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("setup:auth-progress", subscription);
return () => {
ipcRenderer.removeListener("setup:auth-progress", subscription);
};
},
},
});
// Also expose a flag to detect if we're in Electron

View File

@@ -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;

View File

@@ -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;

View File

@@ -408,16 +408,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

View File

@@ -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 {