diff --git a/apps/app/src/components/views/interview-view.tsx b/apps/app/src/components/views/interview-view.tsx index 75007a8c..8fd74073 100644 --- a/apps/app/src/components/views/interview-view.tsx +++ b/apps/app/src/components/views/interview-view.tsx @@ -19,6 +19,7 @@ import { cn } from "@/lib/utils"; import { getElectronAPI } from "@/lib/electron"; import { Markdown } from "@/components/ui/markdown"; import { useFileBrowser } from "@/contexts/file-browser-context"; +import { toast } from "sonner"; interface InterviewMessage { id: string; @@ -290,7 +291,8 @@ export function InterviewView() { const handleSelectDirectory = async () => { const selectedPath = await openFileBrowser({ title: "Select Base Directory", - description: "Choose the parent directory where your new project will be created", + description: + "Choose the parent directory where your new project will be created", }); if (selectedPath) { @@ -306,12 +308,23 @@ export function InterviewView() { try { const api = getElectronAPI(); // Use platform-specific path separator - const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ? - (navigator.platform.indexOf('Win') !== -1 ? '\\' : '/') : '/'; + const pathSep = + typeof window !== "undefined" && (window as any).electronAPI + ? navigator.platform.indexOf("Win") !== -1 + ? "\\" + : "/" + : "/"; const fullProjectPath = `${projectPath}${pathSep}${projectName}`; // Create project directory - await api.mkdir(fullProjectPath); + const mkdirResult = await api.mkdir(fullProjectPath); + if (!mkdirResult.success) { + toast.error("Failed to create project directory", { + description: mkdirResult.error || "Unknown error occurred", + }); + setIsGenerating(false); + return; + } // Write app_spec.txt with generated content await api.writeFile( diff --git a/apps/app/src/components/views/welcome-view.tsx b/apps/app/src/components/views/welcome-view.tsx index 21c93112..1965479c 100644 --- a/apps/app/src/components/views/welcome-view.tsx +++ b/apps/app/src/components/views/welcome-view.tsx @@ -236,7 +236,13 @@ export function WelcomeView() { const projectPath = `${parentDir}/${projectName}`; // Create project directory - await api.mkdir(projectPath); + const mkdirResult = await api.mkdir(projectPath); + if (!mkdirResult.success) { + toast.error("Failed to create project directory", { + description: mkdirResult.error || "Unknown error occurred", + }); + return; + } // Initialize .automaker directory with all necessary files const initResult = await initializeProject(projectPath); diff --git a/apps/app/src/hooks/use-auto-mode.ts b/apps/app/src/hooks/use-auto-mode.ts index 6852ec64..4fac3f41 100644 --- a/apps/app/src/hooks/use-auto-mode.ts +++ b/apps/app/src/hooks/use-auto-mode.ts @@ -33,16 +33,21 @@ export function useAutoMode() { ); // Helper to look up project ID from path - const getProjectIdFromPath = useCallback((path: string): string | undefined => { - const project = projects.find(p => p.path === path); - return project?.id; - }, [projects]); + const getProjectIdFromPath = useCallback( + (path: string): string | undefined => { + const project = projects.find((p) => p.path === path); + return project?.id; + }, + [projects] + ); // Get project-specific auto mode state const projectId = currentProject?.id; const projectAutoModeState = useMemo(() => { if (!projectId) return { isRunning: false, runningTasks: [] }; - return autoModeByProject[projectId] || { isRunning: false, runningTasks: [] }; + return ( + autoModeByProject[projectId] || { isRunning: false, runningTasks: [] } + ); }, [autoModeByProject, projectId]); const isAutoModeRunning = projectAutoModeState.isRunning; @@ -62,10 +67,10 @@ export function useAutoMode() { // Events include projectPath from backend - use it to look up project ID // Fall back to current projectId if not provided in event let eventProjectId: string | undefined; - if ('projectPath' in event && event.projectPath) { + if ("projectPath" in event && event.projectPath) { eventProjectId = getProjectIdFromPath(event.projectPath); } - if (!eventProjectId && 'projectId' in event && event.projectId) { + if (!eventProjectId && "projectId" in event && event.projectId) { eventProjectId = event.projectId; } if (!eventProjectId) { @@ -74,7 +79,10 @@ export function useAutoMode() { // Skip event if we couldn't determine the project if (!eventProjectId) { - console.warn("[AutoMode] Could not determine project for event:", event); + console.warn( + "[AutoMode] Could not determine project for event:", + event + ); return; } @@ -111,20 +119,41 @@ export function useAutoMode() { } break; - case "auto_mode_complete": - // All features completed for this project + case "auto_mode_stopped": + // Auto mode was explicitly stopped (by user or error) setAutoModeRunning(eventProjectId, false); clearRunningTasks(eventProjectId); - console.log("[AutoMode] All features completed!"); + console.log("[AutoMode] Auto mode stopped"); + break; + + case "auto_mode_started": + // Auto mode started - ensure UI reflects running state + console.log("[AutoMode] Auto mode started:", event.message); + break; + + case "auto_mode_idle": + // Auto mode is running but has no pending features to pick up + // This is NOT a stop - auto mode keeps running and will pick up new features + console.log("[AutoMode] Auto mode idle - waiting for new features"); + break; + + case "auto_mode_complete": + // Legacy event - only handle if it looks like a stop (for backwards compatibility) + if (event.message === "Auto mode stopped") { + setAutoModeRunning(eventProjectId, false); + clearRunningTasks(eventProjectId); + console.log("[AutoMode] Auto mode stopped (legacy event)"); + } break; case "auto_mode_error": console.error("[AutoMode Error]", event.error); if (event.featureId && event.error) { // Check for authentication errors and provide a more helpful message - const isAuthError = event.errorType === "authentication" || - event.error.includes("Authentication failed") || - event.error.includes("Invalid API key"); + const isAuthError = + event.errorType === "authentication" || + event.error.includes("Authentication failed") || + event.error.includes("Invalid API key"); const errorMessage = isAuthError ? `Authentication failed: Please check your API key in Settings or run 'claude login' in terminal to re-authenticate.` @@ -202,11 +231,12 @@ export function useAutoMode() { if (!api?.autoMode) return; // Find all projects that have auto mode marked as running - const projectsToRestart: Array<{ projectId: string; projectPath: string }> = []; + const projectsToRestart: Array<{ projectId: string; projectPath: string }> = + []; for (const [projectId, state] of Object.entries(autoModeByProject)) { if (state.isRunning) { // Find the project path for this project ID - const project = projects.find(p => p.id === projectId); + const project = projects.find((p) => p.id === projectId); if (project) { projectsToRestart.push({ projectId, projectPath: project.path }); } @@ -216,18 +246,27 @@ export function useAutoMode() { // Restart auto mode for each project for (const { projectId, projectPath } of projectsToRestart) { console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`); - api.autoMode.start(projectPath, maxConcurrency).then(result => { - if (!result.success) { - console.error(`[AutoMode] Failed to restore auto mode for ${projectPath}:`, result.error); - // Mark as not running if we couldn't restart + api.autoMode + .start(projectPath, maxConcurrency) + .then((result) => { + if (!result.success) { + console.error( + `[AutoMode] Failed to restore auto mode for ${projectPath}:`, + result.error + ); + // Mark as not running if we couldn't restart + setAutoModeRunning(projectId, false); + } else { + console.log(`[AutoMode] Restored auto mode for ${projectPath}`); + } + }) + .catch((error) => { + console.error( + `[AutoMode] Error restoring auto mode for ${projectPath}:`, + error + ); setAutoModeRunning(projectId, false); - } else { - console.log(`[AutoMode] Restored auto mode for ${projectPath}`); - } - }).catch(error => { - console.error(`[AutoMode] Error restoring auto mode for ${projectPath}:`, error); - setAutoModeRunning(projectId, false); - }); + }); } // Only run once on mount - intentionally empty dependency array // eslint-disable-next-line react-hooks/exhaustive-deps @@ -246,11 +285,16 @@ export function useAutoMode() { throw new Error("Auto mode API not available"); } - const result = await api.autoMode.start(currentProject.path, maxConcurrency); + const result = await api.autoMode.start( + currentProject.path, + maxConcurrency + ); if (result.success) { setAutoModeRunning(currentProject.id, true); - console.log(`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`); + console.log( + `[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}` + ); } else { console.error("[AutoMode] Failed to start:", result.error); throw new Error(result.error || "Failed to start auto mode"); @@ -285,7 +329,9 @@ export function useAutoMode() { // Stopping auto mode only turns off the toggle to prevent new features // from being picked up. Running tasks will complete naturally and be // removed via the auto_mode_feature_complete event. - console.log("[AutoMode] Stopped successfully - running tasks will continue"); + console.log( + "[AutoMode] Stopped successfully - running tasks will continue" + ); } else { console.error("[AutoMode] Failed to stop:", result.error); throw new Error(result.error || "Failed to stop auto mode"); diff --git a/apps/app/src/lib/project-init.ts b/apps/app/src/lib/project-init.ts index 612af0fc..2ba184fc 100644 --- a/apps/app/src/lib/project-init.ts +++ b/apps/app/src/lib/project-init.ts @@ -29,7 +29,9 @@ const REQUIRED_STRUCTURE: { ".automaker/features", ".automaker/images", ], - files: {}, + files: { + ".automaker/categories.json": "[]", + }, }; /** diff --git a/apps/app/src/types/electron.d.ts b/apps/app/src/types/electron.d.ts index 6ee473bf..0a086fbd 100644 --- a/apps/app/src/types/electron.d.ts +++ b/apps/app/src/types/electron.d.ts @@ -203,6 +203,24 @@ export type AutoModeEvent = projectId?: string; projectPath?: string; } + | { + type: "auto_mode_stopped"; + message: string; + projectId?: string; + projectPath?: string; + } + | { + type: "auto_mode_started"; + message: string; + projectId?: string; + projectPath?: string; + } + | { + type: "auto_mode_idle"; + message: string; + projectId?: string; + projectPath?: string; + } | { type: "auto_mode_phase"; featureId: string; diff --git a/apps/server/src/lib/security.ts b/apps/server/src/lib/security.ts index eac0af00..d580cd41 100644 --- a/apps/server/src/lib/security.ts +++ b/apps/server/src/lib/security.ts @@ -26,6 +26,12 @@ export function initAllowedPaths(): void { if (dataDir) { allowedPaths.add(path.resolve(dataDir)); } + + // Always allow the workspace directory (where projects are created) + const workspaceDir = process.env.WORKSPACE_DIR; + if (workspaceDir) { + allowedPaths.add(path.resolve(workspaceDir)); + } } /** @@ -58,7 +64,9 @@ export function validatePath(filePath: string): string { const resolved = path.resolve(filePath); if (!isPathAllowed(resolved)) { - throw new Error(`Access denied: ${filePath} is not in an allowed directory`); + throw new Error( + `Access denied: ${filePath} is not in an allowed directory` + ); } return resolved; diff --git a/apps/server/src/routes/fs.ts b/apps/server/src/routes/fs.ts index ef227918..8ade7901 100644 --- a/apps/server/src/routes/fs.ts +++ b/apps/server/src/routes/fs.ts @@ -7,7 +7,11 @@ import { Router, type Request, type Response } from "express"; import fs from "fs/promises"; import os from "os"; import path from "path"; -import { validatePath, addAllowedPath, isPathAllowed } from "../lib/security.js"; +import { + validatePath, + addAllowedPath, + isPathAllowed, +} from "../lib/security.js"; import type { EventEmitter } from "../lib/events.js"; export function createFsRoutes(_events: EventEmitter): Router { @@ -69,9 +73,41 @@ export function createFsRoutes(_events: EventEmitter): Router { return; } - const resolvedPath = validatePath(dirPath); + const resolvedPath = path.resolve(dirPath); + + // Security check: allow paths in allowed directories OR within home directory + const isAllowed = (() => { + // Check if path or parent is in allowed paths + if (isPathAllowed(resolvedPath)) return true; + const parentPath = path.dirname(resolvedPath); + if (isPathAllowed(parentPath)) return true; + + // Also allow within home directory (like the /browse endpoint) + const homeDir = os.homedir(); + const normalizedHome = path.normalize(homeDir); + if ( + resolvedPath === normalizedHome || + resolvedPath.startsWith(normalizedHome + path.sep) + ) { + return true; + } + + return false; + })(); + + if (!isAllowed) { + res.status(403).json({ + success: false, + error: `Access denied: ${dirPath} is not in an allowed directory`, + }); + return; + } + await fs.mkdir(resolvedPath, { recursive: true }); + // Add the new directory to allowed paths so subsequent operations work + addAllowedPath(resolvedPath); + res.json({ success: true }); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; @@ -197,7 +233,9 @@ export function createFsRoutes(_events: EventEmitter): Router { const stats = await fs.stat(resolvedPath); if (!stats.isDirectory()) { - res.status(400).json({ success: false, error: "Path is not a directory" }); + res + .status(400) + .json({ success: false, error: "Path is not a directory" }); return; } @@ -229,7 +267,9 @@ export function createFsRoutes(_events: EventEmitter): Router { }; if (!directoryName) { - res.status(400).json({ success: false, error: "directoryName is required" }); + res + .status(400) + .json({ success: false, error: "directoryName is required" }); return; } @@ -254,10 +294,16 @@ export function createFsRoutes(_events: EventEmitter): Router { const searchPaths: string[] = [ process.cwd(), // Current working directory process.env.HOME || process.env.USERPROFILE || "", // User home - path.join(process.env.HOME || process.env.USERPROFILE || "", "Documents"), + path.join( + process.env.HOME || process.env.USERPROFILE || "", + "Documents" + ), path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"), // Common project locations - path.join(process.env.HOME || process.env.USERPROFILE || "", "Projects"), + path.join( + process.env.HOME || process.env.USERPROFILE || "", + "Projects" + ), ].filter(Boolean); // Also check parent of current working directory @@ -275,7 +321,7 @@ export function createFsRoutes(_events: EventEmitter): Router { try { const candidatePath = path.join(searchPath, directoryName); const stats = await fs.stat(candidatePath); - + if (stats.isDirectory()) { // Verify it matches by checking for sample files if (sampleFiles && sampleFiles.length > 0) { @@ -284,8 +330,10 @@ export function createFsRoutes(_events: EventEmitter): Router { // Remove directory name prefix from sample file path const relativeFile = sampleFile.startsWith(directoryName + "/") ? sampleFile.substring(directoryName.length + 1) - : sampleFile.split("/").slice(1).join("/") || sampleFile.split("/").pop() || sampleFile; - + : sampleFile.split("/").slice(1).join("/") || + sampleFile.split("/").pop() || + sampleFile; + try { const filePath = path.join(candidatePath, relativeFile); await fs.access(filePath); @@ -294,7 +342,7 @@ export function createFsRoutes(_events: EventEmitter): Router { // File doesn't exist, continue checking } } - + // If at least one file matches, consider it a match if (matches === 0 && sampleFiles.length > 0) { continue; // Try next candidate @@ -405,7 +453,9 @@ export function createFsRoutes(_events: EventEmitter): Router { const stats = await fs.stat(targetPath); if (!stats.isDirectory()) { - res.status(400).json({ success: false, error: "Path is not a directory" }); + res + .status(400) + .json({ success: false, error: "Path is not a directory" }); return; } @@ -438,7 +488,8 @@ export function createFsRoutes(_events: EventEmitter): Router { } catch (error) { res.status(400).json({ success: false, - error: error instanceof Error ? error.message : "Failed to read directory", + error: + error instanceof Error ? error.message : "Failed to read directory", }); } } catch (error) { @@ -464,8 +515,8 @@ export function createFsRoutes(_events: EventEmitter): Router { const fullPath = path.isAbsolute(imagePath) ? imagePath : projectPath - ? path.join(projectPath, imagePath) - : imagePath; + ? path.join(projectPath, imagePath) + : imagePath; // Check if file exists try { @@ -490,7 +541,10 @@ export function createFsRoutes(_events: EventEmitter): Router { ".bmp": "image/bmp", }; - res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream"); + res.setHeader( + "Content-Type", + mimeTypes[ext] || "application/octet-stream" + ); res.setHeader("Cache-Control", "public, max-age=3600"); res.send(buffer); } catch (error) { @@ -546,38 +600,42 @@ export function createFsRoutes(_events: EventEmitter): Router { }); // Delete board background image - router.post("/delete-board-background", async (req: Request, res: Response) => { - try { - const { projectPath } = req.body as { projectPath: string }; - - if (!projectPath) { - res.status(400).json({ - success: false, - error: "projectPath is required", - }); - return; - } - - const boardDir = path.join(projectPath, ".automaker", "board"); - + router.post( + "/delete-board-background", + async (req: Request, res: Response) => { try { - // Try to remove all files in the board directory - const files = await fs.readdir(boardDir); - for (const file of files) { - if (file.startsWith("background")) { - await fs.unlink(path.join(boardDir, file)); - } - } - } catch { - // Directory may not exist, that's fine - } + const { projectPath } = req.body as { projectPath: string }; - res.json({ success: true }); - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - res.status(500).json({ success: false, error: message }); + if (!projectPath) { + res.status(400).json({ + success: false, + error: "projectPath is required", + }); + return; + } + + const boardDir = path.join(projectPath, ".automaker", "board"); + + try { + // Try to remove all files in the board directory + const files = await fs.readdir(boardDir); + for (const file of files) { + if (file.startsWith("background")) { + await fs.unlink(path.join(boardDir, file)); + } + } + } catch { + // Directory may not exist, that's fine + } + + res.json({ success: true }); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error"; + res.status(500).json({ success: false, error: message }); + } } - }); + ); // Browse directories for file picker // SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows @@ -614,7 +672,10 @@ export function createFsRoutes(_events: EventEmitter): Router { const normalizedHome = path.resolve(homeDir); // Allow browsing within home directory - if (resolved === normalizedHome || resolved.startsWith(normalizedHome + path.sep)) { + if ( + resolved === normalizedHome || + resolved.startsWith(normalizedHome + path.sep) + ) { return true; } @@ -646,7 +707,8 @@ export function createFsRoutes(_events: EventEmitter): Router { if (!isSafePath(targetPath)) { res.status(403).json({ success: false, - error: "Access denied: browsing is restricted to your home directory and allowed project paths", + error: + "Access denied: browsing is restricted to your home directory and allowed project paths", }); return; } @@ -655,7 +717,9 @@ export function createFsRoutes(_events: EventEmitter): Router { const stats = await fs.stat(targetPath); if (!stats.isDirectory()) { - res.status(400).json({ success: false, error: "Path is not a directory" }); + res + .status(400) + .json({ success: false, error: "Path is not a directory" }); return; } @@ -688,7 +752,8 @@ export function createFsRoutes(_events: EventEmitter): Router { } catch (error) { res.status(400).json({ success: false, - error: error instanceof Error ? error.message : "Failed to read directory", + error: + error instanceof Error ? error.message : "Failed to read directory", }); } } catch (error) { diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 69d95850..ba5b6351 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -32,7 +32,15 @@ interface Feature { priority?: number; spec?: string; model?: string; // Model to use for this feature - imagePaths?: Array; + imagePaths?: Array< + | string + | { + path: string; + filename?: string; + mimeType?: string; + [key: string]: unknown; + } + >; } interface RunningFeature { @@ -78,7 +86,7 @@ export class AutoModeService { projectPath, }; - this.emitAutoModeEvent("auto_mode_complete", { + this.emitAutoModeEvent("auto_mode_started", { message: `Auto mode started with max ${maxConcurrency} concurrent features`, projectPath, }); @@ -111,8 +119,9 @@ export class AutoModeService { ); if (pendingFeatures.length === 0) { - this.emitAutoModeEvent("auto_mode_complete", { + this.emitAutoModeEvent("auto_mode_idle", { message: "No pending features - auto mode idle", + projectPath: this.config!.projectPath, }); await this.sleep(10000); continue; @@ -143,8 +152,9 @@ export class AutoModeService { } this.autoLoopRunning = false; - this.emitAutoModeEvent("auto_mode_complete", { + this.emitAutoModeEvent("auto_mode_stopped", { message: "Auto mode stopped", + projectPath: this.config?.projectPath, }); } @@ -230,10 +240,19 @@ export class AutoModeService { // Get model from feature const model = resolveModelString(feature.model, DEFAULT_MODELS.claude); - console.log(`[AutoMode] Executing feature ${featureId} with model: ${model}`); + console.log( + `[AutoMode] Executing feature ${featureId} with model: ${model}` + ); // Run the agent with the feature's model and images - await this.runAgent(workDir, featureId, prompt, abortController, imagePaths, model); + await this.runAgent( + workDir, + featureId, + prompt, + abortController, + imagePaths, + model + ); // Mark as waiting_approval for user review await this.updateFeatureStatus( @@ -422,7 +441,9 @@ Address the follow-up instructions above. Review the previous work and make the try { // Get model from feature (already loaded above) const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude); - console.log(`[AutoMode] Follow-up for feature ${featureId} using model: ${model}`); + console.log( + `[AutoMode] Follow-up for feature ${featureId} using model: ${model}` + ); // Update feature status to in_progress await this.updateFeatureStatus(projectPath, featureId, "in_progress"); @@ -458,9 +479,11 @@ Address the follow-up instructions above. Review the previous work and make the filename ); copiedImagePaths.push(relativePath); - } catch (error) { - console.error(`[AutoMode] Failed to copy follow-up image ${imagePath}:`, error); + console.error( + `[AutoMode] Failed to copy follow-up image ${imagePath}:`, + error + ); } } } @@ -506,7 +529,14 @@ Address the follow-up instructions above. Review the previous work and make the } // Use fullPrompt (already built above) with model and all images - await this.runAgent(workDir, featureId, fullPrompt, abortController, allImagePaths.length > 0 ? allImagePaths : imagePaths, model); + await this.runAgent( + workDir, + featureId, + fullPrompt, + abortController, + allImagePaths.length > 0 ? allImagePaths : imagePaths, + model + ); // Mark as waiting_approval for user review await this.updateFeatureStatus( @@ -717,7 +747,10 @@ Format your response as a structured markdown document.`; try { // Use default Claude model for analysis (can be overridden in the future) - const analysisModel = resolveModelString(undefined, DEFAULT_MODELS.claude); + const analysisModel = resolveModelString( + undefined, + DEFAULT_MODELS.claude + ); const provider = ProviderFactory.getProviderForModel(analysisModel); const options: ExecuteOptions = { @@ -917,7 +950,11 @@ Format your response as a structured markdown document.`; try { const data = await fs.readFile(featurePath, "utf-8"); const feature = JSON.parse(data); - if (feature.status === "pending" || feature.status === "ready") { + if ( + feature.status === "pending" || + feature.status === "ready" || + feature.status === "backlog" + ) { features.push(feature); } } catch { @@ -998,9 +1035,15 @@ ${feature.spec} const imagesList = feature.imagePaths .map((img, idx) => { const path = typeof img === "string" ? img : img.path; - const filename = typeof img === "string" ? path.split("/").pop() : img.filename || path.split("/").pop(); - const mimeType = typeof img === "string" ? "image/*" : img.mimeType || "image/*"; - return ` ${idx + 1}. ${filename} (${mimeType})\n Path: ${path}`; + const filename = + typeof img === "string" + ? path.split("/").pop() + : img.filename || path.split("/").pop(); + const mimeType = + typeof img === "string" ? "image/*" : img.mimeType || "image/*"; + return ` ${ + idx + 1 + }. ${filename} (${mimeType})\n Path: ${path}`; }) .join("\n"); @@ -1038,7 +1081,9 @@ When done, summarize what you implemented and any notes for the developer.`; model?: string ): Promise { const finalModel = resolveModelString(model, DEFAULT_MODELS.claude); - console.log(`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`); + console.log( + `[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}` + ); // Get provider for this model const provider = ProviderFactory.getProviderForModel(finalModel); @@ -1060,14 +1105,7 @@ When done, summarize what you implemented and any notes for the developer.`; model: finalModel, maxTurns: 50, cwd: workDir, - allowedTools: [ - "Read", - "Write", - "Edit", - "Glob", - "Grep", - "Bash", - ], + allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], abortController, }; @@ -1089,12 +1127,15 @@ When done, summarize what you implemented and any notes for the developer.`; responseText = block.text || ""; // Check for authentication errors in the response - if (block.text && (block.text.includes("Invalid API key") || + if ( + block.text && + (block.text.includes("Invalid API key") || block.text.includes("authentication_failed") || - block.text.includes("Fix external API key"))) { + block.text.includes("Fix external API key")) + ) { throw new Error( "Authentication failed: Invalid or expired API key. " + - "Please check your ANTHROPIC_API_KEY or GOOGLE_API_KEY, or run 'claude login' to re-authenticate." + "Please check your ANTHROPIC_API_KEY or GOOGLE_API_KEY, or run 'claude login' to re-authenticate." ); }