mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
- 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.
525 lines
16 KiB
JavaScript
525 lines
16 KiB
JavaScript
/**
|
|
* Model Provider Abstraction Layer
|
|
*
|
|
* This module provides an abstract interface for model providers (Claude, Codex, etc.)
|
|
* allowing the application to use different AI models through a unified API.
|
|
*/
|
|
|
|
/**
|
|
* Base class for model providers
|
|
* Concrete implementations should extend this class
|
|
*/
|
|
class ModelProvider {
|
|
constructor(config = {}) {
|
|
this.config = config;
|
|
this.name = 'base';
|
|
}
|
|
|
|
/**
|
|
* Get provider name
|
|
* @returns {string} Provider name
|
|
*/
|
|
getName() {
|
|
return this.name;
|
|
}
|
|
|
|
/**
|
|
* Execute a query with the model provider
|
|
* @param {Object} options Query options
|
|
* @param {string} options.prompt The prompt to send
|
|
* @param {string} options.model The model to use
|
|
* @param {string} options.systemPrompt System prompt
|
|
* @param {string} options.cwd Working directory
|
|
* @param {number} options.maxTurns Maximum turns
|
|
* @param {string[]} options.allowedTools Allowed tools
|
|
* @param {Object} options.mcpServers MCP servers configuration
|
|
* @param {AbortController} options.abortController Abort controller
|
|
* @param {Object} options.thinking Thinking configuration
|
|
* @returns {AsyncGenerator} Async generator yielding messages
|
|
*/
|
|
async *executeQuery(options) {
|
|
throw new Error('executeQuery must be implemented by subclass');
|
|
}
|
|
|
|
/**
|
|
* Detect if this provider's CLI/SDK is installed
|
|
* @returns {Promise<Object>} Installation status
|
|
*/
|
|
async detectInstallation() {
|
|
throw new Error('detectInstallation must be implemented by subclass');
|
|
}
|
|
|
|
/**
|
|
* Get list of available models for this provider
|
|
* @returns {Array<Object>} Array of model definitions
|
|
*/
|
|
getAvailableModels() {
|
|
throw new Error('getAvailableModels must be implemented by subclass');
|
|
}
|
|
|
|
/**
|
|
* Validate provider configuration
|
|
* @returns {Object} Validation result { valid: boolean, errors: string[] }
|
|
*/
|
|
validateConfig() {
|
|
throw new Error('validateConfig must be implemented by subclass');
|
|
}
|
|
|
|
/**
|
|
* Get the full model string for a model key
|
|
* @param {string} modelKey Short model key (e.g., 'opus', 'gpt-5.1-codex')
|
|
* @returns {string} Full model string
|
|
*/
|
|
getModelString(modelKey) {
|
|
throw new Error('getModelString must be implemented by subclass');
|
|
}
|
|
|
|
/**
|
|
* Check if provider supports a specific feature
|
|
* @param {string} feature Feature name (e.g., 'thinking', 'tools', 'streaming')
|
|
* @returns {boolean} Whether the feature is supported
|
|
*/
|
|
supportsFeature(feature) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Claude Provider - Uses Anthropic Claude Agent SDK
|
|
*/
|
|
class ClaudeProvider extends ModelProvider {
|
|
constructor(config = {}) {
|
|
super(config);
|
|
this.name = 'claude';
|
|
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 {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const configPath = path.join(require('os').homedir(), '.claude', 'config.json');
|
|
if (!fs.existsSync(configPath)) {
|
|
return null;
|
|
}
|
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
const parsed = JSON.parse(raw);
|
|
// CLI config stores token as oauth_token (newer) or token (older)
|
|
return parsed.oauth_token || parsed.token || null;
|
|
} catch (err) {
|
|
console.warn('[ClaudeProvider] Failed to read CLI config token:', err?.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
ensureAuthEnv() {
|
|
// 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;
|
|
}
|
|
|
|
// 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 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. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
|
|
}
|
|
} catch (err) {
|
|
console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Lazily load the Claude SDK
|
|
*/
|
|
loadSdk() {
|
|
if (!this.sdk) {
|
|
this.sdk = require('@anthropic-ai/claude-agent-sdk');
|
|
}
|
|
return this.sdk;
|
|
}
|
|
|
|
async *executeQuery(options) {
|
|
// 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. 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. Go to Settings > Setup to provide your subscription token (from `claude setup-token`) or API key.';
|
|
} else {
|
|
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
|
|
}
|
|
console.error(`[ClaudeProvider] ${msg}`);
|
|
yield { type: 'error', error: msg };
|
|
return;
|
|
}
|
|
|
|
const { query } = this.loadSdk();
|
|
|
|
const sdkOptions = {
|
|
model: options.model,
|
|
systemPrompt: options.systemPrompt,
|
|
maxTurns: options.maxTurns || 1000,
|
|
cwd: options.cwd,
|
|
mcpServers: options.mcpServers,
|
|
allowedTools: options.allowedTools,
|
|
permissionMode: options.permissionMode || 'acceptEdits',
|
|
sandbox: options.sandbox,
|
|
abortController: options.abortController,
|
|
};
|
|
|
|
// Add thinking configuration if enabled
|
|
if (options.thinking) {
|
|
sdkOptions.thinking = options.thinking;
|
|
}
|
|
|
|
const currentQuery = query({ prompt: options.prompt, options: sdkOptions });
|
|
|
|
for await (const msg of currentQuery) {
|
|
yield msg;
|
|
}
|
|
}
|
|
|
|
async detectInstallation() {
|
|
const claudeCliDetector = require('./claude-cli-detector');
|
|
return claudeCliDetector.getInstallationInfo();
|
|
}
|
|
|
|
getAvailableModels() {
|
|
return [
|
|
{
|
|
id: 'haiku',
|
|
name: 'Claude Haiku',
|
|
modelString: 'claude-haiku-4-5',
|
|
provider: 'claude',
|
|
description: 'Fast and efficient for simple tasks',
|
|
tier: 'basic'
|
|
},
|
|
{
|
|
id: 'sonnet',
|
|
name: 'Claude Sonnet',
|
|
modelString: 'claude-sonnet-4-20250514',
|
|
provider: 'claude',
|
|
description: 'Balanced performance and capabilities',
|
|
tier: 'standard'
|
|
},
|
|
{
|
|
id: 'opus',
|
|
name: 'Claude Opus 4.5',
|
|
modelString: 'claude-opus-4-5-20251101',
|
|
provider: 'claude',
|
|
description: 'Most capable model for complex tasks',
|
|
tier: 'premium'
|
|
}
|
|
];
|
|
}
|
|
|
|
validateConfig() {
|
|
const errors = [];
|
|
|
|
// 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. Go to Settings > Setup to configure your subscription token or API key.');
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
getModelString(modelKey) {
|
|
const modelMap = {
|
|
haiku: 'claude-haiku-4-5',
|
|
sonnet: 'claude-sonnet-4-20250514',
|
|
opus: 'claude-opus-4-5-20251101'
|
|
};
|
|
return modelMap[modelKey] || modelMap.opus;
|
|
}
|
|
|
|
supportsFeature(feature) {
|
|
const supportedFeatures = ['thinking', 'tools', 'streaming', 'mcp'];
|
|
return supportedFeatures.includes(feature);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Codex Provider - Uses OpenAI Codex CLI
|
|
*/
|
|
class CodexProvider extends ModelProvider {
|
|
constructor(config = {}) {
|
|
super(config);
|
|
this.name = 'codex';
|
|
}
|
|
|
|
async *executeQuery(options) {
|
|
const codexExecutor = require('./codex-executor');
|
|
|
|
// Validate that we're not receiving a Claude model string
|
|
if (options.model && options.model.startsWith('claude-')) {
|
|
const errorMsg = `Codex provider cannot use Claude model '${options.model}'. Codex only supports OpenAI models (gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1-codex-mini, gpt-5.1).`;
|
|
console.error(`[CodexProvider] ${errorMsg}`);
|
|
yield {
|
|
type: 'error',
|
|
error: errorMsg
|
|
};
|
|
return;
|
|
}
|
|
|
|
const executeOptions = {
|
|
prompt: options.prompt,
|
|
model: options.model,
|
|
cwd: options.cwd,
|
|
systemPrompt: options.systemPrompt,
|
|
maxTurns: options.maxTurns || 20,
|
|
allowedTools: options.allowedTools,
|
|
mcpServers: options.mcpServers, // Pass MCP servers config to executor
|
|
env: {
|
|
...process.env,
|
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY
|
|
}
|
|
};
|
|
|
|
// Execute and yield results
|
|
const generator = codexExecutor.execute(executeOptions);
|
|
for await (const msg of generator) {
|
|
yield msg;
|
|
}
|
|
}
|
|
|
|
async detectInstallation() {
|
|
const codexCliDetector = require('./codex-cli-detector');
|
|
return codexCliDetector.getInstallationInfo();
|
|
}
|
|
|
|
getAvailableModels() {
|
|
return [
|
|
{
|
|
id: 'gpt-5.1-codex-max',
|
|
name: 'GPT-5.1 Codex Max',
|
|
modelString: 'gpt-5.1-codex-max',
|
|
provider: 'codex',
|
|
description: 'Latest flagship - deep and fast reasoning for coding',
|
|
tier: 'premium',
|
|
default: true
|
|
},
|
|
{
|
|
id: 'gpt-5.1-codex',
|
|
name: 'GPT-5.1 Codex',
|
|
modelString: 'gpt-5.1-codex',
|
|
provider: 'codex',
|
|
description: 'Optimized for code generation',
|
|
tier: 'standard'
|
|
},
|
|
{
|
|
id: 'gpt-5.1-codex-mini',
|
|
name: 'GPT-5.1 Codex Mini',
|
|
modelString: 'gpt-5.1-codex-mini',
|
|
provider: 'codex',
|
|
description: 'Faster and cheaper option',
|
|
tier: 'basic'
|
|
},
|
|
{
|
|
id: 'gpt-5.1',
|
|
name: 'GPT-5.1',
|
|
modelString: 'gpt-5.1',
|
|
provider: 'codex',
|
|
description: 'Broad world knowledge with strong reasoning',
|
|
tier: 'standard'
|
|
}
|
|
];
|
|
}
|
|
|
|
validateConfig() {
|
|
const errors = [];
|
|
const codexCliDetector = require('./codex-cli-detector');
|
|
const installation = codexCliDetector.detectCodexInstallation();
|
|
|
|
if (!installation.installed && !process.env.OPENAI_API_KEY) {
|
|
errors.push('Codex CLI not installed and no OPENAI_API_KEY found.');
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
getModelString(modelKey) {
|
|
// Codex models use the key directly as the model string
|
|
const modelMap = {
|
|
'gpt-5.1-codex-max': 'gpt-5.1-codex-max',
|
|
'gpt-5.1-codex': 'gpt-5.1-codex',
|
|
'gpt-5.1-codex-mini': 'gpt-5.1-codex-mini',
|
|
'gpt-5.1': 'gpt-5.1'
|
|
};
|
|
return modelMap[modelKey] || 'gpt-5.1-codex-max';
|
|
}
|
|
|
|
supportsFeature(feature) {
|
|
const supportedFeatures = ['tools', 'streaming'];
|
|
return supportedFeatures.includes(feature);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Model Provider Factory
|
|
* Creates the appropriate provider based on model or provider name
|
|
*/
|
|
class ModelProviderFactory {
|
|
static providers = {
|
|
claude: ClaudeProvider,
|
|
codex: CodexProvider
|
|
};
|
|
|
|
/**
|
|
* Get provider for a specific model
|
|
* @param {string} modelId Model ID (e.g., 'opus', 'gpt-5.1-codex')
|
|
* @returns {ModelProvider} Provider instance
|
|
*/
|
|
static getProviderForModel(modelId) {
|
|
// Check if it's a Claude model
|
|
const claudeModels = ['haiku', 'sonnet', 'opus'];
|
|
if (claudeModels.includes(modelId)) {
|
|
return new ClaudeProvider();
|
|
}
|
|
|
|
// Check if it's a Codex/OpenAI model
|
|
const codexModels = [
|
|
'gpt-5.1-codex-max', 'gpt-5.1-codex', 'gpt-5.1-codex-mini', 'gpt-5.1'
|
|
];
|
|
if (codexModels.includes(modelId)) {
|
|
return new CodexProvider();
|
|
}
|
|
|
|
// Default to Claude
|
|
return new ClaudeProvider();
|
|
}
|
|
|
|
/**
|
|
* Get provider by name
|
|
* @param {string} providerName Provider name ('claude' or 'codex')
|
|
* @returns {ModelProvider} Provider instance
|
|
*/
|
|
static getProvider(providerName) {
|
|
const ProviderClass = this.providers[providerName];
|
|
if (!ProviderClass) {
|
|
throw new Error(`Unknown provider: ${providerName}`);
|
|
}
|
|
return new ProviderClass();
|
|
}
|
|
|
|
/**
|
|
* Get all available providers
|
|
* @returns {string[]} List of provider names
|
|
*/
|
|
static getAvailableProviders() {
|
|
return Object.keys(this.providers);
|
|
}
|
|
|
|
/**
|
|
* Get all available models across all providers
|
|
* @returns {Array<Object>} All available models
|
|
*/
|
|
static getAllModels() {
|
|
const allModels = [];
|
|
for (const providerName of this.getAvailableProviders()) {
|
|
const provider = this.getProvider(providerName);
|
|
const models = provider.getAvailableModels();
|
|
allModels.push(...models);
|
|
}
|
|
return allModels;
|
|
}
|
|
|
|
/**
|
|
* Check installation status for all providers
|
|
* @returns {Promise<Object>} Installation status for each provider
|
|
*/
|
|
static async checkAllProviders() {
|
|
const status = {};
|
|
for (const providerName of this.getAvailableProviders()) {
|
|
const provider = this.getProvider(providerName);
|
|
status[providerName] = await provider.detectInstallation();
|
|
}
|
|
return status;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
ModelProvider,
|
|
ClaudeProvider,
|
|
CodexProvider,
|
|
ModelProviderFactory
|
|
};
|