Compare commits

..

21 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
3b8b14b714 Initial plan 2025-12-16 00:28:05 +00:00
Cody Seibert
049f9a9e37 chore: add Git configuration for HTTPS in workflow files to support CI environment 2025-12-15 19:19:23 -05:00
Cody Seibert
19f1c32805 chore: update Node.js version in workflow files from 20 to 22 2025-12-15 19:08:00 -05:00
Cody Seibert
ece8ff8cbc Merge branch 'main' into api-key-redesign 2025-12-15 19:00:14 -05:00
Cody Seibert
a3a648aef1 feat: add Accordion component with customizable behavior and animations, update Checkbox and Slider components for improved functionality, and enhance package dependencies 2025-12-15 18:57:32 -05:00
Web Dev Cody
3bc2b74d30 Merge pull request #105 from AutoMaker-Org/fix/bug-button-position
Fix/bug button position
2025-12-15 17:57:08 -05:00
trueheads
123b471b68 How many Devs does it take to center a navbar icon? 3, as it turns out. 2025-12-15 15:13:43 -06:00
Cody Seibert
b66d228460 feat: enhance CLI and API key verification buttons to hide when already verified 2025-12-15 15:12:49 -05:00
Kacper
770d67d8c4 feat: refactor bug report button into a reusable component for improved sidebar functionality 2025-12-15 20:49:22 +01:00
Cody Seibert
d42857ec26 refactor: remove CLAUDE_CODE_OAUTH_TOKEN references and update authentication to use ANTHROPIC_API_KEY exclusively 2025-12-15 14:33:58 -05:00
Cody Seibert
54b977ee1b redesign our approach for api keys to not use claude setup-token 2025-12-15 14:24:18 -05:00
Kacper
e8999ba908 chore: update README to include a detailed Table of Contents and Community & Support section 2025-12-15 20:14:44 +01:00
Kacper
96c4383b29 feat: Whe sidebar is closed the bug button is overlapping the l... 2025-12-15 20:07:27 +01:00
Web Dev Cody
93d1d2c41a Merge pull request #104 from AutoMaker-Org/chore/update-readme
chore: update clone url from ssh to https
2025-12-15 13:52:58 -05:00
Shirone
b075af5bc9 chore: update clone url from ssh to https 2025-12-15 19:51:03 +01:00
Web Dev Cody
07ca7fccb8 Merge pull request #102 from AutoMaker-Org/feat/disable-worktree-in-ui
feat: In our Feature Defaults section in setting view we have a...
2025-12-15 12:47:31 -05:00
Web Dev Cody
797643ffdc Merge pull request #101 from AutoMaker-Org/readme-update
updating readme to reflect logo and featureset
2025-12-15 12:47:11 -05:00
trueheads
7d4052be95 adjustments 2025-12-15 11:24:01 -06:00
Shirone
1036719f2a Update apps/app/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-15 18:19:46 +01:00
Kacper
1ab520eda3 feat: In our Feature Defaults section in setting view we have a...
Implemented by Automaker auto-mode
2025-12-15 18:17:19 +01:00
trueheads
658f7d816e updating readme to reflect logo and featureset 2025-12-15 11:01:36 -06:00
47 changed files with 13649 additions and 12020 deletions

View File

@@ -21,10 +21,15 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
cache: "npm" cache: "npm"
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- name: Configure Git for HTTPS
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies - name: Install dependencies
# Use npm install instead of npm ci to correctly resolve platform-specific # Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)

View File

@@ -20,10 +20,15 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
cache: "npm" cache: "npm"
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- name: Configure Git for HTTPS
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies - name: Install dependencies
# Use npm install instead of npm ci to correctly resolve platform-specific # Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)

View File

@@ -39,10 +39,15 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
cache: "npm" cache: "npm"
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- name: Configure Git for HTTPS
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies - name: Install dependencies
# Use npm install instead of npm ci to correctly resolve platform-specific # Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)

View File

@@ -20,10 +20,15 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
cache: "npm" cache: "npm"
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- name: Configure Git for HTTPS
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies - name: Install dependencies
# Use npm install instead of npm ci to correctly resolve platform-specific # Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)

View File

@@ -1,3 +1,7 @@
<p align="center">
<img src="apps/app/public/readme_logo.png" alt="Automaker Logo" height="80" />
</p>
> **[!TIP]** > **[!TIP]**
> >
> **Learn more about Agentic Coding!** > **Learn more about Agentic Coding!**
@@ -10,8 +14,39 @@
**Stop typing code. Start directing AI agents.** **Stop typing code. Start directing AI agents.**
<details open>
<summary><h2>Table of Contents</h2></summary>
- [What Makes Automaker Different?](#what-makes-automaker-different)
- [The Workflow](#the-workflow)
- [Powered by Claude Code](#powered-by-claude-code)
- [Why This Matters](#why-this-matters)
- [Security Disclaimer](#security-disclaimer)
- [Community & Support](#community--support)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Quick Start](#quick-start)
- [How to Run](#how-to-run)
- [Development Mode](#development-mode)
- [Electron Desktop App (Recommended)](#electron-desktop-app-recommended)
- [Web Browser Mode](#web-browser-mode)
- [Building for Production](#building-for-production)
- [Running Production Build](#running-production-build)
- [Testing](#testing)
- [Linting](#linting)
- [Authentication Options](#authentication-options)
- [Persistent Setup (Optional)](#persistent-setup-optional)
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Learn More](#learn-more)
- [License](#license)
</details>
Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Code automatically implement them. Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Code automatically implement them.
![Automaker UI](https://i.imgur.com/jdwKydM.png)
## What Makes Automaker Different? ## What Makes Automaker Different?
Traditional development tools help you write code. Automaker helps you **orchestrate AI agents** to build entire features autonomously. Think of it as having a team of AI developers working for you—you define what needs to be built, and Automaker handles the implementation. Traditional development tools help you write code. Automaker helps you **orchestrate AI agents** to build entire features autonomously. Think of it as having a team of AI developers working for you—you define what needs to be built, and Automaker handles the implementation.
@@ -48,6 +83,22 @@ The future of software development is **agentic coding**—where developers beco
--- ---
## Community & Support
Join the **Agentic Jumpstart** to connect with other builders exploring **agentic coding** and autonomous development workflows.
In the Discord, you can:
- 💬 Discuss agentic coding patterns and best practices
- 🧠 Share ideas for AI-driven development workflows
- 🛠️ Get help setting up or extending Automaker
- 🚀 Show off projects built with AI agents
- 🤝 Collaborate with other developers and contributors
👉 **Join the Discord:**
https://discord.gg/jjem7aEDKU
---
## Getting Started ## Getting Started
### Prerequisites ### Prerequisites
@@ -60,7 +111,7 @@ The future of software development is **agentic coding**—where developers beco
```bash ```bash
# 1. Clone the repo # 1. Clone the repo
git clone git@github.com:AutoMaker-Org/automaker.git git clone https://github.com/AutoMaker-Org/automaker.git
cd automaker cd automaker
# 2. Install dependencies # 2. Install dependencies
@@ -144,21 +195,17 @@ npm run lint
Automaker supports multiple authentication methods (in order of priority): Automaker supports multiple authentication methods (in order of priority):
| Method | Environment Variable | Description | | Method | Environment Variable | Description |
| -------------------- | ------------------------- | --------------------------------------------------------- | | ---------------- | -------------------- | ------------------------------- |
| OAuth Token (env) | `CLAUDE_CODE_OAUTH_TOKEN` | From `claude setup-token` - uses your Claude subscription | | API Key (env) | `ANTHROPIC_API_KEY` | Anthropic API key |
| OAuth Token (stored) | — | Stored in app credentials file | | API Key (stored) | — | Anthropic API key stored in app |
| API Key (stored) | — | Anthropic API key stored in app |
| API Key (env) | `ANTHROPIC_API_KEY` | Pay-per-use API key |
**Recommended:** Use `CLAUDE_CODE_OAUTH_TOKEN` if you have a Claude subscription.
### Persistent Setup (Optional) ### Persistent Setup (Optional)
Add to your `~/.bashrc` or `~/.zshrc`: Add to your `~/.bashrc` or `~/.zshrc`:
```bash ```bash
export CLAUDE_CODE_OAUTH_TOKEN="YOUR_TOKEN_HERE" export ANTHROPIC_API_KEY="YOUR_API_KEY_HERE"
``` ```
Then restart your terminal or run `source ~/.bashrc`. Then restart your terminal or run `source ~/.bashrc`.

View File

@@ -1,123 +0,0 @@
# Automaker
Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion.
---
> **[!CAUTION]**
>
> ## Security Disclaimer
>
> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.**
>
> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it.
>
> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine.
>
> **[Read the full disclaimer](../DISCLAIMER.md)**
---
## Getting Started
**Step 1:** Clone this repository:
```bash
git clone git@github.com:AutoMaker-Org/automaker.git
cd automaker
```
**Step 2:** Install dependencies:
```bash
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:
```bash
claude setup-token
```
> **⚠️ 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:
```bash
export CLAUDE_CODE_OAUTH_TOKEN="your-token-here"
```
**Step 5:** Start the development server:
```bash
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
- 🤖 **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress"
- 🧠 **Multi-Model Support** - Choose from multiple AI models including Claude Opus, Sonnet, and more
- 💭 **Extended Thinking** - Enable extended thinking modes for complex problem-solving
- 📡 **Real-time Agent Output** - View live agent output, logs, and file diffs as features are being implemented
- 🔍 **Project Analysis** - AI-powered project structure analysis to understand your codebase
- 📁 **Context Management** - Add context files to help AI agents understand your project better
- 💡 **Feature Suggestions** - AI-generated feature suggestions based on your project
- 🖼️ **Image Support** - Attach images and screenshots to feature descriptions
-**Concurrent Processing** - Configure concurrency to process multiple features simultaneously
- 🧪 **Test Integration** - Automatic test running and verification for implemented features
- 🔀 **Git Integration** - View git diffs and track changes made by AI agents
- 👤 **AI Profiles** - Create and manage different AI agent profiles for various tasks
- 💬 **Chat History** - Keep track of conversations and interactions with AI agents
- ⌨️ **Keyboard Shortcuts** - Efficient navigation and actions via keyboard shortcuts
- 🎨 **Dark/Light Theme** - Beautiful UI with theme support
- 🖥️ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux
## Tech Stack
- [Next.js](https://nextjs.org) - React framework
- [Electron](https://www.electronjs.org/) - Desktop application framework
- [Tailwind CSS](https://tailwindcss.com/) - Styling
- [Zustand](https://zustand-demo.pmnd.rs/) - State management
- [dnd-kit](https://dndkit.com/) - Drag and drop functionality
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
## License
See [LICENSE](../LICENSE) for details.

View File

@@ -2,9 +2,6 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "export", output: "export",
env: {
CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN || "",
},
}; };
export default nextConfig; export default nextConfig;

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -11,7 +11,7 @@ export async function POST(request: NextRequest) {
const { apiKey } = await request.json(); const { apiKey } = await request.json();
// Use provided API key or fall back to environment variable // Use provided API key or fall back to environment variable
const effectiveApiKey = apiKey || process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN; const effectiveApiKey = apiKey || process.env.ANTHROPIC_API_KEY;
if (!effectiveApiKey) { if (!effectiveApiKey) {
return NextResponse.json( return NextResponse.json(

View File

@@ -2521,3 +2521,34 @@
.xml-editor .xml-highlight { .xml-editor .xml-highlight {
z-index: 0; z-index: 0;
} }
/* Accordion animations - CSS-only approach */
@keyframes accordion-down {
from {
height: 0;
opacity: 0;
}
to {
height: var(--accordion-content-height, auto);
opacity: 1;
}
}
@keyframes accordion-up {
from {
height: var(--accordion-content-height, auto);
opacity: 1;
}
to {
height: 0;
opacity: 0;
}
}
.animate-accordion-down {
animation: accordion-down 0.2s ease-out forwards;
}
.animate-accordion-up {
animation: accordion-up 0.2s ease-out forwards;
}

View File

@@ -195,6 +195,33 @@ const PROJECT_THEME_OPTIONS = [
})), })),
] as const; ] as const;
// Reusable Bug Report Button Component
const BugReportButton = ({
sidebarExpanded,
onClick
}: {
sidebarExpanded: boolean;
onClick: () => void;
}) => {
return (
<button
onClick={onClick}
className={cn(
"titlebar-no-drag px-3 py-2.5 rounded-xl",
"text-muted-foreground hover:text-foreground hover:bg-accent/80",
"border border-transparent hover:border-border/40",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.97]",
sidebarExpanded && "absolute right-3"
)}
title="Report Bug / Feature Request"
data-testid={sidebarExpanded ? "bug-report-link" : "bug-report-link-collapsed"}
>
<Bug className="w-4 h-4" />
</button>
);
};
export function Sidebar() { export function Sidebar() {
const { const {
projects, projects,
@@ -836,6 +863,12 @@ export function Sidebar() {
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject] [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject]
); );
// Handle bug report button click
const handleBugReportClick = useCallback(() => {
const api = getElectronAPI();
api.openExternalLink("https://github.com/AutoMaker-Org/automaker/issues");
}, []);
/** /**
* Opens the system folder selection dialog and initializes the selected project. * Opens the system folder selection dialog and initializes the selected project.
* Used by both the 'O' keyboard shortcut and the folder icon button. * Used by both the 'O' keyboard shortcut and the folder icon button.
@@ -1394,30 +1427,20 @@ export function Sidebar() {
</div> </div>
)} )}
</div> </div>
{/* Bug Report Button */} {/* Bug Report Button - Inside logo container when expanded */}
<button {sidebarOpen && <BugReportButton sidebarExpanded onClick={handleBugReportClick} />}
onClick={() => {
const api = getElectronAPI();
api.openExternalLink(
"https://github.com/AutoMaker-Org/automaker/issues"
);
}}
className={cn(
"titlebar-no-drag p-1.5 rounded-lg absolute right-3",
"text-muted-foreground hover:text-foreground hover:bg-accent/80",
"transition-all duration-200 ease-out",
"hover:scale-105 active:scale-95"
)}
title="Report Bug / Feature Request"
data-testid="bug-report-link"
>
<Bug className="w-4 h-4" />
</button>
</div> </div>
{/* Bug Report Button - Collapsed sidebar version */}
{!sidebarOpen && (
<div className="px-3 mt-1.5 flex justify-center">
<BugReportButton sidebarExpanded={false} onClick={handleBugReportClick} />
</div>
)}
{/* Project Actions - Moved above project selector */} {/* Project Actions - Moved above project selector */}
{sidebarOpen && ( {sidebarOpen && (
<div className="flex items-center gap-2.5 titlebar-no-drag px-3 mt-4"> <div className="flex items-center gap-2.5 titlebar-no-drag px-3 mt-5">
<button <button
onClick={() => setShowNewProjectModal(true)} onClick={() => setShowNewProjectModal(true)}
className={cn( className={cn(
@@ -1789,7 +1812,7 @@ export function Sidebar() {
)} )}
{/* Nav Items - Scrollable */} {/* Nav Items - Scrollable */}
<nav className="flex-1 overflow-y-auto px-3 mt-5 pb-2"> <nav className={cn("flex-1 overflow-y-auto px-3 pb-2", sidebarOpen ? "mt-5" : "mt1")}>
{!currentProject && sidebarOpen ? ( {!currentProject && sidebarOpen ? (
// Placeholder when no project is selected (only in expanded state) // Placeholder when no project is selected (only in expanded state)
<div className="flex items-center justify-center h-full px-4"> <div className="flex items-center justify-center h-full px-4">
@@ -1802,7 +1825,7 @@ export function Sidebar() {
) : currentProject ? ( ) : currentProject ? (
// Navigation sections when project is selected // Navigation sections when project is selected
navSections.map((section, sectionIdx) => ( navSections.map((section, sectionIdx) => (
<div key={sectionIdx} className={sectionIdx > 0 ? "mt-6" : ""}> <div key={sectionIdx} className={sectionIdx > 0 && sidebarOpen ? "mt-6" : ""}>
{/* Section Label */} {/* Section Label */}
{section.label && sidebarOpen && ( {section.label && sidebarOpen && (
<div className="hidden lg:block px-3 mb-2"> <div className="hidden lg:block px-3 mb-2">
@@ -1812,7 +1835,7 @@ export function Sidebar() {
</div> </div>
)} )}
{section.label && !sidebarOpen && ( {section.label && !sidebarOpen && (
<div className="h-px bg-border/30 mx-2 mb-3"></div> <div className="h-px bg-border/30 mx-2 my-1.5"></div>
)} )}
{/* Nav Items */} {/* Nav Items */}

View File

@@ -0,0 +1,243 @@
"use client";
import * as React from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
type AccordionType = "single" | "multiple";
interface AccordionContextValue {
type: AccordionType;
value: string | string[];
onValueChange: (value: string) => void;
collapsible?: boolean;
}
const AccordionContext = React.createContext<AccordionContextValue | null>(
null
);
interface AccordionProps extends React.HTMLAttributes<HTMLDivElement> {
type?: "single" | "multiple";
value?: string | string[];
defaultValue?: string | string[];
onValueChange?: (value: string | string[]) => void;
collapsible?: boolean;
}
const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
(
{
type = "single",
value,
defaultValue,
onValueChange,
collapsible = false,
className,
children,
...props
},
ref
) => {
const [internalValue, setInternalValue] = React.useState<string | string[]>(
() => {
if (value !== undefined) return value;
if (defaultValue !== undefined) return defaultValue;
return type === "single" ? "" : [];
}
);
const currentValue = value !== undefined ? value : internalValue;
const handleValueChange = React.useCallback(
(itemValue: string) => {
let newValue: string | string[];
if (type === "single") {
if (currentValue === itemValue && collapsible) {
newValue = "";
} else if (currentValue === itemValue && !collapsible) {
return;
} else {
newValue = itemValue;
}
} else {
const currentArray = Array.isArray(currentValue)
? currentValue
: [currentValue].filter(Boolean);
if (currentArray.includes(itemValue)) {
newValue = currentArray.filter((v) => v !== itemValue);
} else {
newValue = [...currentArray, itemValue];
}
}
if (value === undefined) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
},
[type, currentValue, collapsible, value, onValueChange]
);
const contextValue = React.useMemo(
() => ({
type,
value: currentValue,
onValueChange: handleValueChange,
collapsible,
}),
[type, currentValue, handleValueChange, collapsible]
);
return (
<AccordionContext.Provider value={contextValue}>
<div
ref={ref}
data-slot="accordion"
className={cn("w-full", className)}
{...props}
>
{children}
</div>
</AccordionContext.Provider>
);
}
);
Accordion.displayName = "Accordion";
interface AccordionItemContextValue {
value: string;
isOpen: boolean;
}
const AccordionItemContext =
React.createContext<AccordionItemContextValue | null>(null);
interface AccordionItemProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
}
const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
({ className, value, children, ...props }, ref) => {
const accordionContext = React.useContext(AccordionContext);
if (!accordionContext) {
throw new Error("AccordionItem must be used within an Accordion");
}
const isOpen = Array.isArray(accordionContext.value)
? accordionContext.value.includes(value)
: accordionContext.value === value;
const contextValue = React.useMemo(
() => ({ value, isOpen }),
[value, isOpen]
);
return (
<AccordionItemContext.Provider value={contextValue}>
<div
ref={ref}
data-slot="accordion-item"
data-state={isOpen ? "open" : "closed"}
className={cn("border-b border-border", className)}
{...props}
>
{children}
</div>
</AccordionItemContext.Provider>
);
}
);
AccordionItem.displayName = "AccordionItem";
interface AccordionTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
const AccordionTrigger = React.forwardRef<
HTMLButtonElement,
AccordionTriggerProps
>(({ className, children, ...props }, ref) => {
const accordionContext = React.useContext(AccordionContext);
const itemContext = React.useContext(AccordionItemContext);
if (!accordionContext || !itemContext) {
throw new Error("AccordionTrigger must be used within an AccordionItem");
}
const { onValueChange } = accordionContext;
const { value, isOpen } = itemContext;
return (
<div data-slot="accordion-header" className="flex">
<button
ref={ref}
type="button"
data-slot="accordion-trigger"
data-state={isOpen ? "open" : "closed"}
aria-expanded={isOpen}
onClick={() => onValueChange(value)}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</button>
</div>
);
});
AccordionTrigger.displayName = "AccordionTrigger";
interface AccordionContentProps extends React.HTMLAttributes<HTMLDivElement> {}
const AccordionContent = React.forwardRef<HTMLDivElement, AccordionContentProps>(
({ className, children, ...props }, ref) => {
const itemContext = React.useContext(AccordionItemContext);
const contentRef = React.useRef<HTMLDivElement>(null);
const [height, setHeight] = React.useState<number | undefined>(undefined);
if (!itemContext) {
throw new Error("AccordionContent must be used within an AccordionItem");
}
const { isOpen } = itemContext;
React.useEffect(() => {
if (contentRef.current) {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
setHeight(entry.contentRect.height);
}
});
resizeObserver.observe(contentRef.current);
return () => resizeObserver.disconnect();
}
}, []);
return (
<div
data-slot="accordion-content"
data-state={isOpen ? "open" : "closed"}
className="overflow-hidden text-sm transition-all duration-200 ease-out"
style={{
height: isOpen ? (height !== undefined ? `${height}px` : "auto") : 0,
opacity: isOpen ? 1 : 0,
}}
{...props}
>
<div ref={contentRef}>
<div ref={ref} className={cn("pb-4 pt-0", className)}>
{children}
</div>
</div>
</div>
);
}
);
AccordionContent.displayName = "AccordionContent";
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -6,25 +6,37 @@ import { Check } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef< interface CheckboxProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "checked" | "defaultChecked"> {
React.ElementRef<typeof CheckboxPrimitive.Root>, checked?: boolean | "indeterminate";
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> defaultChecked?: boolean | "indeterminate";
>(({ className, ...props }, ref) => ( onCheckedChange?: (checked: boolean) => void;
<CheckboxPrimitive.Root required?: boolean;
ref={ref} }
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground hover:border-primary/80", const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
className ({ className, onCheckedChange, ...props }, ref) => (
)} <CheckboxPrimitive.Root
{...props} ref={ref}
> className={cn(
<CheckboxPrimitive.Indicator "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground hover:border-primary/80",
className={cn("flex items-center justify-center text-current")} className
)}
onCheckedChange={(checked) => {
// Handle indeterminate state by treating it as false for consumers expecting boolean
if (onCheckedChange) {
onCheckedChange(checked === true);
}
}}
{...props}
> >
<Check className="h-4 w-4" /> <CheckboxPrimitive.Indicator
</CheckboxPrimitive.Indicator> className={cn("flex items-center justify-center text-current")}
</CheckboxPrimitive.Root> >
)); <Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
);
Checkbox.displayName = CheckboxPrimitive.Root.displayName; Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox }; export { Checkbox };

View File

@@ -1,39 +1,43 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react" import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} /> return <SheetPrimitive.Root data-slot="sheet" {...props} />;
} }
function SheetTrigger({ function SheetTrigger({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
} }
function SheetClose({ function SheetClose({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) { }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
} }
function SheetPortal({ function SheetPortal({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) { }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
} }
function SheetOverlay({ interface SheetOverlayProps extends React.HTMLAttributes<HTMLDivElement> {
className, forceMount?: true;
...props }
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
const SheetOverlay = ({ className, ...props }: SheetOverlayProps) => {
const Overlay = SheetPrimitive.Overlay as React.ComponentType<
SheetOverlayProps & { "data-slot": string }
>;
return ( return (
<SheetPrimitive.Overlay <Overlay
data-slot="sheet-overlay" data-slot="sheet-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
@@ -41,21 +45,35 @@ function SheetOverlay({
)} )}
{...props} {...props}
/> />
) );
};
interface SheetContentProps extends React.HTMLAttributes<HTMLDivElement> {
side?: "top" | "right" | "bottom" | "left";
forceMount?: true;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
onPointerDownOutside?: (event: PointerEvent) => void;
onInteractOutside?: (event: Event) => void;
} }
function SheetContent({ const SheetContent = ({
className, className,
children, children,
side = "right", side = "right",
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & { }: SheetContentProps) => {
side?: "top" | "right" | "bottom" | "left" const Content = SheetPrimitive.Content as React.ComponentType<
}) { SheetContentProps & { "data-slot": string }
>;
const Close = SheetPrimitive.Close as React.ComponentType<{
className: string;
children: React.ReactNode;
}>;
return ( return (
<SheetPortal> <SheetPortal>
<SheetOverlay /> <SheetOverlay />
<SheetPrimitive.Content <Content
data-slot="sheet-content" data-slot="sheet-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
@@ -72,14 +90,14 @@ function SheetContent({
{...props} {...props}
> >
{children} {children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> <Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" /> <XIcon className="size-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</SheetPrimitive.Close> </Close>
</SheetPrimitive.Content> </Content>
</SheetPortal> </SheetPortal>
) );
} };
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@@ -88,7 +106,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-1.5 p-4", className)} className={cn("flex flex-col gap-1.5 p-4", className)}
{...props} {...props}
/> />
) );
} }
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -98,34 +116,39 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
) );
} }
function SheetTitle({ interface SheetTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}
className,
...props const SheetTitle = ({ className, ...props }: SheetTitleProps) => {
}: React.ComponentProps<typeof SheetPrimitive.Title>) { const Title = SheetPrimitive.Title as React.ComponentType<
SheetTitleProps & { "data-slot": string }
>;
return ( return (
<SheetPrimitive.Title <Title
data-slot="sheet-title" data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)} className={cn("text-foreground font-semibold", className)}
{...props} {...props}
/> />
) );
} };
function SheetDescription({ interface SheetDescriptionProps
className, extends React.HTMLAttributes<HTMLParagraphElement> {}
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) { const SheetDescription = ({ className, ...props }: SheetDescriptionProps) => {
const Description = SheetPrimitive.Description as React.ComponentType<
SheetDescriptionProps & { "data-slot": string }
>;
return ( return (
<SheetPrimitive.Description <Description
data-slot="sheet-description" data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} };
export { export {
Sheet, Sheet,
@@ -136,4 +159,4 @@ export {
SheetFooter, SheetFooter,
SheetTitle, SheetTitle,
SheetDescription, SheetDescription,
} };

View File

@@ -4,24 +4,38 @@ import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider"; import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const Slider = React.forwardRef< interface SliderProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "defaultValue" | "dir"> {
React.ComponentRef<typeof SliderPrimitive.Root>, value?: number[];
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> defaultValue?: number[];
>(({ className, ...props }, ref) => ( onValueChange?: (value: number[]) => void;
<SliderPrimitive.Root onValueCommit?: (value: number[]) => void;
ref={ref} min?: number;
className={cn( max?: number;
"relative flex w-full touch-none select-none items-center", step?: number;
className disabled?: boolean;
)} orientation?: "horizontal" | "vertical";
{...props} dir?: "ltr" | "rtl";
> inverted?: boolean;
<SliderPrimitive.Track className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted cursor-pointer"> minStepsBetweenThumbs?: number;
<SliderPrimitive.Range className="slider-range absolute h-full bg-primary" /> }
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent" /> const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
</SliderPrimitive.Root> ({ className, ...props }, ref) => (
)); <SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted cursor-pointer">
<SliderPrimitive.Range className="slider-range absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent" />
</SliderPrimitive.Root>
)
);
Slider.displayName = SliderPrimitive.Root.displayName; Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider }; export { Slider };

View File

@@ -1,23 +1,61 @@
import { useAppStore } from "@/store/app-store"; import { useAppStore } from "@/store/app-store";
import { useSetupStore } from "@/store/setup-store"; import { useSetupStore } from "@/store/setup-store";
import { Button } from "@/components/ui/button"; 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 { ApiKeyField } from "./api-key-field";
import { buildProviderConfigs } from "@/config/api-providers"; import { buildProviderConfigs } from "@/config/api-providers";
import { AuthenticationStatusDisplay } from "./authentication-status-display"; import { AuthenticationStatusDisplay } from "./authentication-status-display";
import { SecurityNotice } from "./security-notice"; import { SecurityNotice } from "./security-notice";
import { useApiKeyManagement } from "./hooks/use-api-key-management"; import { useApiKeyManagement } from "./hooks/use-api-key-management";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useState, useCallback } from "react";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
export function ApiKeysSection() { export function ApiKeysSection() {
const { apiKeys } = useAppStore(); const { apiKeys, setApiKeys } = useAppStore();
const { claudeAuthStatus } = useSetupStore(); const { claudeAuthStatus, setClaudeAuthStatus, setSetupComplete } = useSetupStore();
const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false);
const { providerConfigParams, apiKeyStatus, handleSave, saved } = const { providerConfigParams, apiKeyStatus, handleSave, saved } =
useApiKeyManagement(); useApiKeyManagement();
const providerConfigs = buildProviderConfigs(providerConfigParams); 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 ( return (
<div <div
id="api-keys" id="api-keys"
@@ -55,8 +93,8 @@ export function ApiKeysSection() {
{/* Security Notice */} {/* Security Notice */}
<SecurityNotice /> <SecurityNotice />
{/* Save Button */} {/* Action Buttons */}
<div className="flex items-center gap-4 pt-2"> <div className="flex flex-wrap items-center gap-3 pt-2">
<Button <Button
onClick={handleSave} onClick={handleSave}
data-testid="save-settings" data-testid="save-settings"
@@ -79,6 +117,33 @@ export function ApiKeysSection() {
"Save API Keys" "Save API Keys"
)} )}
</Button> </Button>
<Button
onClick={openSetupWizard}
variant="outline"
className="h-10 border-border"
data-testid="run-setup-wizard"
>
<Settings className="w-4 h-4 mr-2" />
Run Setup Wizard
</Button>
{apiKeys.anthropic && (
<Button
onClick={deleteAnthropicKey}
disabled={isDeletingAnthropicKey}
variant="outline"
className="h-10 border-red-500/30 text-red-500 hover:bg-red-500/10 hover:border-red-500/50"
data-testid="delete-anthropic-key"
>
{isDeletingAnthropicKey ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Trash2 className="w-4 h-4 mr-2" />
)}
Delete Anthropic Key
</Button>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -48,14 +48,14 @@ export function AuthenticationStatusDisplay({
<> <>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" /> <CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
<span className="text-green-400 font-medium">Authenticated</span> <span className="text-green-400 font-medium">
Authenticated
</span>
</div> </div>
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center gap-2 text-muted-foreground">
<Info className="w-3 h-3 shrink-0" /> <Info className="w-3 h-3 shrink-0" />
<span> <span>
{claudeAuthStatus.method === "oauth_token_env" {claudeAuthStatus.method === "oauth_token"
? "Using CLAUDE_CODE_OAUTH_TOKEN"
: claudeAuthStatus.method === "oauth_token"
? "Using stored OAuth token (subscription)" ? "Using stored OAuth token (subscription)"
: claudeAuthStatus.method === "api_key_env" : claudeAuthStatus.method === "api_key_env"
? "Using ANTHROPIC_API_KEY" ? "Using ANTHROPIC_API_KEY"
@@ -65,7 +65,9 @@ export function AuthenticationStatusDisplay({
? "Using credentials file" ? "Using credentials file"
: claudeAuthStatus.method === "cli_authenticated" : claudeAuthStatus.method === "cli_authenticated"
? "Using Claude CLI authentication" ? "Using Claude CLI authentication"
: `Using ${claudeAuthStatus.method || "detected"} authentication`} : `Using ${
claudeAuthStatus.method || "detected"
} authentication`}
</span> </span>
</div> </div>
</> </>
@@ -87,46 +89,6 @@ export function AuthenticationStatusDisplay({
)} )}
</div> </div>
</div> </div>
{/* Google/Gemini Authentication Status */}
<div className="p-3 rounded-lg bg-card border border-border">
<div className="flex items-center gap-2 mb-1.5">
<Sparkles className="w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-foreground">
Gemini (Google)
</span>
</div>
<div className="space-y-1.5 text-xs min-h-12">
{apiKeyStatus?.hasGoogleKey ? (
<>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
<span className="text-green-400 font-medium">Authenticated</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<Info className="w-3 h-3 shrink-0" />
<span>Using GOOGLE_API_KEY</span>
</div>
</>
) : apiKeys.google ? (
<>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
<span className="text-green-400 font-medium">Authenticated</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<Info className="w-3 h-3 shrink-0" />
<span>Using stored API key</span>
</div>
</>
) : (
<div className="flex items-center gap-1.5 text-yellow-500 py-0.5">
<AlertCircle className="w-3 h-3 shrink-0" />
<span className="text-xs">Not configured</span>
</div>
)}
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -104,20 +104,21 @@ export function FeatureDefaultsSection({
<div className="border-t border-border/30" /> <div className="border-t border-border/30" />
{/* Worktree Isolation Setting */} {/* Worktree Isolation Setting */}
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3"> <div className="group flex items-start space-x-3 p-3 rounded-xl transition-colors duration-200 -mx-3 opacity-60">
<Checkbox <Checkbox
id="use-worktrees" id="use-worktrees"
checked={useWorktrees} checked={useWorktrees}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
onUseWorktreesChange(checked === true) onUseWorktreesChange(checked === true)
} }
disabled={true}
className="mt-1" className="mt-1"
data-testid="use-worktrees-checkbox" data-testid="use-worktrees-checkbox"
/> />
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label <Label
htmlFor="use-worktrees" htmlFor="use-worktrees"
className="text-foreground cursor-pointer font-medium flex items-center gap-2" className="text-foreground font-medium flex items-center gap-2"
> >
<GitBranch className="w-4 h-4 text-brand-500" /> <GitBranch className="w-4 h-4 text-brand-500" />
Enable Git Worktree Isolation Enable Git Worktree Isolation
@@ -129,6 +130,9 @@ export function FeatureDefaultsSection({
Creates isolated git branches for each feature. When disabled, Creates isolated git branches for each feature. When disabled,
agents work directly in the main project directory. agents work directly in the main project directory.
</p> </p>
<p className="text-xs text-orange-500/80 leading-relaxed font-medium">
This feature is still under development and temporarily disabled.
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { CheckCircle2, XCircle, Loader2 } from "lucide-react"; import { CheckCircle2, XCircle, Loader2, AlertCircle } from "lucide-react";
interface StatusBadgeProps { interface StatusBadgeProps {
status: status:
@@ -6,7 +6,9 @@ interface StatusBadgeProps {
| "not_installed" | "not_installed"
| "checking" | "checking"
| "authenticated" | "authenticated"
| "not_authenticated"; | "not_authenticated"
| "error"
| "unverified";
label: string; label: string;
} }
@@ -25,11 +27,21 @@ export function StatusBadge({ status, label }: StatusBadgeProps) {
icon: <XCircle className="w-4 h-4" />, icon: <XCircle className="w-4 h-4" />,
className: "bg-red-500/10 text-red-500 border-red-500/20", className: "bg-red-500/10 text-red-500 border-red-500/20",
}; };
case "error":
return {
icon: <XCircle className="w-4 h-4" />,
className: "bg-red-500/10 text-red-500 border-red-500/20",
};
case "checking": case "checking":
return { return {
icon: <Loader2 className="w-4 h-4 animate-spin" />, icon: <Loader2 className="w-4 h-4 animate-spin" />,
className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
}; };
case "unverified":
return {
icon: <AlertCircle className="w-4 h-4" />,
className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
};
} }
}; };

View File

@@ -1,2 +1,2 @@
// Re-export all setup dialog components for easier imports // 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)

View File

@@ -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<HTMLDivElement>(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 (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent
className="max-w-2xl bg-card border-border"
data-testid="setup-token-modal"
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-foreground">
<Terminal className="w-5 h-5 text-brand-500" />
Claude Subscription Authentication
</DialogTitle>
<DialogDescription className="text-muted-foreground">
{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."}
</DialogDescription>
</DialogHeader>
{/* Terminal Output */}
<div
ref={scrollRef}
className="bg-zinc-900 rounded-lg p-4 font-mono text-sm max-h-48 overflow-y-auto border border-border mt-3"
>
{output.map((line, index) => (
<div key={index} className="text-zinc-300 whitespace-pre-wrap">
{line.startsWith("Error") || line.startsWith("⚠") ? (
<span className="text-yellow-400">{line}</span>
) : line.startsWith("✓") ? (
<span className="text-green-400">{line}</span>
) : (
line
)}
</div>
))}
{output.length === 0 && (
<div className="text-zinc-500 italic">Waiting to start...</div>
)}
{authState === "running" && (
<div className="flex items-center gap-2 text-brand-400 mt-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span>Waiting for authentication...</span>
</div>
)}
</div>
{/* Manual Token Input (for fallback) */}
{(authState === "manual" || authState === "error") && (
<div className="space-y-3 pt-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>Run this command in your terminal:</span>
<code className="bg-muted px-2 py-1 rounded font-mono text-foreground">
claude setup-token
</code>
<Button
variant="ghost"
size="icon"
onClick={copyCommand}
className="h-7 w-7"
>
<Copy className="w-4 h-4" />
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="manual-token" className="text-foreground">
Paste your token:
</Label>
<Input
id="manual-token"
type="password"
placeholder="Paste token here..."
value={manualToken}
onChange={(e) => setManualToken(e.target.value)}
className="bg-input border-border text-foreground"
data-testid="manual-token-input"
/>
</div>
</div>
)}
{/* Success State */}
{authState === "success" && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-6 h-6 text-green-500 shrink-0" />
<div>
<p className="font-medium text-foreground">
Token captured successfully!
</p>
<p className="text-sm text-muted-foreground">
Click &quot;Use Token&quot; to save and continue.
</p>
</div>
</div>
)}
{/* Error State */}
{error && authState === "error" && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-red-500/10 border border-red-500/20">
<XCircle className="w-6 h-6 text-red-500 shrink-0" />
<div>
<p className="font-medium text-foreground">Error</p>
<p className="text-sm text-muted-foreground">{error}</p>
</div>
</div>
)}
<DialogFooter className="mt-5 flex gap-2">
<Button
variant="outline"
onClick={onClose}
className="text-muted-foreground hover:text-foreground"
>
Cancel
</Button>
{authState === "idle" && (
<Button
onClick={startAuth}
className="bg-brand-500 hover:bg-brand-600 text-white"
data-testid="start-auth-button"
>
<Terminal className="w-4 h-4 mr-2" />
Start Authentication
</Button>
)}
{authState === "running" && (
<Button disabled className="bg-brand-500">
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Authenticating...
</Button>
)}
{authState === "success" && (
<Button
onClick={handleUseToken}
className="bg-green-500 hover:bg-green-600 text-white"
data-testid="use-token-button"
>
<CheckCircle2 className="w-4 h-4 mr-2" />
Use Token
</Button>
)}
{authState === "manual" && (
<Button
onClick={handleUseToken}
disabled={!manualToken.trim()}
className="bg-brand-500 hover:bg-brand-600 text-white disabled:opacity-50"
data-testid="use-manual-token-button"
>
<CheckCircle2 className="w-4 h-4 mr-2" />
Use Token
</Button>
)}
{authState === "error" && (
<>
{manualToken.trim() && (
<Button
onClick={handleUseToken}
className="bg-green-500 hover:bg-green-600 text-white"
>
Use Manual Token
</Button>
)}
<Button
onClick={handleRetry}
className="bg-brand-500 hover:bg-brand-600 text-white"
>
<RotateCcw className="w-4 h-4 mr-2" />
Retry
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,5 +1,4 @@
// Re-export all hooks for easier imports // Re-export all hooks for easier imports
export { useCliStatus } from "./use-cli-status"; export { useCliStatus } from "./use-cli-status";
export { useCliInstallation } from "./use-cli-installation"; export { useCliInstallation } from "./use-cli-installation";
export { useOAuthAuthentication } from "./use-oauth-authentication";
export { useTokenSave } from "./use-token-save"; export { useTokenSave } from "./use-token-save";

View File

@@ -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<AuthState>("idle");
const [output, setOutput] = useState<string[]>([]);
const [token, setToken] = useState("");
const [error, setError] = useState<string | null>(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 };
}

View File

@@ -1,11 +1,6 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { import { CheckCircle2, AlertCircle, Shield, Sparkles } from "lucide-react";
CheckCircle2,
AlertCircle,
Shield,
Sparkles,
} from "lucide-react";
import { useSetupStore } from "@/store/setup-store"; import { useSetupStore } from "@/store/setup-store";
import { useAppStore } from "@/store/app-store"; import { useAppStore } from "@/store/app-store";
@@ -14,8 +9,7 @@ interface CompleteStepProps {
} }
export function CompleteStep({ onFinish }: CompleteStepProps) { export function CompleteStep({ onFinish }: CompleteStepProps) {
const { claudeCliStatus, claudeAuthStatus } = const { claudeCliStatus, claudeAuthStatus } = useSetupStore();
useSetupStore();
const { apiKeys } = useAppStore(); const { apiKeys } = useAppStore();
const claudeReady = const claudeReady =
@@ -38,44 +32,6 @@ export function CompleteStep({ onFinish }: CompleteStepProps) {
</p> </p>
</div> </div>
<div className="max-w-md mx-auto">
<Card
className={`bg-card/50 border ${
claudeReady ? "border-green-500/50" : "border-yellow-500/50"
}`}
>
<CardContent className="py-4">
<div className="flex items-center gap-3">
{claudeReady ? (
<CheckCircle2 className="w-6 h-6 text-green-500" />
) : (
<AlertCircle className="w-6 h-6 text-yellow-500" />
)}
<div className="text-left">
<p className="font-medium text-foreground">Claude</p>
<p className="text-sm text-muted-foreground">
{claudeReady ? "Ready to use" : "Configure later in settings"}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="p-4 rounded-lg bg-muted/50 border border-border max-w-md mx-auto">
<div className="flex items-start gap-3">
<Shield className="w-5 h-5 text-brand-500 mt-0.5" />
<div className="text-left">
<p className="text-sm font-medium text-foreground">
Your credentials are secure
</p>
<p className="text-xs text-muted-foreground">
API keys are stored locally and never sent to our servers
</p>
</div>
</div>
</div>
<Button <Button
size="lg" size="lg"
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white" className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white"

View File

@@ -19,29 +19,11 @@ export function WelcomeStep({ onNext }: WelcomeStepProps) {
Welcome to Automaker Welcome to Automaker
</h2> </h2>
<p className="text-muted-foreground max-w-md mx-auto"> <p className="text-muted-foreground max-w-md mx-auto">
Let&apos;s set up your development environment. We&apos;ll check for To get started, we&apos;ll need to verify either claude code cli is
required CLI tools and help you configure them. installed or you have Anthropic api keys
</p> </p>
</div> </div>
<div className="grid grid-cols-1 gap-4 max-w-md mx-auto place-items-center">
<Card className="bg-card/50 border-border hover:border-brand-500/50 transition-colors">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Terminal className="w-5 h-5 text-brand-500" />
Claude CLI
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Anthropic&apos;s powerful AI assistant for code generation and
analysis
</p>
</CardContent>
</Card>
</div>
<Button <Button
size="lg" size="lg"
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white" className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white"

View File

@@ -60,7 +60,7 @@ export const buildProviderConfigs = ({
}: ProviderConfigParams): ProviderConfig[] => [ }: ProviderConfigParams): ProviderConfig[] => [
{ {
key: "anthropic", key: "anthropic",
label: "Anthropic API Key (Claude)", label: "Anthropic API Key",
inputId: "anthropic-key", inputId: "anthropic-key",
placeholder: "sk-ant-...", placeholder: "sk-ant-...",
value: anthropic.value, value: anthropic.value,
@@ -82,33 +82,32 @@ export const buildProviderConfigs = ({
descriptionPrefix: "Used for Claude AI features. Get your key at", descriptionPrefix: "Used for Claude AI features. Get your key at",
descriptionLinkHref: "https://console.anthropic.com/account/keys", descriptionLinkHref: "https://console.anthropic.com/account/keys",
descriptionLinkText: "console.anthropic.com", descriptionLinkText: "console.anthropic.com",
descriptionSuffix: descriptionSuffix: ".",
". Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.",
},
{
key: "google",
label: "Google API Key (Gemini)",
inputId: "google-key",
placeholder: "AIza...",
value: google.value,
setValue: google.setValue,
showValue: google.show,
setShowValue: google.setShow,
hasStoredKey: apiKeys.google,
inputTestId: "google-api-key-input",
toggleTestId: "toggle-google-visibility",
testButton: {
onClick: google.onTest,
disabled: !google.value || google.testing,
loading: google.testing,
testId: "test-gemini-connection",
},
result: google.result,
resultTestId: "gemini-test-connection-result",
resultMessageTestId: "gemini-test-connection-message",
descriptionPrefix:
"Used for Gemini AI features (including image/design prompts). Get your key at",
descriptionLinkHref: "https://makersuite.google.com/app/apikey",
descriptionLinkText: "makersuite.google.com",
}, },
// {
// key: "google",
// label: "Google API Key (Gemini)",
// inputId: "google-key",
// placeholder: "AIza...",
// value: google.value,
// setValue: google.setValue,
// showValue: google.show,
// setShowValue: google.setShow,
// hasStoredKey: apiKeys.google,
// inputTestId: "google-api-key-input",
// toggleTestId: "toggle-google-visibility",
// testButton: {
// onClick: google.onTest,
// disabled: !google.value || google.testing,
// loading: google.testing,
// testId: "test-gemini-connection",
// },
// result: google.result,
// resultTestId: "gemini-test-connection-result",
// resultMessageTestId: "gemini-test-connection-message",
// descriptionPrefix:
// "Used for Gemini AI features (including image/design prompts). Get your key at",
// descriptionLinkHref: "https://makersuite.google.com/app/apikey",
// descriptionLinkText: "makersuite.google.com",
// },
]; ];

View File

@@ -91,3 +91,4 @@ export const CHAT_TOOLS = [
* Default max turns for chat * Default max turns for chat
*/ */
export const CHAT_MAX_TURNS = 1000; export const CHAT_MAX_TURNS = 1000;

View File

@@ -355,6 +355,9 @@ export interface ElectronAPI {
provider: string, provider: string,
apiKey: string apiKey: string
) => Promise<{ success: boolean; error?: string }>; ) => Promise<{ success: boolean; error?: string }>;
deleteApiKey: (
provider: string
) => Promise<{ success: boolean; error?: string; message?: string }>;
getApiKeys: () => Promise<{ getApiKeys: () => Promise<{
success: boolean; success: boolean;
hasAnthropicKey: boolean; hasAnthropicKey: boolean;
@@ -369,6 +372,11 @@ export interface ElectronAPI {
isMac: boolean; isMac: boolean;
isLinux: boolean; isLinux: boolean;
}>; }>;
verifyClaudeAuth: (authMethod?: "cli" | "api_key") => Promise<{
success: boolean;
authenticated: boolean;
error?: string;
}>;
onInstallProgress?: (callback: (progress: any) => void) => () => void; onInstallProgress?: (callback: (progress: any) => void) => () => void;
onAuthProgress?: (callback: (progress: any) => void) => () => void; onAuthProgress?: (callback: (progress: any) => void) => () => void;
}; };
@@ -874,6 +882,9 @@ interface SetupAPI {
hasAnthropicKey: boolean; hasAnthropicKey: boolean;
hasGoogleKey: boolean; hasGoogleKey: boolean;
}>; }>;
deleteApiKey: (
provider: string
) => Promise<{ success: boolean; error?: string; message?: string }>;
getPlatform: () => Promise<{ getPlatform: () => Promise<{
success: boolean; success: boolean;
platform: string; platform: string;
@@ -883,6 +894,11 @@ interface SetupAPI {
isMac: boolean; isMac: boolean;
isLinux: boolean; isLinux: boolean;
}>; }>;
verifyClaudeAuth: (authMethod?: "cli" | "api_key") => Promise<{
success: boolean;
authenticated: boolean;
error?: string;
}>;
onInstallProgress?: (callback: (progress: any) => void) => () => void; onInstallProgress?: (callback: (progress: any) => void) => () => void;
onAuthProgress?: (callback: (progress: any) => void) => () => void; onAuthProgress?: (callback: (progress: any) => void) => () => void;
} }
@@ -942,6 +958,11 @@ function createMockSetupAPI(): SetupAPI {
}; };
}, },
deleteApiKey: async (provider: string) => {
console.log("[Mock] Deleting API key for:", provider);
return { success: true, message: `API key for ${provider} deleted` };
},
getPlatform: async () => { getPlatform: async () => {
return { return {
success: true, success: true,
@@ -954,6 +975,16 @@ function createMockSetupAPI(): SetupAPI {
}; };
}, },
verifyClaudeAuth: async (authMethod?: "cli" | "api_key") => {
console.log("[Mock] Verifying Claude auth with method:", authMethod);
// Mock always returns not authenticated
return {
success: true,
authenticated: false,
error: "Mock environment - authentication not available",
};
},
onInstallProgress: (callback) => { onInstallProgress: (callback) => {
// Mock progress events // Mock progress events
return () => {}; return () => {};

View File

@@ -438,6 +438,14 @@ export class HttpApiClient implements ElectronAPI {
error?: string; error?: string;
}> => this.post("/api/setup/store-api-key", { provider, apiKey }), }> => this.post("/api/setup/store-api-key", { provider, apiKey }),
deleteApiKey: (
provider: string
): Promise<{
success: boolean;
error?: string;
message?: string;
}> => this.post("/api/setup/delete-api-key", { provider }),
getApiKeys: (): Promise<{ getApiKeys: (): Promise<{
success: boolean; success: boolean;
hasAnthropicKey: boolean; hasAnthropicKey: boolean;
@@ -454,6 +462,12 @@ export class HttpApiClient implements ElectronAPI {
isLinux: boolean; isLinux: boolean;
}> => this.get("/api/setup/platform"), }> => this.get("/api/setup/platform"),
verifyClaudeAuth: (authMethod?: "cli" | "api_key"): Promise<{
success: boolean;
authenticated: boolean;
error?: string;
}> => this.post("/api/setup/verify-claude-auth", { authMethod }),
onInstallProgress: (callback: (progress: unknown) => void) => { onInstallProgress: (callback: (progress: unknown) => void) => {
return this.subscribeToEvent("agent:stream", callback); return this.subscribeToEvent("agent:stream", callback);
}, },

View File

@@ -12,7 +12,7 @@ export interface CliStatus {
// Claude Auth Method - all possible authentication sources // Claude Auth Method - all possible authentication sources
export type ClaudeAuthMethod = export type ClaudeAuthMethod =
| "oauth_token_env" // CLAUDE_CODE_OAUTH_TOKEN environment variable | "oauth_token_env"
| "oauth_token" // Stored OAuth token from claude login | "oauth_token" // Stored OAuth token from claude login
| "api_key_env" // ANTHROPIC_API_KEY environment variable | "api_key_env" // ANTHROPIC_API_KEY environment variable
| "api_key" // Manually stored API key | "api_key" // Manually stored API key
@@ -65,6 +65,7 @@ export interface SetupState {
export interface SetupActions { export interface SetupActions {
// Setup flow // Setup flow
setCurrentStep: (step: SetupStep) => void; setCurrentStep: (step: SetupStep) => void;
setSetupComplete: (complete: boolean) => void;
completeSetup: () => void; completeSetup: () => void;
resetSetup: () => void; resetSetup: () => void;
setIsFirstRun: (isFirstRun: boolean) => void; setIsFirstRun: (isFirstRun: boolean) => void;
@@ -109,6 +110,12 @@ export const useSetupStore = create<SetupState & SetupActions>()(
// Setup flow // Setup flow
setCurrentStep: (step) => set({ currentStep: step }), setCurrentStep: (step) => set({ currentStep: step }),
setSetupComplete: (complete) =>
set({
setupComplete: complete,
currentStep: complete ? "complete" : "welcome",
}),
completeSetup: () => completeSetup: () =>
set({ setupComplete: true, currentStep: "complete" }), set({ setupComplete: true, currentStep: "complete" }),

View File

@@ -50,30 +50,21 @@ const DATA_DIR = process.env.DATA_DIR || "./data";
const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== "false"; // Default to true const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== "false"; // Default to true
// Check for required environment variables // Check for required environment variables
// Claude Agent SDK supports EITHER OAuth token (subscription) OR API key (pay-per-use)
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
if (!hasAnthropicKey && !hasOAuthToken) { if (!hasAnthropicKey) {
console.warn(` console.warn(`
╔═══════════════════════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════════════════════╗
║ ⚠️ WARNING: No Claude authentication configured ║ ║ ⚠️ WARNING: No Claude authentication configured ║
║ ║ ║ ║
║ The Claude Agent SDK requires authentication to function. ║ ║ The Claude Agent SDK requires authentication to function. ║
║ ║ ║ ║
Option 1 - Subscription (OAuth Token): Set your Anthropic API key:
║ export CLAUDE_CODE_OAUTH_TOKEN="your-oauth-token" ║
║ ║
║ Option 2 - Pay-per-use (API Key): ║
║ export ANTHROPIC_API_KEY="sk-ant-..." ║ ║ export ANTHROPIC_API_KEY="sk-ant-..." ║
║ ║ ║ ║
║ Or use the setup wizard in Settings to configure authentication. ║ ║ Or use the setup wizard in Settings to configure authentication. ║
╚═══════════════════════════════════════════════════════════════════════╝ ╚═══════════════════════════════════════════════════════════════════════╝
`); `);
} else if (hasOAuthToken) {
console.log(
"[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)"
);
} else { } else {
console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)"); console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)");
} }

View File

@@ -72,3 +72,4 @@ export function getLogLevel(): LogLevel {
export function setLogLevel(level: LogLevel): void { export function setLogLevel(level: LogLevel): void {
currentLogLevel = level; currentLogLevel = level;
} }

View File

@@ -112,9 +112,7 @@ export class ClaudeProvider extends BaseProvider {
*/ */
async detectInstallation(): Promise<InstallationStatus> { async detectInstallation(): Promise<InstallationStatus> {
// Claude SDK is always available since it's a dependency // Claude SDK is always available since it's a dependency
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
const hasApiKey = hasAnthropicKey || hasOAuthToken;
const status: InstallationStatus = { const status: InstallationStatus = {
installed: true, installed: true,

View File

@@ -35,19 +35,9 @@ export function setRunningState(
* Helper to log authentication status * Helper to log authentication status
*/ */
export function logAuthStatus(context: string): void { export function logAuthStatus(context: string): void {
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
const hasApiKey = !!process.env.ANTHROPIC_API_KEY; const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
logger.info(`${context} - Auth Status:`); logger.info(`${context} - Auth Status:`);
logger.info(
` CLAUDE_CODE_OAUTH_TOKEN: ${
hasOAuthToken
? "SET (" +
process.env.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) +
"...)"
: "NOT SET"
}`
);
logger.info( logger.info(
` ANTHROPIC_API_KEY: ${ ` ANTHROPIC_API_KEY: ${
hasApiKey hasApiKey
@@ -56,7 +46,7 @@ export function logAuthStatus(context: string): void {
}` }`
); );
if (!hasOAuthToken && !hasApiKey) { if (!hasApiKey) {
logger.warn("⚠️ WARNING: No authentication configured! SDK will fail."); logger.warn("⚠️ WARNING: No authentication configured! SDK will fail.");
} }
} }

View File

@@ -21,3 +21,4 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
return router; return router;
} }

View File

@@ -15,9 +15,7 @@ export function createProvidersHandler() {
const providers: Record<string, any> = { const providers: Record<string, any> = {
anthropic: { anthropic: {
available: statuses.claude?.installed || false, available: statuses.claude?.installed || false,
hasApiKey: hasApiKey: !!process.env.ANTHROPIC_API_KEY,
!!process.env.ANTHROPIC_API_KEY ||
!!process.env.CLAUDE_CODE_OAUTH_TOKEN,
}, },
google: { google: {
available: !!process.env.GOOGLE_API_KEY, available: !!process.env.GOOGLE_API_KEY,

View File

@@ -74,7 +74,6 @@ export async function getClaudeStatus() {
hasStoredOAuthToken: !!getApiKey("anthropic_oauth_token"), hasStoredOAuthToken: !!getApiKey("anthropic_oauth_token"),
hasStoredApiKey: !!getApiKey("anthropic"), hasStoredApiKey: !!getApiKey("anthropic"),
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY, hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
hasEnvOAuthToken: !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
// Additional fields for detailed status // Additional fields for detailed status
oauthTokenValid: false, oauthTokenValid: false,
apiKeyValid: false, apiKeyValid: false,
@@ -148,11 +147,7 @@ export async function getClaudeStatus() {
} }
// Environment variables override stored credentials (higher priority) // Environment variables override stored credentials (higher priority)
if (auth.hasEnvOAuthToken) { if (auth.hasEnvApiKey) {
auth.authenticated = true;
auth.oauthTokenValid = true;
auth.method = "oauth_token_env"; // OAuth token from CLAUDE_CODE_OAUTH_TOKEN env var
} else if (auth.hasEnvApiKey) {
auth.authenticated = true; auth.authenticated = true;
auth.apiKeyValid = true; auth.apiKeyValid = true;
auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var

View File

@@ -7,8 +7,10 @@ import { createClaudeStatusHandler } from "./routes/claude-status.js";
import { createInstallClaudeHandler } from "./routes/install-claude.js"; import { createInstallClaudeHandler } from "./routes/install-claude.js";
import { createAuthClaudeHandler } from "./routes/auth-claude.js"; import { createAuthClaudeHandler } from "./routes/auth-claude.js";
import { createStoreApiKeyHandler } from "./routes/store-api-key.js"; import { createStoreApiKeyHandler } from "./routes/store-api-key.js";
import { createDeleteApiKeyHandler } from "./routes/delete-api-key.js";
import { createApiKeysHandler } from "./routes/api-keys.js"; import { createApiKeysHandler } from "./routes/api-keys.js";
import { createPlatformHandler } from "./routes/platform.js"; import { createPlatformHandler } from "./routes/platform.js";
import { createVerifyClaudeAuthHandler } from "./routes/verify-claude-auth.js";
export function createSetupRoutes(): Router { export function createSetupRoutes(): Router {
const router = Router(); const router = Router();
@@ -17,8 +19,10 @@ export function createSetupRoutes(): Router {
router.post("/install-claude", createInstallClaudeHandler()); router.post("/install-claude", createInstallClaudeHandler());
router.post("/auth-claude", createAuthClaudeHandler()); router.post("/auth-claude", createAuthClaudeHandler());
router.post("/store-api-key", createStoreApiKeyHandler()); router.post("/store-api-key", createStoreApiKeyHandler());
router.post("/delete-api-key", createDeleteApiKeyHandler());
router.get("/api-keys", createApiKeysHandler()); router.get("/api-keys", createApiKeysHandler());
router.get("/platform", createPlatformHandler()); router.get("/platform", createPlatformHandler());
router.post("/verify-claude-auth", createVerifyClaudeAuthHandler());
return router; return router;
} }

View File

@@ -0,0 +1,104 @@
/**
* POST /delete-api-key endpoint - Delete a stored API key
*/
import type { Request, Response } from "express";
import { createLogger } from "../../../lib/logger.js";
import path from "path";
import fs from "fs/promises";
const logger = createLogger("Setup");
// In-memory storage reference (imported from common.ts pattern)
// We need to modify common.ts to export a deleteApiKey function
import { setApiKey } from "../common.js";
/**
* Remove an API key from the .env file
*/
async function removeApiKeyFromEnv(key: string): Promise<void> {
const envPath = path.join(process.cwd(), ".env");
try {
let envContent = "";
try {
envContent = await fs.readFile(envPath, "utf-8");
} catch {
// .env file doesn't exist, nothing to delete
return;
}
// Parse existing env content and remove the key
const lines = envContent.split("\n");
const keyRegex = new RegExp(`^${key}=`);
const newLines = lines.filter((line) => !keyRegex.test(line));
// Remove empty lines at the end
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === "") {
newLines.pop();
}
await fs.writeFile(envPath, newLines.join("\n") + (newLines.length > 0 ? "\n" : ""));
logger.info(`[Setup] Removed ${key} from .env file`);
} catch (error) {
logger.error(`[Setup] Failed to remove ${key} from .env:`, error);
throw error;
}
}
export function createDeleteApiKeyHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { provider } = req.body as { provider: string };
if (!provider) {
res.status(400).json({
success: false,
error: "Provider is required",
});
return;
}
logger.info(`[Setup] Deleting API key for provider: ${provider}`);
// Map provider to env key name
const envKeyMap: Record<string, string> = {
anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_GENERATIVE_AI_API_KEY",
openai: "OPENAI_API_KEY",
};
const envKey = envKeyMap[provider];
if (!envKey) {
res.status(400).json({
success: false,
error: `Unknown provider: ${provider}`,
});
return;
}
// Clear from in-memory storage
setApiKey(provider, "");
// Remove from environment
delete process.env[envKey];
// Remove from .env file
await removeApiKeyFromEnv(envKey);
logger.info(`[Setup] Successfully deleted API key for ${provider}`);
res.json({
success: true,
message: `API key for ${provider} has been deleted`,
});
} catch (error) {
logger.error("[Setup] Delete API key error:", error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : "Failed to delete API key",
});
}
};
}

View File

@@ -31,16 +31,8 @@ export function createStoreApiKeyHandler() {
setApiKey(provider, apiKey); setApiKey(provider, apiKey);
// Also set as environment variable and persist to .env // Also set as environment variable and persist to .env
// IMPORTANT: OAuth tokens and API keys must be stored separately if (provider === "anthropic" || provider === "anthropic_oauth_token") {
// - OAuth tokens (subscription auth) -> CLAUDE_CODE_OAUTH_TOKEN // Both API key and OAuth token use ANTHROPIC_API_KEY
// - API keys (pay-per-use) -> ANTHROPIC_API_KEY
if (provider === "anthropic_oauth_token") {
// OAuth token from claude setup-token (subscription-based auth)
process.env.CLAUDE_CODE_OAUTH_TOKEN = apiKey;
await persistApiKeyToEnv("CLAUDE_CODE_OAUTH_TOKEN", apiKey);
logger.info("[Setup] Stored OAuth token as CLAUDE_CODE_OAUTH_TOKEN");
} else if (provider === "anthropic") {
// Direct API key (pay-per-use)
process.env.ANTHROPIC_API_KEY = apiKey; process.env.ANTHROPIC_API_KEY = apiKey;
await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey); await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey);
logger.info("[Setup] Stored API key as ANTHROPIC_API_KEY"); logger.info("[Setup] Stored API key as ANTHROPIC_API_KEY");

View File

@@ -0,0 +1,330 @@
/**
* POST /verify-claude-auth endpoint - Verify Claude authentication by running a test query
* Supports verifying either CLI auth or API key auth independently
*/
import type { Request, Response } from "express";
import { query } from "@anthropic-ai/claude-agent-sdk";
import { createLogger } from "../../../lib/logger.js";
import { getApiKey } from "../common.js";
const logger = createLogger("Setup");
// Known error patterns that indicate auth failure
const AUTH_ERROR_PATTERNS = [
"OAuth token revoked",
"Please run /login",
"please run /login",
"token revoked",
"invalid_api_key",
"authentication_error",
"unauthorized",
"not authenticated",
"authentication failed",
"invalid api key",
"api key is invalid",
];
// Patterns that indicate billing/credit issues - should FAIL verification
const BILLING_ERROR_PATTERNS = [
"credit balance is too low",
"credit balance too low",
"insufficient credits",
"insufficient balance",
"no credits",
"out of credits",
"billing",
"payment required",
"add credits",
];
// Patterns that indicate rate/usage limits - should FAIL verification
// Users need to wait or upgrade their plan
const RATE_LIMIT_PATTERNS = [
"limit reached",
"rate limit",
"rate_limit",
"resets", // Only valid if it's a temporary reset, not a billing issue
"/upgrade",
"extra-usage",
];
function isRateLimitError(text: string): boolean {
const lowerText = text.toLowerCase();
// First check if it's a billing error - billing errors are NOT rate limits
if (isBillingError(text)) {
return false;
}
return RATE_LIMIT_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
}
function isBillingError(text: string): boolean {
const lowerText = text.toLowerCase();
return BILLING_ERROR_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
}
function containsAuthError(text: string): boolean {
const lowerText = text.toLowerCase();
return AUTH_ERROR_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
}
export function createVerifyClaudeAuthHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
// Get the auth method from the request body
const { authMethod } = req.body as { authMethod?: "cli" | "api_key" };
logger.info(
`[Setup] Verifying Claude authentication using method: ${
authMethod || "auto"
}`
);
// Create an AbortController with a 30-second timeout
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), 30000);
let authenticated = false;
let errorMessage = "";
let receivedAnyContent = false;
// Save original env values
const originalAnthropicKey = process.env.ANTHROPIC_API_KEY;
try {
// Configure environment based on auth method
if (authMethod === "cli") {
// For CLI verification, remove any API key so it uses CLI credentials only
delete process.env.ANTHROPIC_API_KEY;
logger.info(
"[Setup] Cleared API key environment for CLI verification"
);
} else if (authMethod === "api_key") {
// For API key verification, ensure we're using the stored API key
const storedApiKey = getApiKey("anthropic");
if (storedApiKey) {
process.env.ANTHROPIC_API_KEY = storedApiKey;
logger.info("[Setup] Using stored API key for verification");
} else {
// Check env var
if (!process.env.ANTHROPIC_API_KEY) {
res.json({
success: true,
authenticated: false,
error: "No API key configured. Please enter an API key first.",
});
return;
}
}
}
// Run a minimal query to verify authentication
const stream = query({
prompt: "Reply with only the word 'ok'",
options: {
model: "claude-sonnet-4-20250514",
maxTurns: 1,
allowedTools: [],
abortController,
},
});
// Collect all messages and check for errors
const allMessages: string[] = [];
for await (const msg of stream) {
const msgStr = JSON.stringify(msg);
allMessages.push(msgStr);
logger.info("[Setup] Stream message:", msgStr.substring(0, 500));
// Check for billing errors FIRST - these should fail verification
if (isBillingError(msgStr)) {
logger.error("[Setup] Found billing error in message");
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
authenticated = false;
break;
}
// Check if any part of the message contains auth errors
if (containsAuthError(msgStr)) {
logger.error("[Setup] Found auth error in message");
if (authMethod === "cli") {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
}
break;
}
// Check specifically for assistant messages with text content
if (msg.type === "assistant" && (msg as any).message?.content) {
const content = (msg as any).message.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === "text" && block.text) {
const text = block.text;
logger.info("[Setup] Assistant text:", text);
if (containsAuthError(text)) {
if (authMethod === "cli") {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
}
break;
}
// Valid text response that's not an error
if (text.toLowerCase().includes("ok") || text.length > 0) {
receivedAnyContent = true;
}
}
}
}
}
// Check for result messages
if (msg.type === "result") {
const resultStr = JSON.stringify(msg);
// First check for billing errors - these should FAIL verification
if (isBillingError(resultStr)) {
logger.error(
"[Setup] Billing error detected - insufficient credits"
);
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
authenticated = false;
break;
}
// Check if it's a rate limit error - should FAIL verification
else if (isRateLimitError(resultStr)) {
logger.warn(
"[Setup] Rate limit detected - treating as unverified"
);
errorMessage =
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
authenticated = false;
break;
} else if (containsAuthError(resultStr)) {
if (authMethod === "cli") {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
}
} else {
// Got a result without errors
receivedAnyContent = true;
}
}
}
// Determine authentication status
if (errorMessage) {
authenticated = false;
} else if (receivedAnyContent) {
authenticated = true;
} else {
// No content received - might be an issue
logger.warn("[Setup] No content received from stream");
logger.warn("[Setup] All messages:", allMessages.join("\n"));
errorMessage =
"No response received from Claude. Please check your authentication.";
}
} catch (error: unknown) {
const errMessage =
error instanceof Error ? error.message : String(error);
logger.error("[Setup] Claude auth verification exception:", errMessage);
// Check for billing errors FIRST - these always fail
if (isBillingError(errMessage)) {
authenticated = false;
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
}
// Check for rate limit in exception - should FAIL verification
else if (isRateLimitError(errMessage)) {
authenticated = false;
errorMessage =
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
logger.warn(
"[Setup] Rate limit in exception - treating as unverified"
);
}
// If we already determined auth was successful, keep it
else if (authenticated) {
logger.info("[Setup] Auth already confirmed, ignoring exception");
}
// Check for auth-related errors in exception
else if (containsAuthError(errMessage)) {
if (authMethod === "cli") {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
}
} else if (
errMessage.includes("abort") ||
errMessage.includes("timeout")
) {
errorMessage = "Verification timed out. Please try again.";
} else if (
errMessage.includes("exit") &&
errMessage.includes("code 1")
) {
// Process exited with code 1 but we might have gotten rate limit info in the stream
// Check if we received any content that indicated auth worked
if (receivedAnyContent && !errorMessage) {
authenticated = true;
logger.info(
"[Setup] Process exit 1 but content received - auth valid"
);
} else if (!errorMessage) {
errorMessage = errMessage;
}
} else if (!errorMessage) {
errorMessage = errMessage;
}
} finally {
clearTimeout(timeoutId);
// Restore original environment
if (originalAnthropicKey !== undefined) {
process.env.ANTHROPIC_API_KEY = originalAnthropicKey;
} else if (authMethod === "cli") {
// If we cleared it and there was no original, keep it cleared
delete process.env.ANTHROPIC_API_KEY;
}
}
logger.info("[Setup] Verification result:", {
authenticated,
errorMessage,
authMethod,
});
res.json({
success: true,
authenticated,
error: errorMessage || undefined,
});
} catch (error) {
logger.error("[Setup] Verify Claude auth endpoint error:", error);
res.status(500).json({
success: false,
authenticated: false,
error: error instanceof Error ? error.message : "Verification failed",
});
}
};
}

View File

@@ -12,7 +12,6 @@ describe("claude-provider.ts", () => {
vi.clearAllMocks(); vi.clearAllMocks();
provider = new ClaudeProvider(); provider = new ClaudeProvider();
delete process.env.ANTHROPIC_API_KEY; delete process.env.ANTHROPIC_API_KEY;
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
}); });
describe("getName", () => { describe("getName", () => {
@@ -254,15 +253,6 @@ describe("claude-provider.ts", () => {
expect(result.authenticated).toBe(true); expect(result.authenticated).toBe(true);
}); });
it("should detect CLAUDE_CODE_OAUTH_TOKEN", async () => {
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-token";
const result = await provider.detectInstallation();
expect(result.hasApiKey).toBe(true);
expect(result.authenticated).toBe(true);
});
it("should return hasApiKey false when no keys present", async () => { it("should return hasApiKey false when no keys present", async () => {
const result = await provider.detectInstallation(); const result = await provider.detectInstallation();

View File

@@ -179,9 +179,8 @@ Routes models that:
#### Authentication #### Authentication
Requires one of: Requires:
- `ANTHROPIC_API_KEY` environment variable - `ANTHROPIC_API_KEY` environment variable
- `CLAUDE_CODE_OAUTH_TOKEN` environment variable
#### Example Usage #### Example Usage
@@ -704,9 +703,8 @@ describe("Provider Integration", () => {
### Claude Provider ### Claude Provider
```bash ```bash
# Required (one of): # Required:
ANTHROPIC_API_KEY=sk-ant-... ANTHROPIC_API_KEY=sk-ant-...
CLAUDE_CODE_OAUTH_TOKEN=...
``` ```
### Codex Provider ### Codex Provider

View File

@@ -580,3 +580,4 @@ The route organization pattern provides:
5. **Testability** - Functions can be tested independently 5. **Testability** - Functions can be tested independently
Apply this pattern to all route modules for consistency and improved code quality. Apply this pattern to all route modules for consistency and improved code quality.

22630
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,5 +29,8 @@
"test:headed": "npm run test:headed --workspace=apps/app", "test:headed": "npm run test:headed --workspace=apps/app",
"test:server": "npm run test --workspace=apps/server", "test:server": "npm run test --workspace=apps/server",
"test:server:coverage": "npm run test:cov --workspace=apps/server" "test:server:coverage": "npm run test:cov --workspace=apps/server"
},
"dependencies": {
"cross-spawn": "^7.0.6"
} }
} }