diff --git a/.gitignore b/.gitignore index 47e039a..d92a6d2 100644 --- a/.gitignore +++ b/.gitignore @@ -177,4 +177,5 @@ storybook-static/ coverage/ .DS_Store *.pem -tasks_backup.json \ No newline at end of file +tasks_backup.json +.credentials.json \ No newline at end of file diff --git a/async-code-web/app/settings/page.tsx b/async-code-web/app/settings/page.tsx index e0e51a9..ac4a2b2 100644 --- a/async-code-web/app/settings/page.tsx +++ b/async-code-web/app/settings/page.tsx @@ -1,12 +1,13 @@ "use client"; import { useState, useEffect } from "react"; -import { Github, CheckCircle, ArrowLeft, Settings, Key, Shield, Info } from "lucide-react"; +import { Github, CheckCircle, ArrowLeft, Settings, Key, Shield, Info, Code } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; +import { CodeAgentSettings } from "@/components/code-agent-settings"; import Link from "next/link"; @@ -106,7 +107,7 @@ export default function SettingsPage() {

Settings

-

Configure your GitHub authentication

+

Configure GitHub authentication and code agent environments

@@ -236,6 +237,9 @@ export default function SettingsPage() { + {/* Code Agent Settings */} + + {/* Token Creation Instructions */} diff --git a/async-code-web/components/code-agent-settings.tsx b/async-code-web/components/code-agent-settings.tsx new file mode 100644 index 0000000..90cb31b --- /dev/null +++ b/async-code-web/components/code-agent-settings.tsx @@ -0,0 +1,180 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import CodeMirror from "@uiw/react-codemirror"; +import { javascript } from "@codemirror/lang-javascript"; +import { githubLight } from "@uiw/codemirror-theme-github"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle, Save } from "lucide-react"; +import { toast } from "sonner"; +import { SupabaseService } from "@/lib/supabase-service"; +import { useUserProfile } from "@/hooks/useUserProfile"; + +interface CodeAgentConfig { + claudeCode?: Record; + codexCLI?: Record; +} + +const DEFAULT_CLAUDE_ENV = { + ANTHROPIC_API_KEY: "", + // Add other Claude-specific env vars here if needed +}; + +const DEFAULT_CODEX_ENV = { + OPENAI_API_KEY: "", + DISABLE_SANDBOX: "yes", + CONTINUE_ON_BROWSER: "no", + // Add other Codex-specific env vars here if needed +}; + +export function CodeAgentSettings() { + const { profile, refreshProfile } = useUserProfile(); + const [claudeEnv, setClaudeEnv] = useState(""); + const [codexEnv, setCodexEnv] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState<{ claude?: string; codex?: string }>({}); + + // Load settings from profile on mount + useEffect(() => { + if (profile?.preferences) { + const prefs = profile.preferences as CodeAgentConfig; + setClaudeEnv(JSON.stringify(prefs.claudeCode || DEFAULT_CLAUDE_ENV, null, 2)); + setCodexEnv(JSON.stringify(prefs.codexCLI || DEFAULT_CODEX_ENV, null, 2)); + } else { + setClaudeEnv(JSON.stringify(DEFAULT_CLAUDE_ENV, null, 2)); + setCodexEnv(JSON.stringify(DEFAULT_CODEX_ENV, null, 2)); + } + }, [profile]); + + const validateJSON = (value: string, agent: "claude" | "codex") => { + try { + JSON.parse(value); + setErrors(prev => ({ ...prev, [agent]: undefined })); + return true; + } catch (e) { + setErrors(prev => ({ ...prev, [agent]: "Invalid JSON format" })); + return false; + } + }; + + const handleSave = async () => { + // Validate both JSONs + const isClaudeValid = validateJSON(claudeEnv, "claude"); + const isCodexValid = validateJSON(codexEnv, "codex"); + + if (!isClaudeValid || !isCodexValid) { + toast.error("Please fix JSON errors before saving"); + return; + } + + setIsLoading(true); + try { + const claudeConfig = JSON.parse(claudeEnv); + const codexConfig = JSON.parse(codexEnv); + + const preferences: CodeAgentConfig = { + claudeCode: claudeConfig, + codexCLI: codexConfig, + }; + + // Merge with existing preferences if any + const existingPrefs = (profile?.preferences || {}) as Record; + const mergedPrefs = { + ...existingPrefs, + ...preferences, + }; + + await SupabaseService.updateUserProfile({ preferences: mergedPrefs }); + await refreshProfile(); + toast.success("Code agent settings saved successfully"); + } catch (error) { + console.error("Failed to save settings:", error); + toast.error("Failed to save settings"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + + Code Agent Settings + + Configure environment variables for each code agent. These settings will be used when creating containers. + + + + + + + Important: Store sensitive API keys here instead of hardcoding them. Personal settings will override default environment variables. + + + + {/* Claude Code Settings */} +
+ +
+ { + setClaudeEnv(value); + validateJSON(value, "claude"); + }} + placeholder={JSON.stringify(DEFAULT_CLAUDE_ENV, null, 2)} + /> +
+ {errors.claude && ( +

{errors.claude}

+ )} +

+ Configure environment variables for Claude Code CLI (@anthropic-ai/claude-code) +

+
+ + {/* Codex CLI Settings */} +
+ +
+ { + setCodexEnv(value); + validateJSON(value, "codex"); + }} + placeholder={JSON.stringify(DEFAULT_CODEX_ENV, null, 2)} + /> +
+ {errors.codex && ( +

{errors.codex}

+ )} +

+ Configure environment variables for Codex CLI (@openai/codex) +

+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/async-code-web/components/ui/alert.tsx b/async-code-web/components/ui/alert.tsx new file mode 100644 index 0000000..5a7ba0f --- /dev/null +++ b/async-code-web/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } \ No newline at end of file diff --git a/async-code-web/hooks/useUserProfile.tsx b/async-code-web/hooks/useUserProfile.tsx new file mode 100644 index 0000000..8092fcf --- /dev/null +++ b/async-code-web/hooks/useUserProfile.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { SupabaseService } from "@/lib/supabase-service"; +import { User } from "@/types"; + +export function useUserProfile() { + const [profile, setProfile] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchProfile = useCallback(async () => { + try { + setIsLoading(true); + const data = await SupabaseService.getUserProfile(); + setProfile(data); + setError(null); + } catch (err) { + setError(err as Error); + console.error("Failed to fetch user profile:", err); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + fetchProfile(); + }, [fetchProfile]); + + const refreshProfile = useCallback(async () => { + await fetchProfile(); + }, [fetchProfile]); + + return { + profile, + isLoading, + error, + refreshProfile, + }; +} \ No newline at end of file diff --git a/server/database.py b/server/database.py index e44b8a3..cad3937 100644 --- a/server/database.py +++ b/server/database.py @@ -216,4 +216,14 @@ def migrate_legacy_task(legacy_task: Dict, user_id: str) -> Optional[Dict]: return result.data[0] if result.data else None except Exception as e: logger.error(f"Error migrating legacy task: {e}") - raise \ No newline at end of file + raise + + @staticmethod + def get_user_by_id(user_id: str) -> Optional[Dict]: + """Get user by ID""" + try: + result = supabase.table('users').select('*').eq('id', user_id).single().execute() + return result.data + except Exception as e: + logger.error(f"Error getting user: {e}") + return None \ No newline at end of file diff --git a/server/utils/code_task_v2.py b/server/utils/code_task_v2.py index 3461f8b..449b8b3 100644 --- a/server/utils/code_task_v2.py +++ b/server/utils/code_task_v2.py @@ -142,20 +142,38 @@ def _run_ai_code_task_v2_internal(task_id: int, user_id: str, github_token: str) # Add model-specific API keys and environment variables model_cli = task.get('agent', 'claude') + + # Get user preferences for custom environment variables + user = DatabaseOperations.get_user_by_id(user_id) + user_preferences = user.get('preferences', {}) if user else {} + + if user_preferences: + logger.info(f"🔧 Found user preferences for {model_cli}: {list(user_preferences.keys())}") + if model_cli == 'claude': - env_vars.update({ + # Start with default Claude environment + claude_env = { 'ANTHROPIC_API_KEY': os.getenv('ANTHROPIC_API_KEY'), 'ANTHROPIC_NONINTERACTIVE': '1' # Custom flag for Anthropic tools - }) + } + # Merge with user's custom Claude environment variables + if user_preferences.get('claudeCode'): + claude_env.update(user_preferences['claudeCode']) + env_vars.update(claude_env) elif model_cli == 'codex': - env_vars.update({ + # Start with default Codex environment + codex_env = { 'OPENAI_API_KEY': os.getenv('OPENAI_API_KEY'), 'OPENAI_NONINTERACTIVE': '1', # Custom flag for OpenAI tools 'CODEX_QUIET_MODE': '1', # Official Codex non-interactive flag 'CODEX_UNSAFE_ALLOW_NO_SANDBOX': '1', # Disable Codex internal sandboxing to prevent Docker conflicts 'CODEX_DISABLE_SANDBOX': '1', # Alternative sandbox disable flag 'CODEX_NO_SANDBOX': '1' # Another potential sandbox disable flag - }) + } + # Merge with user's custom Codex environment variables + if user_preferences.get('codexCLI'): + codex_env.update(user_preferences['codexCLI']) + env_vars.update(codex_env) # Use specialized container images based on model if model_cli == 'codex': @@ -185,6 +203,33 @@ def _run_ai_code_task_v2_internal(task_id: int, user_id: str, github_token: str) logger.info(f"🕐 Adding additional {additional_delay:.1f}s delay due to lock conflict") time.sleep(additional_delay) + # Load Claude credentials from user preferences in Supabase + credentials_content = "" + escaped_credentials = "" + if model_cli == 'claude': + logger.info(f"🔍 Looking for Claude credentials in user preferences for task {task_id}") + + # Check if user has Claude credentials in their preferences + claude_preferences = user_preferences.get('claudeCode', {}) + if claude_preferences and 'credentials' in claude_preferences: + try: + credentials_json = claude_preferences['credentials'] + if credentials_json: + # Convert JSON object to string for writing to container + credentials_content = json.dumps(credentials_json) + logger.info(f"📋 Successfully loaded Claude credentials from user preferences and stringified ({len(credentials_content)} characters) for task {task_id}") + # Escape credentials content for shell + escaped_credentials = credentials_content.replace("'", "'\"'\"'").replace('\n', '\\n') + logger.info(f"📋 Credentials content escaped for shell injection") + else: + logger.warning(f"âš ī¸ Claude credentials field exists but is empty in user preferences for task {task_id}") + except Exception as e: + logger.error(f"❌ Failed to process Claude credentials from user preferences: {e}") + credentials_content = "" + escaped_credentials = "" + else: + logger.info(f"â„šī¸ No Claude credentials found in user preferences for task {task_id} - skipping credentials setup") + # Create the command to run in container (v2 function) container_command = f''' set -e @@ -203,8 +248,29 @@ def _run_ai_code_task_v2_internal(task_id: int, user_id: str, github_token: str) echo "Starting {model_cli.upper()} Code with prompt..." -# Create a temporary file with the prompt -echo "{escaped_prompt}" > /tmp/prompt.txt +# Create a temporary file with the prompt using heredoc for proper handling +cat << 'PROMPT_EOF' > /tmp/prompt.txt +{prompt} +PROMPT_EOF + +# Setup Claude credentials for Claude tasks +if [ "{model_cli}" = "claude" ]; then + echo "Setting up Claude credentials..." + + # Create ~/.claude directory if it doesn't exist + mkdir -p ~/.claude + + # Write credentials content directly to file + if [ ! -z '{escaped_credentials}' ]; then + echo "📋 Writing credentials to ~/.claude/.credentials.json" + cat << 'CREDENTIALS_EOF' > ~/.claude/.credentials.json +{credentials_content} +CREDENTIALS_EOF + echo "✅ Claude credentials configured" + else + echo "âš ī¸ No credentials content available" + fi +fi # Check which CLI tool to use based on model selection if [ "{model_cli}" = "codex" ]; then