From 54b977ee1b10550dc0ab0689d64926e439761e47 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Mon, 15 Dec 2025 14:24:18 -0500 Subject: [PATCH] redesign our approach for api keys to not use claude setup-token --- apps/app/README.md | 36 +- apps/app/package.json | 1 + apps/app/src/app/globals.css | 27 + apps/app/src/components/ui/accordion.tsx | 57 + .../api-keys/api-keys-section.tsx | 75 +- .../authentication-status-display.tsx | 48 +- .../setup-view/components/status-badge.tsx | 16 +- .../views/setup-view/dialogs/index.ts | 2 +- .../setup-view/dialogs/setup-token-modal.tsx | 262 ----- .../views/setup-view/hooks/index.ts | 1 - .../hooks/use-oauth-authentication.ts | 174 --- .../setup-view/steps/claude-setup-step.tsx | 992 ++++++++++-------- .../views/setup-view/steps/complete-step.tsx | 48 +- .../views/setup-view/steps/welcome-step.tsx | 22 +- apps/app/src/config/api-providers.ts | 57 +- apps/app/src/config/model-config.ts | 1 + apps/app/src/lib/electron.ts | 31 + apps/app/src/lib/http-api-client.ts | 14 + apps/app/src/store/setup-store.ts | 9 +- apps/server/src/lib/logger.ts | 1 + apps/server/src/routes/app-spec/index.ts | 1 + .../src/routes/setup/get-claude-status.ts | 2 +- apps/server/src/routes/setup/index.ts | 4 + .../src/routes/setup/routes/delete-api-key.ts | 104 ++ .../routes/setup/routes/verify-claude-auth.ts | 330 ++++++ docs/server/route-organization.md | 1 + package-lock.json | 478 +++++---- 27 files changed, 1564 insertions(+), 1230 deletions(-) create mode 100644 apps/app/src/components/ui/accordion.tsx delete mode 100644 apps/app/src/components/views/setup-view/dialogs/setup-token-modal.tsx delete mode 100644 apps/app/src/components/views/setup-view/hooks/use-oauth-authentication.ts create mode 100644 apps/server/src/routes/setup/routes/delete-api-key.ts create mode 100644 apps/server/src/routes/setup/routes/verify-claude-auth.ts diff --git a/apps/app/README.md b/apps/app/README.md index 2cb022b7..ee060308 100644 --- a/apps/app/README.md +++ b/apps/app/README.md @@ -33,27 +33,24 @@ cd automaker npm install ``` -### Windows notes (in-app Claude auth) - -- Node.js 22.x -- Prebuilt PTY is bundled; Visual Studio build tools are not required for Claude auth. -- If you prefer the external terminal flow, set `CLAUDE_AUTH_DISABLE_PTY=1`. -- If you later add native modules beyond the prebuilt PTY, you may still need VS Build Tools + Python to rebuild those. - -**Step 3:** Run the Claude Code setup token command: +**Step 3:** Get your Claude subscription token: ```bash claude setup-token ``` +This command will authenticate you via your browser and print a token to your terminal. + > **⚠️ Warning:** This command will print your token to your terminal. Be careful if you're streaming or sharing your screen, as the token will be visible to anyone watching. -**Step 4:** Export the Claude Code OAuth token in your shell: +**Step 4:** Export the Claude Code OAuth token in your shell (optional - you can also enter it in the app's setup wizard): ```bash export CLAUDE_CODE_OAUTH_TOKEN="your-token-here" ``` +Alternatively, you can enter your token directly in the Automaker setup wizard when you launch the app. + **Step 5:** Start the development server: ```bash @@ -62,27 +59,6 @@ npm run dev:electron This will start both the Next.js development server and the Electron application. -### Auth smoke test (Windows) - -1. Ensure dependencies are installed (prebuilt pty is included). -2. Run `npm run dev:electron` and open the Setup modal. -3. Click Start on Claude auth; watch the embedded terminal stream logs. -4. Successful runs show β€œToken captured automatically.”; otherwise copy/paste the token from the log. -5. Optional: `node --test tests/claude-cli-detector.test.js` to verify token parsing. - -**Step 6:** MOST IMPORTANT: Run the Following after all is setup - -```bash -echo "W" -echo "W" -echo "W" -echo "W" -echo "W" -echo "W" -echo "W" -echo "W" -``` - ## Features - πŸ“‹ **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages diff --git a/apps/app/package.json b/apps/app/package.json index ad9100db..86a92fa5 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -40,6 +40,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@lezer/highlight": "^1.2.3", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", diff --git a/apps/app/src/app/globals.css b/apps/app/src/app/globals.css index 0f2e2d70..11d2f859 100644 --- a/apps/app/src/app/globals.css +++ b/apps/app/src/app/globals.css @@ -2521,3 +2521,30 @@ .xml-editor .xml-highlight { z-index: 0; } + +/* Accordion animations */ +@keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } +} + +@keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } +} + +.animate-accordion-down { + animation: accordion-down 0.2s ease-out; +} + +.animate-accordion-up { + animation: accordion-up 0.2s ease-out; +} diff --git a/apps/app/src/components/ui/accordion.tsx b/apps/app/src/components/ui/accordion.tsx new file mode 100644 index 00000000..5591544b --- /dev/null +++ b/apps/app/src/components/ui/accordion.tsx @@ -0,0 +1,57 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; + diff --git a/apps/app/src/components/views/settings-view/api-keys/api-keys-section.tsx b/apps/app/src/components/views/settings-view/api-keys/api-keys-section.tsx index be774940..527cbf22 100644 --- a/apps/app/src/components/views/settings-view/api-keys/api-keys-section.tsx +++ b/apps/app/src/components/views/settings-view/api-keys/api-keys-section.tsx @@ -1,23 +1,61 @@ import { useAppStore } from "@/store/app-store"; import { useSetupStore } from "@/store/setup-store"; import { Button } from "@/components/ui/button"; -import { Key, CheckCircle2 } from "lucide-react"; +import { Key, CheckCircle2, Settings, Trash2, Loader2 } from "lucide-react"; import { ApiKeyField } from "./api-key-field"; import { buildProviderConfigs } from "@/config/api-providers"; import { AuthenticationStatusDisplay } from "./authentication-status-display"; import { SecurityNotice } from "./security-notice"; import { useApiKeyManagement } from "./hooks/use-api-key-management"; import { cn } from "@/lib/utils"; +import { useState, useCallback } from "react"; +import { getElectronAPI } from "@/lib/electron"; +import { toast } from "sonner"; export function ApiKeysSection() { - const { apiKeys } = useAppStore(); - const { claudeAuthStatus } = useSetupStore(); + const { apiKeys, setApiKeys } = useAppStore(); + const { claudeAuthStatus, setClaudeAuthStatus, setSetupComplete } = useSetupStore(); + const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false); const { providerConfigParams, apiKeyStatus, handleSave, saved } = useApiKeyManagement(); const providerConfigs = buildProviderConfigs(providerConfigParams); + // Delete Anthropic API key + const deleteAnthropicKey = useCallback(async () => { + setIsDeletingAnthropicKey(true); + try { + const api = getElectronAPI(); + if (!api.setup?.deleteApiKey) { + toast.error("Delete API not available"); + return; + } + + const result = await api.setup.deleteApiKey("anthropic"); + if (result.success) { + setApiKeys({ ...apiKeys, anthropic: "" }); + setClaudeAuthStatus({ + authenticated: false, + method: "none", + hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false, + }); + toast.success("Anthropic API key deleted"); + } else { + toast.error(result.error || "Failed to delete API key"); + } + } catch (error) { + toast.error("Failed to delete API key"); + } finally { + setIsDeletingAnthropicKey(false); + } + }, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]); + + // Open setup wizard + const openSetupWizard = useCallback(() => { + setSetupComplete(false); + }, [setSetupComplete]); + return (
- {/* Save Button */} -
+ {/* Action Buttons */} +
+ + {apiKeys.anthropic && ( + + )}
diff --git a/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx b/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx index 988e62cb..ad0de6b8 100644 --- a/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx +++ b/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx @@ -48,7 +48,9 @@ export function AuthenticationStatusDisplay({ <>
- Authenticated + + Authenticated +
@@ -65,7 +67,9 @@ export function AuthenticationStatusDisplay({ ? "Using credentials file" : claudeAuthStatus.method === "cli_authenticated" ? "Using Claude CLI authentication" - : `Using ${claudeAuthStatus.method || "detected"} authentication`} + : `Using ${ + claudeAuthStatus.method || "detected" + } authentication`}
@@ -87,46 +91,6 @@ export function AuthenticationStatusDisplay({ )} - - {/* Google/Gemini Authentication Status */} -
-
- - - Gemini (Google) - -
-
- {apiKeyStatus?.hasGoogleKey ? ( - <> -
- - Authenticated -
-
- - Using GOOGLE_API_KEY -
- - ) : apiKeys.google ? ( - <> -
- - Authenticated -
-
- - Using stored API key -
- - ) : ( -
- - Not configured -
- )} -
-
); diff --git a/apps/app/src/components/views/setup-view/components/status-badge.tsx b/apps/app/src/components/views/setup-view/components/status-badge.tsx index cf9cfc24..44ba8794 100644 --- a/apps/app/src/components/views/setup-view/components/status-badge.tsx +++ b/apps/app/src/components/views/setup-view/components/status-badge.tsx @@ -1,4 +1,4 @@ -import { CheckCircle2, XCircle, Loader2 } from "lucide-react"; +import { CheckCircle2, XCircle, Loader2, AlertCircle } from "lucide-react"; interface StatusBadgeProps { status: @@ -6,7 +6,9 @@ interface StatusBadgeProps { | "not_installed" | "checking" | "authenticated" - | "not_authenticated"; + | "not_authenticated" + | "error" + | "unverified"; label: string; } @@ -25,11 +27,21 @@ export function StatusBadge({ status, label }: StatusBadgeProps) { icon: , className: "bg-red-500/10 text-red-500 border-red-500/20", }; + case "error": + return { + icon: , + className: "bg-red-500/10 text-red-500 border-red-500/20", + }; case "checking": return { icon: , className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", }; + case "unverified": + return { + icon: , + className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", + }; } }; diff --git a/apps/app/src/components/views/setup-view/dialogs/index.ts b/apps/app/src/components/views/setup-view/dialogs/index.ts index 0466469f..c762ecfe 100644 --- a/apps/app/src/components/views/setup-view/dialogs/index.ts +++ b/apps/app/src/components/views/setup-view/dialogs/index.ts @@ -1,2 +1,2 @@ // Re-export all setup dialog components for easier imports -export { SetupTokenModal } from "./setup-token-modal"; +// (SetupTokenModal was removed - setup flow now uses inline API key entry) diff --git a/apps/app/src/components/views/setup-view/dialogs/setup-token-modal.tsx b/apps/app/src/components/views/setup-view/dialogs/setup-token-modal.tsx deleted file mode 100644 index bf8a5256..00000000 --- a/apps/app/src/components/views/setup-view/dialogs/setup-token-modal.tsx +++ /dev/null @@ -1,262 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef, useCallback } from "react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Loader2, - Terminal, - CheckCircle2, - XCircle, - Copy, - RotateCcw, -} from "lucide-react"; -import { toast } from "sonner"; -import { useOAuthAuthentication } from "../hooks"; - -interface SetupTokenModalProps { - open: boolean; - onClose: () => void; - onTokenObtained: (token: string) => void; -} - -export function SetupTokenModal({ - open, - onClose, - onTokenObtained, -}: SetupTokenModalProps) { - // Use the OAuth authentication hook - const { authState, output, token, error, startAuth, reset } = - useOAuthAuthentication({ cliType: "claude" }); - - const [manualToken, setManualToken] = useState(""); - const scrollRef = useRef(null); - - // Auto-scroll to bottom when output changes - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [output]); - - // Reset state when modal opens/closes - useEffect(() => { - if (open) { - reset(); - setManualToken(""); - } - }, [open, reset]); - - const handleUseToken = useCallback(() => { - const tokenToUse = token || manualToken; - if (tokenToUse.trim()) { - onTokenObtained(tokenToUse.trim()); - onClose(); - } - }, [token, manualToken, onTokenObtained, onClose]); - - const copyCommand = useCallback(() => { - navigator.clipboard.writeText("claude setup-token"); - toast.success("Command copied to clipboard"); - }, []); - - const handleRetry = useCallback(() => { - reset(); - setManualToken(""); - }, [reset]); - - return ( - - - - - - Claude Subscription Authentication - - - {authState === "idle" && - "Click Start to begin the authentication process."} - {authState === "running" && - "Complete the sign-in in your browser..."} - {authState === "success" && - "Authentication successful! Your token has been captured."} - {authState === "error" && - "Authentication failed. Please try again or enter the token manually."} - {authState === "manual" && - "Copy the token from your terminal and paste it below."} - - - - {/* Terminal Output */} -
- {output.map((line, index) => ( -
- {line.startsWith("Error") || line.startsWith("⚠") ? ( - {line} - ) : line.startsWith("βœ“") ? ( - {line} - ) : ( - line - )} -
- ))} - {output.length === 0 && ( -
Waiting to start...
- )} - {authState === "running" && ( -
- - Waiting for authentication... -
- )} -
- - {/* Manual Token Input (for fallback) */} - {(authState === "manual" || authState === "error") && ( -
-
- Run this command in your terminal: - - claude setup-token - - -
-
- - setManualToken(e.target.value)} - className="bg-input border-border text-foreground" - data-testid="manual-token-input" - /> -
-
- )} - - {/* Success State */} - {authState === "success" && ( -
- -
-

- Token captured successfully! -

-

- Click "Use Token" to save and continue. -

-
-
- )} - - {/* Error State */} - {error && authState === "error" && ( -
- -
-

Error

-

{error}

-
-
- )} - - - - - {authState === "idle" && ( - - )} - - {authState === "running" && ( - - )} - - {authState === "success" && ( - - )} - - {authState === "manual" && ( - - )} - - {authState === "error" && ( - <> - {manualToken.trim() && ( - - )} - - - )} - -
-
- ); -} diff --git a/apps/app/src/components/views/setup-view/hooks/index.ts b/apps/app/src/components/views/setup-view/hooks/index.ts index 88db7c8c..f4414692 100644 --- a/apps/app/src/components/views/setup-view/hooks/index.ts +++ b/apps/app/src/components/views/setup-view/hooks/index.ts @@ -1,5 +1,4 @@ // Re-export all hooks for easier imports export { useCliStatus } from "./use-cli-status"; export { useCliInstallation } from "./use-cli-installation"; -export { useOAuthAuthentication } from "./use-oauth-authentication"; export { useTokenSave } from "./use-token-save"; diff --git a/apps/app/src/components/views/setup-view/hooks/use-oauth-authentication.ts b/apps/app/src/components/views/setup-view/hooks/use-oauth-authentication.ts deleted file mode 100644 index f1b8957f..00000000 --- a/apps/app/src/components/views/setup-view/hooks/use-oauth-authentication.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { useState, useCallback, useRef, useEffect } from "react"; -import { getElectronAPI } from "@/lib/electron"; - -type AuthState = "idle" | "running" | "success" | "error" | "manual"; - -interface UseOAuthAuthenticationOptions { - cliType: "claude"; - enabled?: boolean; -} - -export function useOAuthAuthentication({ - cliType, - enabled = true, -}: UseOAuthAuthenticationOptions) { - const [authState, setAuthState] = useState("idle"); - const [output, setOutput] = useState([]); - const [token, setToken] = useState(""); - const [error, setError] = useState(null); - const unsubscribeRef = useRef<(() => void) | null>(null); - - // Reset state when disabled - useEffect(() => { - if (!enabled) { - setAuthState("idle"); - setOutput([]); - setToken(""); - setError(null); - - // Cleanup subscription - if (unsubscribeRef.current) { - unsubscribeRef.current(); - unsubscribeRef.current = null; - } - } - }, [enabled]); - - const startAuth = useCallback(async () => { - const api = getElectronAPI(); - if (!api.setup) { - setError("Setup API not available"); - setAuthState("error"); - return; - } - - setAuthState("running"); - setOutput([ - "Starting authentication...", - `Running ${cliType} CLI in an embedded terminal so you don't need to copy/paste.`, - "When your browser opens, complete sign-in and return here.", - "", - ]); - setError(null); - setToken(""); - - // Subscribe to progress events - if (api.setup.onAuthProgress) { - unsubscribeRef.current = api.setup.onAuthProgress((progress) => { - if (progress.cli === cliType && progress.data) { - // Split by newlines and add each line - const normalized = progress.data.replace(/\r/g, "\n"); - const lines = normalized - .split("\n") - .map((line: string) => line.trimEnd()) - .filter((line: string) => line.length > 0); - if (lines.length > 0) { - setOutput((prev) => [...prev, ...lines]); - } - } - }); - } - - try { - // Call the auth API - const result = await api.setup.authClaude(); - - // Cleanup subscription - if (unsubscribeRef.current) { - unsubscribeRef.current(); - unsubscribeRef.current = null; - } - - if (!result) { - setError("Authentication API not available"); - setAuthState("error"); - return; - } - - // Check for token (only available for Claude) - const resultToken = - cliType === "claude" && "token" in result ? result.token : undefined; - const resultTerminalOpened = - cliType === "claude" && "terminalOpened" in result - ? result.terminalOpened - : false; - - if (result.success && resultToken && typeof resultToken === "string") { - setToken(resultToken); - setAuthState("success"); - setOutput((prev) => [ - ...prev, - "", - "βœ“ Authentication successful!", - "βœ“ Token captured automatically.", - ]); - } else if (result.requiresManualAuth) { - // Terminal was opened - user needs to copy token manually - setAuthState("manual"); - // Don't add extra messages if terminalOpened - the progress messages already explain - if (!resultTerminalOpened) { - const extraMessages = [ - "", - "⚠ Could not capture token automatically.", - ]; - if (result.error) { - extraMessages.push(result.error); - } - setOutput((prev) => [ - ...prev, - ...extraMessages, - "Please copy the token from above and paste it below.", - ]); - } - } else { - setError(result.error || "Authentication failed"); - setAuthState("error"); - } - } catch (err: unknown) { - // Cleanup subscription - if (unsubscribeRef.current) { - unsubscribeRef.current(); - unsubscribeRef.current = null; - } - - const errorMessage = - err instanceof Error - ? err.message - : typeof err === "object" && err !== null && "error" in err - ? String((err as { error: unknown }).error) - : "Authentication failed"; - - // Check if we should fall back to manual mode - if ( - typeof err === "object" && - err !== null && - "requiresManualAuth" in err && - (err as { requiresManualAuth: boolean }).requiresManualAuth - ) { - setAuthState("manual"); - setOutput((prev) => [ - ...prev, - "", - "⚠ " + errorMessage, - "Please copy the token manually and paste it below.", - ]); - } else { - setError(errorMessage); - setAuthState("error"); - } - } - }, [cliType]); - - const reset = useCallback(() => { - setAuthState("idle"); - setOutput([]); - setToken(""); - setError(null); - if (unsubscribeRef.current) { - unsubscribeRef.current(); - unsubscribeRef.current = null; - } - }, []); - - return { authState, output, token, error, startAuth, reset }; -} diff --git a/apps/app/src/components/views/setup-view/steps/claude-setup-step.tsx b/apps/app/src/components/views/setup-view/steps/claude-setup-step.tsx index 20637530..f1601d36 100644 --- a/apps/app/src/components/views/setup-view/steps/claude-setup-step.tsx +++ b/apps/app/src/components/views/setup-view/steps/claude-setup-step.tsx @@ -11,6 +11,12 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { useSetupStore } from "@/store/setup-store"; import { useAppStore } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; @@ -23,19 +29,17 @@ import { ArrowLeft, ExternalLink, Copy, - AlertCircle, RefreshCw, Download, - Shield, + Info, + AlertTriangle, + ShieldCheck, + XCircle, + Trash2, } from "lucide-react"; import { toast } from "sonner"; -import { SetupTokenModal } from "../dialogs"; import { StatusBadge, TerminalOutput } from "../components"; -import { - useCliStatus, - useCliInstallation, - useTokenSave, -} from "../hooks"; +import { useCliStatus, useCliInstallation, useTokenSave } from "../hooks"; interface ClaudeSetupStepProps { onNext: () => void; @@ -43,9 +47,12 @@ interface ClaudeSetupStepProps { onSkip: () => void; } -// Claude Setup Step - 2 Authentication Options: -// 1. OAuth Token (Subscription): User runs `claude setup-token` and provides the token -// 2. API Key (Pay-per-use): User provides their Anthropic API key directly +type VerificationStatus = "idle" | "verifying" | "verified" | "error"; + +// Claude Setup Step +// Users can either: +// 1. Have Claude CLI installed and authenticated (verified by running a test query) +// 2. Provide an Anthropic API key manually export function ClaudeSetupStep({ onNext, onBack, @@ -60,10 +67,24 @@ export function ClaudeSetupStep({ } = useSetupStore(); const { setApiKeys, apiKeys } = useAppStore(); - const [authMethod, setAuthMethod] = useState<"token" | "api_key" | null>(null); - const [oauthToken, setOAuthToken] = useState(""); const [apiKey, setApiKey] = useState(""); - const [showTokenModal, setShowTokenModal] = useState(false); + + // CLI Verification state + const [cliVerificationStatus, setCliVerificationStatus] = + useState("idle"); + const [cliVerificationError, setCliVerificationError] = useState< + string | null + >(null); + + // API Key Verification state + const [apiKeyVerificationStatus, setApiKeyVerificationStatus] = + useState("idle"); + const [apiKeyVerificationError, setApiKeyVerificationError] = useState< + string | null + >(null); + + // Delete API Key state + const [isDeletingApiKey, setIsDeletingApiKey] = useState(false); // Memoize API functions to prevent infinite loops const statusApi = useCallback( @@ -101,34 +122,151 @@ export function ClaudeSetupStep({ getStoreState, }); - const { isSaving: isSavingOAuth, saveToken: saveOAuthToken } = useTokenSave({ - provider: "anthropic_oauth_token", - onSuccess: () => { - setClaudeAuthStatus({ - authenticated: true, - method: "oauth_token", - hasCredentialsFile: false, - oauthTokenValid: true, - }); - setAuthMethod(null); - checkStatus(); - }, - }); + const { isSaving: isSavingApiKey, saveToken: saveApiKeyToken } = useTokenSave( + { + provider: "anthropic", + onSuccess: () => { + setClaudeAuthStatus({ + authenticated: true, + method: "api_key", + hasCredentialsFile: false, + apiKeyValid: true, + }); + setApiKeys({ ...apiKeys, anthropic: apiKey }); + toast.success("API key saved successfully!"); + }, + } + ); - const { isSaving: isSavingApiKey, saveToken: saveApiKeyToken } = useTokenSave({ - provider: "anthropic", - onSuccess: () => { - setClaudeAuthStatus({ - authenticated: true, - method: "api_key", - hasCredentialsFile: false, - apiKeyValid: true, - }); - setApiKeys({ ...apiKeys, anthropic: apiKey }); - setAuthMethod(null); - checkStatus(); - }, - }); + // Verify CLI authentication by running a test query (uses CLI credentials only, not API key) + const verifyCliAuth = useCallback(async () => { + setCliVerificationStatus("verifying"); + setCliVerificationError(null); + + try { + const api = getElectronAPI(); + if (!api.setup?.verifyClaudeAuth) { + setCliVerificationStatus("error"); + setCliVerificationError("Verification API not available"); + return; + } + + // Pass "cli" to verify CLI authentication only (ignores any API key) + const result = await api.setup.verifyClaudeAuth("cli"); + + // Check for "Limit reached" error - treat as unverified + const hasLimitReachedError = + result.error?.toLowerCase().includes("limit reached") || + result.error?.toLowerCase().includes("rate limit"); + + if (result.authenticated && !hasLimitReachedError) { + setCliVerificationStatus("verified"); + setClaudeAuthStatus({ + authenticated: true, + method: "cli_authenticated", + hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false, + }); + toast.success("Claude CLI authentication verified!"); + } else { + setCliVerificationStatus("error"); + setCliVerificationError( + hasLimitReachedError + ? "Rate limit reached. Please try again later." + : result.error || "Authentication failed" + ); + setClaudeAuthStatus({ + authenticated: false, + method: "none", + hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false, + }); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Verification failed"; + // Also check for limit reached in caught errors + const isLimitError = + errorMessage.toLowerCase().includes("limit reached") || + errorMessage.toLowerCase().includes("rate limit"); + setCliVerificationStatus("error"); + setCliVerificationError( + isLimitError + ? "Rate limit reached. Please try again later." + : errorMessage + ); + } + }, [claudeAuthStatus, setClaudeAuthStatus]); + + // Verify API Key authentication (uses API key only) + const verifyApiKeyAuth = useCallback(async () => { + setApiKeyVerificationStatus("verifying"); + setApiKeyVerificationError(null); + + try { + const api = getElectronAPI(); + if (!api.setup?.verifyClaudeAuth) { + setApiKeyVerificationStatus("error"); + setApiKeyVerificationError("Verification API not available"); + return; + } + + // Pass "api_key" to verify API key authentication only + const result = await api.setup.verifyClaudeAuth("api_key"); + + if (result.authenticated) { + setApiKeyVerificationStatus("verified"); + setClaudeAuthStatus({ + authenticated: true, + method: "api_key", + hasCredentialsFile: false, + apiKeyValid: true, + }); + toast.success("API key authentication verified!"); + } else { + setApiKeyVerificationStatus("error"); + setApiKeyVerificationError(result.error || "Authentication failed"); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Verification failed"; + setApiKeyVerificationStatus("error"); + setApiKeyVerificationError(errorMessage); + } + }, [setClaudeAuthStatus]); + + // Delete API Key + const deleteApiKey = useCallback(async () => { + setIsDeletingApiKey(true); + try { + const api = getElectronAPI(); + if (!api.setup?.deleteApiKey) { + toast.error("Delete API not available"); + return; + } + + const result = await api.setup.deleteApiKey("anthropic"); + if (result.success) { + // Clear local state + setApiKey(""); + setApiKeys({ ...apiKeys, anthropic: "" }); + setApiKeyVerificationStatus("idle"); + setApiKeyVerificationError(null); + setClaudeAuthStatus({ + authenticated: false, + method: "none", + hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false, + }); + toast.success("API key deleted successfully"); + } else { + toast.error(result.error || "Failed to delete API key"); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Failed to delete API key"; + toast.error(errorMessage); + } finally { + setIsDeletingApiKey(false); + } + }, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]); // Sync install progress to store useEffect(() => { @@ -148,32 +286,52 @@ export function ClaudeSetupStep({ toast.success("Command copied to clipboard"); }; - // Handle token obtained from the OAuth modal - const handleTokenFromModal = useCallback( - async (token: string) => { - setOAuthToken(token); - setShowTokenModal(false); - await saveOAuthToken(token); - }, - [saveOAuthToken] - ); - - const isAuthenticated = claudeAuthStatus?.authenticated || apiKeys.anthropic; + // User is ready if either method is verified + const hasApiKey = + !!apiKeys.anthropic || + claudeAuthStatus?.method === "api_key" || + claudeAuthStatus?.method === "api_key_env"; + const isCliVerified = cliVerificationStatus === "verified"; + const isApiKeyVerified = apiKeyVerificationStatus === "verified"; + const isReady = isCliVerified || isApiKeyVerified; const getAuthMethodLabel = () => { - if (!isAuthenticated) return null; - if ( - claudeAuthStatus?.method === "oauth_token_env" || - claudeAuthStatus?.method === "oauth_token" - ) - return "Subscription Token"; - if ( - apiKeys.anthropic || - claudeAuthStatus?.method === "api_key" || - claudeAuthStatus?.method === "api_key_env" - ) - return "API Key"; - return "Authenticated"; + if (isApiKeyVerified) return "API Key"; + if (isCliVerified) return "Claude CLI"; + return null; + }; + + // Helper to get status badge for CLI + const getCliStatusBadge = () => { + if (cliVerificationStatus === "verified") { + return ; + } + if (cliVerificationStatus === "error") { + return ; + } + if (isChecking) { + return ; + } + if (claudeCliStatus?.installed) { + // Installed but not yet verified - show yellow unverified badge + return ; + } + return ; + }; + + // Helper to get status badge for API Key + const getApiKeyStatusBadge = () => { + if (apiKeyVerificationStatus === "verified") { + return ; + } + if (apiKeyVerificationStatus === "error") { + return ; + } + if (hasApiKey) { + // API key configured but not yet verified - show yellow unverified badge + return ; + } + return ; }; return ( @@ -183,18 +341,19 @@ export function ClaudeSetupStep({

- Claude Setup + API Key Setup

-

- Configure Claude for code generation -

+

Configure for code generation

- {/* Status Card */} + {/* Requirements Info */}
- Status + + + Authentication Methods +
+ + Choose one of the following methods to authenticate with Claude: +
- -
- CLI Installation - {isChecking ? ( - - ) : claudeCliStatus?.installed ? ( - - ) : ( - - )} -
+ + + {/* Option 1: Claude CLI */} + + +
+
+ +
+

Claude CLI

+

+ Use Claude Code subscription +

+
+
+ {getCliStatusBadge()} +
+
+ + {/* CLI Install Section */} + {!claudeCliStatus?.installed && ( +
+
+ +

+ Install Claude CLI +

+
- {claudeCliStatus?.version && ( -
- Version - - {claudeCliStatus.version} - -
- )} - -
- Authentication - {isAuthenticated ? ( -
- - {getAuthMethodLabel() && ( - - ({getAuthMethodLabel()}) - - )} -
- ) : ( - - )} -
- - - - {/* Installation Section */} - {!claudeCliStatus?.installed && ( - - - - - Install Claude CLI - - - Required for subscription-based authentication - - - -
- -
- - curl -fsSL https://claude.ai/install.sh | bash - - -
-
- -
- -
- - irm https://claude.ai/install.ps1 | iex - - -
-
- - {isInstalling && ( - - )} - - -
-
- )} - - {/* Authentication Section */} - {!isAuthenticated && ( - - - - - Authentication - - Choose your authentication method - - - {/* Option 1: Subscription Token */} - {authMethod === "token" ? ( -
-
- -
-

- Subscription Token -

-

- Use your Claude subscription (no API charges) -

- - {claudeCliStatus?.installed ? ( - <> - {/* Primary: Automated OAuth setup */} +
+ +
+ + curl -fsSL https://claude.ai/install.sh | bash + - - {/* Divider */} -
-
- -
-
- - or paste manually - -
-
- - {/* Fallback: Manual token entry */} -
- - setOAuthToken(e.target.value)} - className="bg-input border-border text-foreground" - data-testid="oauth-token-input" - /> -
- -
- - -
- - ) : ( -
-

- - Install Claude CLI first to use subscription - authentication -

+
+ +
+ +
+ + irm https://claude.ai/install.ps1 | iex + + +
+
+ + {isInstalling && ( + + )} + + +
+ )} + + {/* CLI Version Info */} + {claudeCliStatus?.installed && claudeCliStatus?.version && ( +

+ Version: {claudeCliStatus.version} +

+ )} + + {/* CLI Verification Status */} + {cliVerificationStatus === "verifying" && ( +
+ +
+

+ Verifying CLI authentication... +

+

+ Running a test query +

+
+
+ )} + + {cliVerificationStatus === "verified" && ( +
+ +
+

+ CLI Authentication verified! +

+

+ Your Claude CLI is working correctly. +

+
+
+ )} + + {cliVerificationStatus === "error" && cliVerificationError && ( +
+ +
+

+ Verification failed +

+

+ {cliVerificationError} +

+ {cliVerificationError.includes("login") && ( +
+

+ Run this command in your terminal: +

+
+ + claude login + + +
+
+ )} +
+
+ )} + + {/* CLI Verify Button */} + + + + + {/* Option 2: API Key */} + + +
+
+ +
+

+ Anthropic API Key +

+

+ Pay-per-use with your own API key +

+
+
+ {getApiKeyStatusBadge()} +
+
+ + {/* API Key Input */} +
+
+ + setApiKey(e.target.value)} + className="bg-input border-border text-foreground" + data-testid="anthropic-api-key-input" + /> +

+ Don't have an API key?{" "} + + Get one from Anthropic Console + + +

+
+ +
+ + {hasApiKey && ( + )}
-
- ) : authMethod === "api_key" ? ( - /* Option 2: API Key */ -
-
- -
-

API Key

-

- Pay-per-use with your Anthropic API key -

-
- - setApiKey(e.target.value)} - className="bg-input border-border text-foreground" - data-testid="anthropic-api-key-input" - /> -

- Get your API key from{" "} - - console.anthropic.com - - -

-
- -
- - -
-
-
-
- ) : ( - /* Auth Method Selection */ -
- + )} - -
- )} - - - )} + )} - {/* Success State */} - {isAuthenticated && ( - - -
-
- -
-
-

- Claude is ready to use! -

-

- {getAuthMethodLabel() && `Using ${getAuthMethodLabel()}. `}You - can proceed to the next step -

-
-
-
-
- )} + {apiKeyVerificationStatus === "error" && + apiKeyVerificationError && ( +
+ +
+

+ Verification failed +

+

+ {apiKeyVerificationError} +

+
+
+ )} + + {/* API Key Verify Button */} + + + + + + {/* Navigation */}
@@ -582,7 +756,8 @@ export function ClaudeSetupStep({
- - {/* OAuth Setup Modal */} - setShowTokenModal(false)} - onTokenObtained={handleTokenFromModal} - />
); } diff --git a/apps/app/src/components/views/setup-view/steps/complete-step.tsx b/apps/app/src/components/views/setup-view/steps/complete-step.tsx index bcffebc1..a4391e3d 100644 --- a/apps/app/src/components/views/setup-view/steps/complete-step.tsx +++ b/apps/app/src/components/views/setup-view/steps/complete-step.tsx @@ -1,11 +1,6 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { - CheckCircle2, - AlertCircle, - Shield, - Sparkles, -} from "lucide-react"; +import { CheckCircle2, AlertCircle, Shield, Sparkles } from "lucide-react"; import { useSetupStore } from "@/store/setup-store"; import { useAppStore } from "@/store/app-store"; @@ -14,8 +9,7 @@ interface CompleteStepProps { } export function CompleteStep({ onFinish }: CompleteStepProps) { - const { claudeCliStatus, claudeAuthStatus } = - useSetupStore(); + const { claudeCliStatus, claudeAuthStatus } = useSetupStore(); const { apiKeys } = useAppStore(); const claudeReady = @@ -38,44 +32,6 @@ export function CompleteStep({ onFinish }: CompleteStepProps) {

-
- - -
- {claudeReady ? ( - - ) : ( - - )} -
-

Claude

-

- {claudeReady ? "Ready to use" : "Configure later in settings"} -

-
-
-
-
-
- -
-
- -
-

- Your credentials are secure -

-

- API keys are stored locally and never sent to our servers -

-
-
-
-