Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions apps/backend/runners/insights_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import argparse
import asyncio
import json
import os
import sys
from pathlib import Path

Expand Down Expand Up @@ -132,7 +133,7 @@ async def run_with_sdk(
project_dir: str,
message: str,
history: list,
model: str = "claude-sonnet-4-5-20250929",
model: str = None,
thinking_level: str = "medium",
) -> None:
"""Run the chat using Claude SDK with streaming."""
Expand All @@ -152,6 +153,10 @@ async def run_with_sdk(
# Ensure SDK can find the token
ensure_claude_code_oauth_token()

# Use environment variable for model if not provided
if model is None:
model = os.getenv("AUTO_BUILD_MODEL", "claude-sonnet-4-5-20250929")

system_prompt = build_system_prompt(project_dir)
project_path = Path(project_dir).resolve()

Expand Down Expand Up @@ -336,8 +341,8 @@ def main():
)
parser.add_argument(
"--model",
default="claude-sonnet-4-5-20250929",
help="Claude model ID (default: claude-sonnet-4-5-20250929)",
default=None,
help="Claude model ID (default: from AUTO_BUILD_MODEL env var or claude-sonnet-4-5-20250929)",
)
parser.add_argument(
"--thinking-level",
Expand Down
107 changes: 106 additions & 1 deletion apps/frontend/src/main/ipc-handlers/settings-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ipcMain, dialog, app, shell } from 'electron';
import { existsSync, writeFileSync, mkdirSync, statSync } from 'fs';
import { existsSync, writeFileSync, mkdirSync, statSync, readFileSync } from 'fs';
import { execFileSync } from 'node:child_process';
import path from 'path';
import { is } from '@electron-toolkit/utils';
Expand Down Expand Up @@ -442,4 +442,109 @@ export function registerSettingsHandlers(
}
}
);

// ============================================
// Custom API Settings
// ============================================

ipcMain.handle(
IPC_CHANNELS.SETTINGS_SAVE_CUSTOM_API,
async (
_,
settings: { baseUrl: string; authToken: string; model?: string }
): Promise<IPCResult> => {
try {
// Load current settings to get autoBuildPath
const savedSettings = readSettingsFile();
const currentSettings = { ...DEFAULT_APP_SETTINGS, ...savedSettings };
let autoBuildPath = currentSettings.autoBuildPath || detectAutoBuildSourcePath();

// Fallback: try development paths if detected path doesn't exist
if (!autoBuildPath || !existsSync(autoBuildPath)) {
const devPaths = [
path.resolve(process.cwd(), 'apps', 'backend'),
path.resolve(__dirname, '..', '..', '..', 'backend'),
path.resolve(__dirname, '..', '..', '..', '..', 'apps', 'backend')
];

for (const p of devPaths) {
if (existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py'))) {
autoBuildPath = p;
console.log('[SAVE_CUSTOM_API_SETTINGS] Using fallback path:', p);
break;
}
}
}

if (!autoBuildPath || !existsSync(autoBuildPath)) {
return {
success: false,
error: 'Auto Claude backend path not found. Searched paths:\n' +
`- ${currentSettings.autoBuildPath || 'not configured'}\n` +
`- ${process.cwd()}/apps/backend\n` +
'Please configure backend path in settings.'
};
}

const envPath = path.join(autoBuildPath, '.env');
console.log('[SAVE_CUSTOM_API_SETTINGS] Writing to:', envPath);

// Read existing .env file or create new content
let envContent = '';
if (existsSync(envPath)) {
envContent = readFileSync(envPath, 'utf-8');
} else {
console.log('[SAVE_CUSTOM_API_SETTINGS] .env file does not exist, creating new one');
}

// Helper function to escape special regex characters
const escapeRegex = (str: string): string => {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};

// Update or add settings with proper escaping
const updateEnvVar = (content: string, key: string, value: string): string => {
const escapedKey = escapeRegex(key);
const regex = new RegExp(`^${escapedKey}=.*$`, 'm');
if (regex.test(content)) {
// Use function replacement to avoid $ interpretation in value
return content.replace(regex, () => `${key}=${value}`);
} else {
return content + `\n${key}=${value}`;
}
};

envContent = updateEnvVar(envContent, 'ANTHROPIC_BASE_URL', settings.baseUrl);
envContent = updateEnvVar(envContent, 'ANTHROPIC_AUTH_TOKEN', settings.authToken);

try {
const hostname = new URL(settings.baseUrl).hostname;
envContent = updateEnvVar(envContent, 'NO_PROXY', hostname);
} catch {
// If URL parsing fails, skip NO_PROXY
console.warn('[SAVE_CUSTOM_API_SETTINGS] Failed to parse URL for NO_PROXY');
}

envContent = updateEnvVar(envContent, 'DISABLE_TELEMETRY', 'true');
envContent = updateEnvVar(envContent, 'DISABLE_COST_WARNINGS', 'true');
envContent = updateEnvVar(envContent, 'API_TIMEOUT_MS', '600000');

if (settings.model) {
envContent = updateEnvVar(envContent, 'AUTO_BUILD_MODEL', settings.model);
}

// Write back to .env file
writeFileSync(envPath, envContent.trim() + '\n');
console.log('[SAVE_CUSTOM_API_SETTINGS] Successfully saved settings to:', envPath);

return { success: true };
} catch (error) {
console.error('[SAVE_CUSTOM_API_SETTINGS] Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to save custom API settings'
};
}
}
);
}
15 changes: 15 additions & 0 deletions apps/frontend/src/preload/api/settings-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export interface SettingsAPI {
// App Info
getAppVersion: () => Promise<string>;

// Custom API Settings
saveCustomApiSettings: (settings: {
baseUrl: string;
authToken: string;
model?: string;
}) => Promise<IPCResult>;

// Auto-Build Source Environment
getSourceEnv: () => Promise<IPCResult<SourceEnvConfig>>;
updateSourceEnv: (config: { claudeOAuthToken?: string }) => Promise<IPCResult>;
Expand Down Expand Up @@ -51,6 +58,14 @@ export const createSettingsAPI = (): SettingsAPI => ({
getAppVersion: (): Promise<string> =>
ipcRenderer.invoke(IPC_CHANNELS.APP_VERSION),

// Custom API Settings
saveCustomApiSettings: (settings: {
baseUrl: string;
authToken: string;
model?: string;
}): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_SAVE_CUSTOM_API, settings),

// Auto-Build Source Environment
getSourceEnv: (): Promise<IPCResult<SourceEnvConfig>> =>
ipcRenderer.invoke(IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_GET),
Expand Down
140 changes: 140 additions & 0 deletions apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
const [showManualToken, setShowManualToken] = useState(false);
const [savingTokenProfileId, setSavingTokenProfileId] = useState<string | null>(null);

// Custom API settings state
const [showCustomApi, setShowCustomApi] = useState(false);
const [customBaseUrl, setCustomBaseUrl] = useState('');
const [customAuthToken, setCustomAuthToken] = useState('');
const [customModel, setCustomModel] = useState('');
const [showCustomAuthToken, setShowCustomAuthToken] = useState(false);
const [savingCustomApi, setSavingCustomApi] = useState(false);

// Error state
const [error, setError] = useState<string | null>(null);

Expand Down Expand Up @@ -281,6 +289,36 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
}
};

const handleSaveCustomApi = async () => {
if (!customBaseUrl.trim() || !customAuthToken.trim()) {
setError('Base URL and Auth Token are required');
return;
}

setSavingCustomApi(true);
setError(null);
try {
// Save to .env file via backend
const result = await window.electronAPI.saveCustomApiSettings({
baseUrl: customBaseUrl.trim(),
authToken: customAuthToken.trim(),
model: customModel.trim() || undefined
});

if (result.success) {
alert('✅ Custom API settings saved successfully!\n\nPlease restart the app for changes to take effect.');
setShowCustomApi(false);
} else {
alert(`Failed to save settings: ${result.error || 'Please try again.'}`);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save custom API settings');
alert('Failed to save settings. Please try again.');
} finally {
setSavingCustomApi(false);
}
};

const handleContinue = () => {
onNext();
};
Expand Down Expand Up @@ -594,6 +632,108 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
</div>
)}

{/* Custom API Settings Section */}
<div className="border-t border-border pt-4 mt-4">
<Button
variant="ghost"
onClick={() => setShowCustomApi(!showCustomApi)}
className="w-full justify-between h-auto py-2 px-3 hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<Key className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">{t('onboarding:oauth.customApi.title')}</span>
</div>
{showCustomApi ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
</Button>

{showCustomApi && (
<div className="bg-muted/30 rounded-lg p-4 mt-2 space-y-3">
<p className="text-xs text-muted-foreground mb-3">
{t('onboarding:oauth.customApi.description')}
</p>

<div className="space-y-2">
<Label className="text-xs font-medium">{t('onboarding:oauth.customApi.baseUrl')}</Label>
<Input
placeholder={t('onboarding:oauth.customApi.baseUrlPlaceholder')}
value={customBaseUrl}
onChange={(e) => setCustomBaseUrl(e.target.value)}
className="font-mono text-xs h-9"
/>
</div>

<div className="space-y-2">
<Label className="text-xs font-medium">{t('onboarding:oauth.customApi.authToken')}</Label>
<div className="relative">
<Input
type={showCustomAuthToken ? 'text' : 'password'}
placeholder={t('onboarding:oauth.customApi.authTokenPlaceholder')}
value={customAuthToken}
onChange={(e) => setCustomAuthToken(e.target.value)}
className="pr-10 font-mono text-xs h-9"
/>
<button
type="button"
onClick={() => setShowCustomAuthToken(!showCustomAuthToken)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showCustomAuthToken ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
</button>
</div>
</div>

<div className="space-y-2">
<Label className="text-xs font-medium">
{t('onboarding:oauth.customApi.model')}
</Label>
<Input
placeholder={t('onboarding:oauth.customApi.modelPlaceholder')}
value={customModel}
onChange={(e) => setCustomModel(e.target.value)}
className="font-mono text-xs h-9"
/>
<p className="text-xs text-muted-foreground">
{t('onboarding:oauth.customApi.modelDescription')}
</p>
</div>

<div className="flex items-center justify-end gap-2 pt-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setShowCustomApi(false);
setCustomBaseUrl('');
setCustomAuthToken('');
setCustomModel('');
setShowCustomAuthToken(false);
}}
className="h-8 text-xs"
>
{t('onboarding:oauth.customApi.cancel')}
</Button>
<Button
size="sm"
onClick={handleSaveCustomApi}
disabled={!customBaseUrl.trim() || !customAuthToken.trim() || savingCustomApi}
className="h-8 text-xs gap-1"
>
{savingCustomApi ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Check className="h-3 w-3" />
)}
{t('onboarding:oauth.customApi.save')}
</Button>
</div>
</div>
)}
</div>

{/* Add new account input */}
<div className="flex items-center gap-2">
<Input
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/shared/constants/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const IPC_CHANNELS = {
SETTINGS_GET: 'settings:get',
SETTINGS_SAVE: 'settings:save',
SETTINGS_GET_CLI_TOOLS_INFO: 'settings:getCliToolsInfo',
SETTINGS_SAVE_CUSTOM_API: 'settings:saveCustomApi',

// Dialogs
DIALOG_SELECT_DIRECTORY: 'dialog:selectDirectory',
Expand Down
15 changes: 14 additions & 1 deletion apps/frontend/src/shared/i18n/locales/en/onboarding.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,20 @@
"title": "Claude Authentication",
"description": "Connect your Claude account to enable AI features",
"keychainTitle": "Secure Storage",
"keychainDescription": "Your tokens are encrypted using your system's keychain. You may see a password prompt from macOS — click \"Always Allow\" to avoid seeing it again."
"keychainDescription": "Your tokens are encrypted using your system's keychain. You may see a password prompt from macOS — click \"Always Allow\" to avoid seeing it again.",
"customApi": {
"title": "Custom API Settings (Proxy/OpenRouter)",
"description": "Configure custom API endpoint for proxy services (litellm, OpenRouter) or self-hosted instances.",
"baseUrl": "Base URL",
"baseUrlPlaceholder": "http://localhost:8000",
"authToken": "Auth Token / API Key",
"authTokenPlaceholder": "your-api-key-here",
"model": "Custom Model Name (optional)",
"modelPlaceholder": "gemini-claude-sonnet-4-5",
"modelDescription": "Override the default model ID for all agent operations",
"cancel": "Cancel",
"save": "Save Settings"
}
},
"memory": {
"title": "Memory",
Expand Down
15 changes: 14 additions & 1 deletion apps/frontend/src/shared/i18n/locales/fr/onboarding.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,20 @@
"title": "Authentification Claude",
"description": "Connectez votre compte Claude pour activer les fonctionnalités IA",
"keychainTitle": "Stockage sécurisé",
"keychainDescription": "Vos jetons sont chiffrés à l'aide du trousseau de clés de votre système. Une demande de mot de passe macOS peut apparaître — cliquez sur « Toujours autoriser » pour ne plus la revoir."
"keychainDescription": "Vos jetons sont chiffrés à l'aide du trousseau de clés de votre système. Une demande de mot de passe macOS peut apparaître — cliquez sur « Toujours autoriser » pour ne plus la revoir.",
"customApi": {
"title": "Paramètres API personnalisés (Proxy/OpenRouter)",
"description": "Configurez un point de terminaison API personnalisé pour les services proxy (litellm, OpenRouter) ou les instances auto-hébergées.",
"baseUrl": "URL de base",
"baseUrlPlaceholder": "http://localhost:8000",
"authToken": "Jeton d'authentification / Clé API",
"authTokenPlaceholder": "votre-clé-api-ici",
"model": "Nom de modèle personnalisé (optionnel)",
"modelPlaceholder": "gemini-claude-sonnet-4-5",
"modelDescription": "Remplacer l'ID de modèle par défaut pour toutes les opérations d'agent",
"cancel": "Annuler",
"save": "Enregistrer les paramètres"
}
},
"memory": {
"title": "Mémoire",
Expand Down
Loading