From 0dc2e7226361a5d95a17683fd16f7a66a9aaf4d3 Mon Sep 17 00:00:00 2001 From: SuperComboGamer Date: Fri, 12 Dec 2025 18:57:41 -0500 Subject: [PATCH] chore: remove ~13,000 lines of dead Electron code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All business logic has been migrated to the REST API server. This removes obsolete Electron IPC handlers and services that are no longer used: - Deleted electron/services/ directory (18 files) - Deleted electron/agent-service.js - Deleted electron/auto-mode-service.js - Renamed main-simplified.js → main.js - Renamed preload-simplified.js → preload.js - Removed unused dependencies from package.json The Electron layer now only handles native OS features (10 IPC handlers): - File dialogs, shell operations, and app info šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/app/electron/agent-service.js | 682 --------- apps/app/electron/auto-mode-service.js | 1353 ----------------- apps/app/electron/main-simplified.js | 241 --- apps/app/electron/main.js | 190 ++- apps/app/electron/preload-simplified.js | 37 - apps/app/electron/preload.js | 41 +- .../electron/services/claude-cli-detector.js | 721 --------- .../electron/services/codex-cli-detector.js | 566 ------- .../electron/services/codex-config-manager.js | 353 ----- apps/app/electron/services/codex-executor.js | 610 -------- apps/app/electron/services/context-manager.js | 452 ------ .../app/electron/services/feature-executor.js | 1269 ---------------- apps/app/electron/services/feature-loader.js | 500 ------ .../services/feature-suggestions-service.js | 379 ----- .../app/electron/services/feature-verifier.js | 185 --- .../electron/services/mcp-server-factory.js | 109 -- .../app/electron/services/mcp-server-stdio.js | 358 ----- apps/app/electron/services/model-provider.js | 524 ------- apps/app/electron/services/model-registry.js | 320 ---- .../app/electron/services/project-analyzer.js | 112 -- apps/app/electron/services/prompt-builder.js | 787 ---------- apps/app/electron/services/pty-runner.js | 84 - .../services/spec-regeneration-service.js | 1075 ------------- .../app/electron/services/worktree-manager.js | 569 ------- apps/app/package.json | 5 +- plan.md | 5 +- 26 files changed, 218 insertions(+), 11309 deletions(-) delete mode 100644 apps/app/electron/agent-service.js delete mode 100644 apps/app/electron/auto-mode-service.js delete mode 100644 apps/app/electron/main-simplified.js delete mode 100644 apps/app/electron/preload-simplified.js delete mode 100644 apps/app/electron/services/claude-cli-detector.js delete mode 100644 apps/app/electron/services/codex-cli-detector.js delete mode 100644 apps/app/electron/services/codex-config-manager.js delete mode 100644 apps/app/electron/services/codex-executor.js delete mode 100644 apps/app/electron/services/context-manager.js delete mode 100644 apps/app/electron/services/feature-executor.js delete mode 100644 apps/app/electron/services/feature-loader.js delete mode 100644 apps/app/electron/services/feature-suggestions-service.js delete mode 100644 apps/app/electron/services/feature-verifier.js delete mode 100644 apps/app/electron/services/mcp-server-factory.js delete mode 100644 apps/app/electron/services/mcp-server-stdio.js delete mode 100644 apps/app/electron/services/model-provider.js delete mode 100644 apps/app/electron/services/model-registry.js delete mode 100644 apps/app/electron/services/project-analyzer.js delete mode 100644 apps/app/electron/services/prompt-builder.js delete mode 100644 apps/app/electron/services/pty-runner.js delete mode 100644 apps/app/electron/services/spec-regeneration-service.js delete mode 100644 apps/app/electron/services/worktree-manager.js diff --git a/apps/app/electron/agent-service.js b/apps/app/electron/agent-service.js deleted file mode 100644 index d8dc0247..00000000 --- a/apps/app/electron/agent-service.js +++ /dev/null @@ -1,682 +0,0 @@ -const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); -const path = require("path"); -const fs = require("fs/promises"); - -/** - * Agent Service - Runs Claude agents in the Electron main process - * This service survives Next.js restarts and maintains conversation state - */ -class AgentService { - constructor() { - this.sessions = new Map(); // sessionId -> { messages, isRunning, abortController } - this.stateDir = null; // Will be set when app is ready - } - - /** - * Initialize the service with app data directory - */ - async initialize(appDataPath) { - this.stateDir = path.join(appDataPath, "agent-sessions"); - this.metadataFile = path.join(appDataPath, "sessions-metadata.json"); - await fs.mkdir(this.stateDir, { recursive: true }); - } - - /** - * Start or resume a conversation - */ - async startConversation({ sessionId, workingDirectory }) { - - // Initialize session if it doesn't exist - if (!this.sessions.has(sessionId)) { - const messages = await this.loadSession(sessionId); - - this.sessions.set(sessionId, { - messages, - isRunning: false, - abortController: null, - workingDirectory: workingDirectory || process.cwd(), - }); - } - - const session = this.sessions.get(sessionId); - return { - success: true, - messages: session.messages, - sessionId, - }; - } - - /** - * Send a message to the agent and stream responses - */ - async sendMessage({ - sessionId, - message, - workingDirectory, - imagePaths, - sendToRenderer, - }) { - const session = this.sessions.get(sessionId); - if (!session) { - throw new Error(`Session ${sessionId} not found`); - } - - if (session.isRunning) { - throw new Error("Agent is already processing a message"); - } - - // Read images from temp files and convert to base64 for storage - const images = []; - if (imagePaths && imagePaths.length > 0) { - const fs = require("fs/promises"); - const path = require("path"); - - for (const imagePath of imagePaths) { - try { - const imageBuffer = await fs.readFile(imagePath); - const base64Data = imageBuffer.toString("base64"); - - // Determine media type from file extension - const ext = path.extname(imagePath).toLowerCase(); - const mimeTypeMap = { - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".png": "image/png", - ".gif": "image/gif", - ".webp": "image/webp", - }; - const mediaType = mimeTypeMap[ext] || "image/png"; - - images.push({ - data: base64Data, - mimeType: mediaType, - filename: path.basename(imagePath), - }); - - console.log( - `[AgentService] Loaded image from ${imagePath} for storage` - ); - } catch (error) { - console.error( - `[AgentService] Failed to load image from ${imagePath}:`, - error - ); - } - } - } - - // Add user message to conversation with base64 images - const userMessage = { - id: this.generateId(), - role: "user", - content: message, - images: images.length > 0 ? images : undefined, - timestamp: new Date().toISOString(), - }; - - session.messages.push(userMessage); - session.isRunning = true; - session.abortController = new AbortController(); - - // Send initial user message to renderer - sendToRenderer({ - type: "message", - message: userMessage, - }); - - // Save state with base64 images - await this.saveSession(sessionId, session.messages); - - try { - // Configure Claude Agent SDK options - const options = { - // model: "claude-sonnet-4-20250514", - model: "claude-opus-4-5-20251101", - systemPrompt: this.getSystemPrompt(), - maxTurns: 20, - cwd: workingDirectory || session.workingDirectory, - allowedTools: [ - "Read", - "Write", - "Edit", - "Glob", - "Grep", - "Bash", - "WebSearch", - "WebFetch", - ], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: session.abortController, - }; - - // Build prompt content with text and images - let promptContent = message; - - // If there are images, create a content array - if (imagePaths && imagePaths.length > 0) { - const contentBlocks = []; - - // Add text block - if (message && message.trim()) { - contentBlocks.push({ - type: "text", - text: message, - }); - } - - // Add image blocks - const fs = require("fs"); - for (const imagePath of imagePaths) { - try { - const imageBuffer = fs.readFileSync(imagePath); - const base64Data = imageBuffer.toString("base64"); - const ext = path.extname(imagePath).toLowerCase(); - const mimeTypeMap = { - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".png": "image/png", - ".gif": "image/gif", - ".webp": "image/webp", - }; - const mediaType = mimeTypeMap[ext] || "image/png"; - - contentBlocks.push({ - type: "image", - source: { - type: "base64", - media_type: mediaType, - data: base64Data, - }, - }); - } catch (error) { - console.error( - `[AgentService] Failed to load image ${imagePath}:`, - error - ); - } - } - - // Use content blocks if we have images - if ( - contentBlocks.length > 1 || - (contentBlocks.length === 1 && contentBlocks[0].type === "image") - ) { - promptContent = contentBlocks; - } - } - - // Build payload for the SDK - const promptPayload = Array.isArray(promptContent) - ? (async function* () { - yield { - type: "user", - session_id: "", - message: { - role: "user", - content: promptContent, - }, - parent_tool_use_id: null, - }; - })() - : promptContent; - - // Send the query via the SDK (conversation state handled by the SDK) - const stream = query({ prompt: promptPayload, options }); - - let currentAssistantMessage = null; - let responseText = ""; - const toolUses = []; - - // Stream responses from the SDK - for await (const msg of stream) { - if (msg.type === "assistant") { - if (msg.message.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - - // Create or update assistant message - if (!currentAssistantMessage) { - currentAssistantMessage = { - id: this.generateId(), - role: "assistant", - content: responseText, - timestamp: new Date().toISOString(), - }; - session.messages.push(currentAssistantMessage); - } else { - currentAssistantMessage.content = responseText; - } - - // Stream to renderer - sendToRenderer({ - type: "stream", - messageId: currentAssistantMessage.id, - content: responseText, - isComplete: false, - }); - } else if (block.type === "tool_use") { - const toolUse = { - name: block.name, - input: block.input, - }; - toolUses.push(toolUse); - - // Send tool use notification - sendToRenderer({ - type: "tool_use", - tool: toolUse, - }); - } - } - } - } else if (msg.type === "result") { - if (msg.subtype === "success" && msg.result) { - // Use the final result - if (currentAssistantMessage) { - currentAssistantMessage.content = msg.result; - responseText = msg.result; - } - } - - // Send completion - sendToRenderer({ - type: "complete", - messageId: currentAssistantMessage?.id, - content: responseText, - toolUses, - }); - } - } - - // Save final state - await this.saveSession(sessionId, session.messages); - - session.isRunning = false; - session.abortController = null; - - return { - success: true, - message: currentAssistantMessage, - }; - } catch (error) { - if (error instanceof AbortError || error?.name === "AbortError") { - // Query aborted - session.isRunning = false; - session.abortController = null; - return { success: false, aborted: true }; - } - - console.error("[AgentService] Error:", error); - - session.isRunning = false; - session.abortController = null; - - // Add error message - const errorMessage = { - id: this.generateId(), - role: "assistant", - content: `Error: ${error.message}`, - timestamp: new Date().toISOString(), - isError: true, - }; - - session.messages.push(errorMessage); - await this.saveSession(sessionId, session.messages); - - sendToRenderer({ - type: "error", - error: error.message, - message: errorMessage, - }); - - throw error; - } - } - - /** - * Get conversation history - */ - getHistory(sessionId) { - const session = this.sessions.get(sessionId); - if (!session) { - return { success: false, error: "Session not found" }; - } - - return { - success: true, - messages: session.messages, - isRunning: session.isRunning, - }; - } - - /** - * Stop current agent execution - */ - async stopExecution(sessionId) { - const session = this.sessions.get(sessionId); - if (!session) { - return { success: false, error: "Session not found" }; - } - - if (session.abortController) { - session.abortController.abort(); - session.isRunning = false; - session.abortController = null; - } - - return { success: true }; - } - - /** - * Clear conversation history - */ - async clearSession(sessionId) { - const session = this.sessions.get(sessionId); - if (session) { - session.messages = []; - session.isRunning = false; - await this.saveSession(sessionId, []); - } - - return { success: true }; - } - - /** - * Load session from disk - */ - async loadSession(sessionId) { - if (!this.stateDir) return []; - - const sessionFile = path.join(this.stateDir, `${sessionId}.json`); - - try { - const data = await fs.readFile(sessionFile, "utf-8"); - const parsed = JSON.parse(data); - console.log( - `[AgentService] Loaded ${parsed.length} messages for ${sessionId}` - ); - return parsed; - } catch (error) { - // Session doesn't exist yet - return []; - } - } - - /** - * Save session to disk - */ - async saveSession(sessionId, messages) { - if (!this.stateDir) return; - - const sessionFile = path.join(this.stateDir, `${sessionId}.json`); - - try { - await fs.writeFile( - sessionFile, - JSON.stringify(messages, null, 2), - "utf-8" - ); - console.log( - `[AgentService] Saved ${messages.length} messages for ${sessionId}` - ); - - // Update timestamp - await this.updateSessionTimestamp(sessionId); - } catch (error) { - console.error("[AgentService] Failed to save session:", error); - } - } - - /** - * Get system prompt - */ - getSystemPrompt() { - return `You are an AI assistant helping users build software. You are part of the Automaker application, -which is designed to help developers plan, design, and implement software projects autonomously. - -**Feature Storage:** -Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. -Use the UpdateFeatureStatus tool to manage features, not direct file edits. - -Your role is to: -- Help users define their project requirements and specifications -- Ask clarifying questions to better understand their needs -- Suggest technical approaches and architectures -- Guide them through the development process -- Be conversational and helpful -- Write, edit, and modify code files as requested -- Execute commands and tests -- Search and analyze the codebase - -When discussing projects, help users think through: -- Core functionality and features -- Technical stack choices -- Data models and architecture -- User experience considerations -- Testing strategies - -You have full access to the codebase and can: -- Read files to understand existing code -- Write new files -- Edit existing files -- Run bash commands -- Search for code patterns -- Execute tests and builds - -IMPORTANT: When making file changes, be aware that the Next.js development server may restart. -This is normal and expected. Your conversation state is preserved across these restarts.`; - } - - /** - * Generate unique ID - */ - generateId() { - return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; - } - - // ============================================================================ - // Session Management - // ============================================================================ - - /** - * Load all session metadata - */ - async loadMetadata() { - if (!this.metadataFile) return {}; - - try { - const data = await fs.readFile(this.metadataFile, "utf-8"); - return JSON.parse(data); - } catch (error) { - return {}; - } - } - - /** - * Save session metadata - */ - async saveMetadata(metadata) { - if (!this.metadataFile) return; - - try { - await fs.writeFile( - this.metadataFile, - JSON.stringify(metadata, null, 2), - "utf-8" - ); - } catch (error) { - console.error("[AgentService] Failed to save metadata:", error); - } - } - - /** - * List all sessions - */ - async listSessions({ includeArchived = false } = {}) { - const metadata = await this.loadMetadata(); - const sessions = []; - - for (const [sessionId, meta] of Object.entries(metadata)) { - if (!includeArchived && meta.isArchived) continue; - - const messages = await this.loadSession(sessionId); - const lastMessage = messages[messages.length - 1]; - - sessions.push({ - id: sessionId, - name: meta.name || sessionId, - projectPath: meta.projectPath || "", - createdAt: meta.createdAt, - updatedAt: meta.updatedAt, - messageCount: messages.length, - isArchived: meta.isArchived || false, - tags: meta.tags || [], - preview: lastMessage?.content.substring(0, 100) || "", - }); - } - - // Sort by most recently updated - sessions.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); - - return sessions; - } - - /** - * Create a new session - */ - async createSession({ name, projectPath, workingDirectory }) { - const sessionId = `session_${Date.now()}_${Math.random() - .toString(36) - .substring(2, 11)}`; - - const metadata = await this.loadMetadata(); - metadata[sessionId] = { - name, - projectPath, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - isArchived: false, - tags: [], - }; - - await this.saveMetadata(metadata); - - this.sessions.set(sessionId, { - messages: [], - isRunning: false, - abortController: null, - workingDirectory: workingDirectory || projectPath, - }); - - await this.saveSession(sessionId, []); - - return { - success: true, - sessionId, - session: metadata[sessionId], - }; - } - - /** - * Update session metadata - */ - async updateSession({ sessionId, name, tags }) { - const metadata = await this.loadMetadata(); - - if (!metadata[sessionId]) { - return { success: false, error: "Session not found" }; - } - - if (name !== undefined) metadata[sessionId].name = name; - if (tags !== undefined) metadata[sessionId].tags = tags; - metadata[sessionId].updatedAt = new Date().toISOString(); - - await this.saveMetadata(metadata); - - return { success: true }; - } - - /** - * Archive a session - */ - async archiveSession(sessionId) { - const metadata = await this.loadMetadata(); - - if (!metadata[sessionId]) { - return { success: false, error: "Session not found" }; - } - - metadata[sessionId].isArchived = true; - metadata[sessionId].updatedAt = new Date().toISOString(); - - await this.saveMetadata(metadata); - - return { success: true }; - } - - /** - * Unarchive a session - */ - async unarchiveSession(sessionId) { - const metadata = await this.loadMetadata(); - - if (!metadata[sessionId]) { - return { success: false, error: "Session not found" }; - } - - metadata[sessionId].isArchived = false; - metadata[sessionId].updatedAt = new Date().toISOString(); - - await this.saveMetadata(metadata); - - return { success: true }; - } - - /** - * Delete a session permanently - */ - async deleteSession(sessionId) { - const metadata = await this.loadMetadata(); - - if (!metadata[sessionId]) { - return { success: false, error: "Session not found" }; - } - - // Remove from metadata - delete metadata[sessionId]; - await this.saveMetadata(metadata); - - // Remove from memory - this.sessions.delete(sessionId); - - // Delete session file - const sessionFile = path.join(this.stateDir, `${sessionId}.json`); - try { - await fs.unlink(sessionFile); - } catch (error) { - console.warn("[AgentService] Failed to delete session file:", error); - } - - return { success: true }; - } - - /** - * Update session metadata when messages change - */ - async updateSessionTimestamp(sessionId) { - const metadata = await this.loadMetadata(); - - if (metadata[sessionId]) { - metadata[sessionId].updatedAt = new Date().toISOString(); - await this.saveMetadata(metadata); - } - } -} - -// Export singleton instance -module.exports = new AgentService(); diff --git a/apps/app/electron/auto-mode-service.js b/apps/app/electron/auto-mode-service.js deleted file mode 100644 index cedd6fd6..00000000 --- a/apps/app/electron/auto-mode-service.js +++ /dev/null @@ -1,1353 +0,0 @@ -const featureLoader = require("./services/feature-loader"); -const featureExecutor = require("./services/feature-executor"); -const featureVerifier = require("./services/feature-verifier"); -const contextManager = require("./services/context-manager"); -const projectAnalyzer = require("./services/project-analyzer"); -const worktreeManager = require("./services/worktree-manager"); - -/** - * Auto Mode Service - Autonomous feature implementation - * Automatically picks and implements features from the kanban board - * - * This service acts as the main orchestrator, delegating work to specialized services: - * - featureLoader: Loading and selecting features - * - featureExecutor: Implementing features - * - featureVerifier: Running tests and verification - * - contextManager: Managing context files - * - projectAnalyzer: Analyzing project structure - */ -class AutoModeService { - constructor() { - // Track multiple concurrent feature executions - this.runningFeatures = new Map(); // featureId -> { abortController, query, projectPath, sendToRenderer } - - // Per-project auto loop state (keyed by projectPath) - this.projectLoops = new Map(); // projectPath -> { isRunning, interval, abortController, sendToRenderer, maxConcurrency } - - this.checkIntervalMs = 5000; // Check every 5 seconds - this.maxConcurrency = 3; // Default max concurrency (global default) - } - - /** - * Get or create project loop state - */ - getProjectLoopState(projectPath) { - if (!this.projectLoops.has(projectPath)) { - this.projectLoops.set(projectPath, { - isRunning: false, - interval: null, - abortController: null, - sendToRenderer: null, - maxConcurrency: this.maxConcurrency, - }); - } - return this.projectLoops.get(projectPath); - } - - /** - * Check if any project has auto mode running - */ - hasAnyAutoLoopRunning() { - for (const [, state] of this.projectLoops) { - if (state.isRunning) return true; - } - return false; - } - - /** - * Get running features for a specific project - */ - getRunningFeaturesForProject(projectPath) { - const features = []; - for (const [featureId, execution] of this.runningFeatures) { - if (execution.projectPath === projectPath) { - features.push(featureId); - } - } - return features; - } - - /** - * Count running features for a specific project - */ - getRunningCountForProject(projectPath) { - let count = 0; - for (const [, execution] of this.runningFeatures) { - if (execution.projectPath === projectPath) { - count++; - } - } - return count; - } - - /** - * Helper to create execution context with isActive check - */ - createExecutionContext(featureId) { - const context = { - abortController: null, - query: null, - projectPath: null, // Original project path - worktreePath: null, // Path to worktree (where agent works) - branchName: null, // Feature branch name - sendToRenderer: null, - isActive: () => this.runningFeatures.has(featureId), - }; - return context; - } - - /** - * Helper to emit event with projectPath included - */ - emitEvent(projectPath, sendToRenderer, event) { - if (sendToRenderer) { - sendToRenderer({ - ...event, - projectPath, - }); - } - } - - /** - * Setup worktree for a feature - * Creates an isolated git worktree where the agent can work - * @param {Object} feature - The feature object - * @param {string} projectPath - Path to the project - * @param {Function} sendToRenderer - Function to send events to the renderer - * @param {boolean} useWorktreesEnabled - Whether worktrees are enabled in settings (default: false) - */ - async setupWorktreeForFeature(feature, projectPath, sendToRenderer, useWorktreesEnabled = false) { - // If worktrees are disabled in settings, skip entirely - if (!useWorktreesEnabled) { - console.log(`[AutoMode] Worktrees disabled in settings, working directly on main project`); - return { useWorktree: false, workPath: projectPath }; - } - - // Check if worktrees are enabled (project must be a git repo) - const isGit = await worktreeManager.isGitRepo(projectPath); - if (!isGit) { - console.log(`[AutoMode] Project is not a git repo, skipping worktree creation`); - return { useWorktree: false, workPath: projectPath }; - } - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_progress", - featureId: feature.id, - content: "Creating isolated worktree for feature...\n", - }); - - const result = await worktreeManager.createWorktree(projectPath, feature); - - if (!result.success) { - console.warn(`[AutoMode] Failed to create worktree: ${result.error}. Falling back to main project.`); - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_progress", - featureId: feature.id, - content: `Warning: Could not create worktree (${result.error}). Working directly on main project.\n`, - }); - return { useWorktree: false, workPath: projectPath }; - } - - console.log(`[AutoMode] Created worktree at: ${result.worktreePath}, branch: ${result.branchName}`); - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_progress", - featureId: feature.id, - content: `Working in isolated branch: ${result.branchName}\n`, - }); - - // Update feature with worktree info - await featureLoader.updateFeatureWorktree( - feature.id, - projectPath, - result.worktreePath, - result.branchName - ); - - return { - useWorktree: true, - workPath: result.worktreePath, - branchName: result.branchName, - baseBranch: result.baseBranch, - }; - } - - /** - * Start auto mode for a specific project - continuously implement features - * Each project can have its own independent auto mode loop - */ - async start({ projectPath, sendToRenderer, maxConcurrency }) { - const projectState = this.getProjectLoopState(projectPath); - - if (projectState.isRunning) { - throw new Error(`Auto mode loop is already running for project: ${projectPath}`); - } - - projectState.isRunning = true; - projectState.maxConcurrency = maxConcurrency || 3; - projectState.sendToRenderer = sendToRenderer; - - console.log( - `[AutoMode] Starting auto mode for project: ${projectPath} with max concurrency: ${projectState.maxConcurrency}` - ); - - // Start the periodic checking loop for this project - this.runPeriodicLoopForProject(projectPath); - - return { success: true }; - } - - /** - * Stop auto mode for a specific project - stops the auto loop but lets running features complete - * This only turns off the auto toggle to prevent picking up new features. - * Running tasks will continue until they complete naturally. - */ - async stop({ projectPath }) { - console.log(`[AutoMode] Stopping auto mode for project: ${projectPath} (letting running features complete)`); - - const projectState = this.projectLoops.get(projectPath); - if (!projectState) { - console.log(`[AutoMode] No auto mode state found for project: ${projectPath}`); - return { success: true, runningFeatures: 0 }; - } - - projectState.isRunning = false; - - // Clear the interval timer for this project - if (projectState.interval) { - clearInterval(projectState.interval); - projectState.interval = null; - } - - // Abort auto loop if running - if (projectState.abortController) { - projectState.abortController.abort(); - projectState.abortController = null; - } - - // NOTE: We intentionally do NOT abort running features here. - // Stopping auto mode should only turn off the toggle to prevent new features - // from being picked up. Running features will complete naturally. - // Use stopFeature() to cancel a specific running feature if needed. - - const runningCount = this.getRunningCountForProject(projectPath); - console.log(`[AutoMode] Auto loop stopped for ${projectPath}. ${runningCount} feature(s) still running and will complete.`); - - return { success: true, runningFeatures: runningCount }; - } - - /** - * Get status of auto mode (global and per-project) - */ - getStatus({ projectPath } = {}) { - // If projectPath is specified, return status for that project - if (projectPath) { - const projectState = this.projectLoops.get(projectPath); - return { - autoLoopRunning: projectState?.isRunning || false, - runningFeatures: this.getRunningFeaturesForProject(projectPath), - runningCount: this.getRunningCountForProject(projectPath), - }; - } - - // Otherwise return global status - const allRunningProjects = []; - for (const [path, state] of this.projectLoops) { - if (state.isRunning) { - allRunningProjects.push(path); - } - } - - return { - autoLoopRunning: this.hasAnyAutoLoopRunning(), - runningProjects: allRunningProjects, - runningFeatures: Array.from(this.runningFeatures.keys()), - runningCount: this.runningFeatures.size, - }; - } - - /** - * Get status for all projects with auto mode - */ - getAllProjectStatuses() { - const statuses = {}; - for (const [projectPath, state] of this.projectLoops) { - statuses[projectPath] = { - isRunning: state.isRunning, - runningFeatures: this.getRunningFeaturesForProject(projectPath), - runningCount: this.getRunningCountForProject(projectPath), - maxConcurrency: state.maxConcurrency, - }; - } - return statuses; - } - - /** - * Run a specific feature by ID - * @param {string} projectPath - Path to the project - * @param {string} featureId - ID of the feature to run - * @param {Function} sendToRenderer - Function to send events to renderer - * @param {boolean} useWorktrees - Whether to use git worktree isolation (default: false) - */ - async runFeature({ projectPath, featureId, sendToRenderer, useWorktrees = false }) { - // Check if this specific feature is already running - if (this.runningFeatures.has(featureId)) { - throw new Error(`Feature ${featureId} is already running`); - } - - console.log(`[AutoMode] Running specific feature: ${featureId} (worktrees: ${useWorktrees})`); - - // Register this feature as running - const execution = this.createExecutionContext(featureId); - execution.projectPath = projectPath; - execution.sendToRenderer = sendToRenderer; - this.runningFeatures.set(featureId, execution); - - try { - // Load features - const features = await featureLoader.loadFeatures(projectPath); - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - throw new Error(`Feature ${featureId} not found`); - } - - console.log(`[AutoMode] Running feature: ${feature.description}`); - - // Setup worktree for isolated work (if enabled) - const worktreeSetup = await this.setupWorktreeForFeature(feature, projectPath, sendToRenderer, useWorktrees); - execution.worktreePath = worktreeSetup.workPath; - execution.branchName = worktreeSetup.branchName; - - // Determine working path (worktree or main project) - const workPath = worktreeSetup.workPath; - - // Update feature status to in_progress - await featureLoader.updateFeatureStatus( - featureId, - "in_progress", - projectPath - ); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_start", - featureId: feature.id, - feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName }, - }); - - // Implement the feature (agent works in worktree) - const result = await featureExecutor.implementFeature( - feature, - workPath, // Use worktree path instead of main project - sendToRenderer, - execution - ); - - // Update feature status based on result - // For skipTests features, go to waiting_approval on success instead of verified - // On failure, ALL features go to waiting_approval so user can review and decide next steps - // This prevents infinite retry loops when the same issue keeps failing - let newStatus; - if (result.passes) { - newStatus = feature.skipTests ? "waiting_approval" : "verified"; - } else { - // On failure, go to waiting_approval for user review - // Don't automatically move back to backlog to avoid infinite retry loops - // (especially when hitting rate limits or persistent errors) - newStatus = "waiting_approval"; - } - await featureLoader.updateFeatureStatus( - feature.id, - newStatus, - projectPath - ); - - // Keep context file for viewing output later (deleted only when card is removed) - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_complete", - featureId: feature.id, - passes: result.passes, - message: result.message, - }); - - return { success: true, passes: result.passes }; - } catch (error) { - console.error("[AutoMode] Error running feature:", error); - - // Write error to context file - try { - await contextManager.writeToContextFile( - projectPath, - featureId, - `\n\nāŒ ERROR: ${error.message}\n\n${error.stack || ''}\n` - ); - } catch (contextError) { - console.error("[AutoMode] Failed to write error to context:", contextError); - } - - // Update feature status to waiting_approval so user can review the error - try { - await featureLoader.updateFeatureStatus( - featureId, - "waiting_approval", - projectPath, - { error: error.message } - ); - } catch (statusError) { - console.error("[AutoMode] Failed to update feature status after error:", statusError); - } - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - throw error; - } finally { - // Clean up this feature's execution - this.runningFeatures.delete(featureId); - } - } - - /** - * Verify a specific feature by running its tests - */ - async verifyFeature({ projectPath, featureId, sendToRenderer }) { - console.log(`[AutoMode] verifyFeature called with:`, { - projectPath, - featureId, - }); - - // Check if this specific feature is already running - if (this.runningFeatures.has(featureId)) { - throw new Error(`Feature ${featureId} is already running`); - } - - console.log(`[AutoMode] Verifying feature: ${featureId}`); - - // Register this feature as running - const execution = this.createExecutionContext(featureId); - execution.projectPath = projectPath; - execution.sendToRenderer = sendToRenderer; - this.runningFeatures.set(featureId, execution); - - try { - // Load features - const features = await featureLoader.loadFeatures(projectPath); - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - throw new Error(`Feature ${featureId} not found`); - } - - console.log(`[AutoMode] Verifying feature: ${feature.description}`); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_start", - featureId: feature.id, - feature: feature, - }); - - // Verify the feature by running tests - const result = await featureVerifier.verifyFeatureTests( - feature, - projectPath, - sendToRenderer, - execution - ); - - // Update feature status based on result - const newStatus = result.passes ? "verified" : "in_progress"; - await featureLoader.updateFeatureStatus( - featureId, - newStatus, - projectPath - ); - - // Keep context file for viewing output later (deleted only when card is removed) - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_complete", - featureId: feature.id, - passes: result.passes, - message: result.message, - }); - - return { success: true, passes: result.passes }; - } catch (error) { - console.error("[AutoMode] Error verifying feature:", error); - - // Write error to context file - try { - await contextManager.writeToContextFile( - projectPath, - featureId, - `\n\nāŒ ERROR: ${error.message}\n\n${error.stack || ''}\n` - ); - } catch (contextError) { - console.error("[AutoMode] Failed to write error to context:", contextError); - } - - // Update feature status to waiting_approval so user can review the error - try { - await featureLoader.updateFeatureStatus( - featureId, - "waiting_approval", - projectPath, - { error: error.message } - ); - } catch (statusError) { - console.error("[AutoMode] Failed to update feature status after error:", statusError); - } - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - throw error; - } finally { - // Clean up this feature's execution - this.runningFeatures.delete(featureId); - } - } - - /** - * Resume a feature that has previous context - loads existing context and continues implementation - */ - async resumeFeature({ projectPath, featureId, sendToRenderer }) { - console.log(`[AutoMode] resumeFeature called with:`, { - projectPath, - featureId, - }); - - // Check if this specific feature is already running - if (this.runningFeatures.has(featureId)) { - throw new Error(`Feature ${featureId} is already running`); - } - - console.log(`[AutoMode] Resuming feature: ${featureId}`); - - // Register this feature as running - const execution = this.createExecutionContext(featureId); - execution.projectPath = projectPath; - execution.sendToRenderer = sendToRenderer; - this.runningFeatures.set(featureId, execution); - - try { - // Load features - const features = await featureLoader.loadFeatures(projectPath); - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - throw new Error(`Feature ${featureId} not found`); - } - - console.log(`[AutoMode] Resuming feature: ${feature.description}`); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_start", - featureId: feature.id, - feature: feature, - }); - - // Read existing context - const previousContext = await contextManager.readContextFile( - projectPath, - featureId - ); - - // Resume implementation with context - const result = await featureExecutor.resumeFeatureWithContext( - feature, - projectPath, - sendToRenderer, - previousContext, - execution - ); - - // If the agent ends early without finishing, automatically re-run - let attempts = 0; - const maxAttempts = 3; - let finalResult = result; - - while (!finalResult.passes && attempts < maxAttempts) { - // Check if feature is still in progress (not verified) - const updatedFeatures = await featureLoader.loadFeatures(projectPath); - const updatedFeature = updatedFeatures.find((f) => f.id === featureId); - - if (updatedFeature && updatedFeature.status === "in_progress") { - attempts++; - console.log( - `[AutoMode] Feature ended early, auto-retrying (attempt ${attempts}/${maxAttempts})...` - ); - - // Update context file with retry message - await contextManager.writeToContextFile( - projectPath, - featureId, - `\n\nšŸ”„ Auto-retry #${attempts} - Continuing implementation...\n\n` - ); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_progress", - featureId: feature.id, - content: `\nšŸ”„ Auto-retry #${attempts} - Agent ended early, continuing...\n`, - }); - - // Read updated context - const retryContext = await contextManager.readContextFile( - projectPath, - featureId - ); - - // Resume again with full context - finalResult = await featureExecutor.resumeFeatureWithContext( - feature, - projectPath, - sendToRenderer, - retryContext, - execution - ); - } else { - break; - } - } - - // Update feature status based on final result - // For skipTests features, go to waiting_approval on success instead of verified - // On failure, go to waiting_approval so user can review and decide next steps - let newStatus; - if (finalResult.passes) { - newStatus = feature.skipTests ? "waiting_approval" : "verified"; - } else { - // On failure after all retry attempts, go to waiting_approval for user review - newStatus = "waiting_approval"; - } - await featureLoader.updateFeatureStatus( - featureId, - newStatus, - projectPath - ); - - // Keep context file for viewing output later (deleted only when card is removed) - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_complete", - featureId: feature.id, - passes: finalResult.passes, - message: finalResult.message, - }); - - return { success: true, passes: finalResult.passes }; - } catch (error) { - console.error("[AutoMode] Error resuming feature:", error); - - // Write error to context file - try { - await contextManager.writeToContextFile( - projectPath, - featureId, - `\n\nāŒ ERROR: ${error.message}\n\n${error.stack || ''}\n` - ); - } catch (contextError) { - console.error("[AutoMode] Failed to write error to context:", contextError); - } - - // Update feature status to waiting_approval so user can review the error - try { - await featureLoader.updateFeatureStatus( - featureId, - "waiting_approval", - projectPath, - { error: error.message } - ); - } catch (statusError) { - console.error("[AutoMode] Failed to update feature status after error:", statusError); - } - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - throw error; - } finally { - // Clean up this feature's execution - this.runningFeatures.delete(featureId); - } - } - - /** - * New periodic loop for a specific project - checks available slots and starts features up to max concurrency - * This loop continues running even if there are no backlog items - */ - runPeriodicLoopForProject(projectPath) { - const projectState = this.getProjectLoopState(projectPath); - - console.log( - `[AutoMode] Starting periodic loop for ${projectPath} with interval: ${this.checkIntervalMs}ms` - ); - - // Initial check immediately - this.checkAndStartFeaturesForProject(projectPath); - - // Then check periodically - projectState.interval = setInterval(() => { - if (projectState.isRunning) { - this.checkAndStartFeaturesForProject(projectPath); - } - }, this.checkIntervalMs); - } - - /** - * Check how many features are running for a specific project and start new ones if under max concurrency - */ - async checkAndStartFeaturesForProject(projectPath) { - const projectState = this.projectLoops.get(projectPath); - if (!projectState || !projectState.isRunning) { - return; - } - - const sendToRenderer = projectState.sendToRenderer; - const maxConcurrency = projectState.maxConcurrency; - - try { - // Check how many are currently running FOR THIS PROJECT - const currentRunningCount = this.getRunningCountForProject(projectPath); - - console.log( - `[AutoMode] [${projectPath}] Checking features - Running: ${currentRunningCount}/${maxConcurrency}` - ); - - // Calculate available slots for this project - const availableSlots = maxConcurrency - currentRunningCount; - - if (availableSlots <= 0) { - console.log(`[AutoMode] [${projectPath}] At max concurrency, waiting...`); - return; - } - - // Load features from backlog - const features = await featureLoader.loadFeatures(projectPath); - const backlogFeatures = features.filter((f) => f.status === "backlog"); - - if (backlogFeatures.length === 0) { - console.log(`[AutoMode] [${projectPath}] No backlog features available, waiting...`); - return; - } - - // Grab up to availableSlots features from backlog - const featuresToStart = backlogFeatures.slice(0, availableSlots); - - console.log( - `[AutoMode] [${projectPath}] Starting ${featuresToStart.length} feature(s) from backlog` - ); - - // Start each feature (don't await - run in parallel like drag operations) - for (const feature of featuresToStart) { - this.startFeatureAsync(feature, projectPath, sendToRenderer); - } - } catch (error) { - console.error(`[AutoMode] [${projectPath}] Error checking/starting features:`, error); - } - } - - /** - * Start a feature asynchronously (similar to drag operation) - * @param {Object} feature - The feature to start - * @param {string} projectPath - Path to the project - * @param {Function} sendToRenderer - Function to send events to renderer - * @param {boolean} useWorktrees - Whether to use git worktree isolation (default: false) - */ - async startFeatureAsync(feature, projectPath, sendToRenderer, useWorktrees = false) { - const featureId = feature.id; - - // Skip if already running - if (this.runningFeatures.has(featureId)) { - console.log(`[AutoMode] Feature ${featureId} already running, skipping`); - return; - } - - try { - console.log( - `[AutoMode] Starting feature: ${feature.description.slice(0, 50)}... (worktrees: ${useWorktrees})` - ); - - // Register this feature as running - const execution = this.createExecutionContext(featureId); - execution.projectPath = projectPath; - execution.sendToRenderer = sendToRenderer; - this.runningFeatures.set(featureId, execution); - - // Setup worktree for isolated work (if enabled) - const worktreeSetup = await this.setupWorktreeForFeature(feature, projectPath, sendToRenderer, useWorktrees); - execution.worktreePath = worktreeSetup.workPath; - execution.branchName = worktreeSetup.branchName; - - // Determine working path (worktree or main project) - const workPath = worktreeSetup.workPath; - - // Update status to in_progress with timestamp - await featureLoader.updateFeatureStatus( - featureId, - "in_progress", - projectPath - ); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_start", - featureId: feature.id, - feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName }, - }); - - // Implement the feature (agent works in worktree) - const result = await featureExecutor.implementFeature( - feature, - workPath, // Use worktree path instead of main project - sendToRenderer, - execution - ); - - // Update feature status based on result - // For skipTests features, go to waiting_approval on success instead of verified - // On failure, ALL features go to waiting_approval so user can review and decide next steps - // This prevents infinite retry loops when the same issue keeps failing - let newStatus; - if (result.passes) { - newStatus = feature.skipTests ? "waiting_approval" : "verified"; - } else { - // On failure, go to waiting_approval for user review - // Don't automatically move back to backlog to avoid infinite retry loops - // (especially when hitting rate limits or persistent errors) - newStatus = "waiting_approval"; - } - await featureLoader.updateFeatureStatus( - feature.id, - newStatus, - projectPath - ); - - // Keep context file for viewing output later (deleted only when card is removed) - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_complete", - featureId: feature.id, - passes: result.passes, - message: result.message, - }); - } catch (error) { - console.error(`[AutoMode] Error running feature ${featureId}:`, error); - - // Write error to context file - try { - await contextManager.writeToContextFile( - projectPath, - featureId, - `\n\nāŒ ERROR: ${error.message}\n\n${error.stack || ''}\n` - ); - } catch (contextError) { - console.error("[AutoMode] Failed to write error to context:", contextError); - } - - // Update feature status to waiting_approval so user can review the error - try { - await featureLoader.updateFeatureStatus( - featureId, - "waiting_approval", - projectPath, - { error: error.message } - ); - } catch (statusError) { - console.error("[AutoMode] Failed to update feature status after error:", statusError); - } - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - } finally { - // Clean up this feature's execution - this.runningFeatures.delete(featureId); - } - } - - /** - * Analyze a new project - scans codebase and updates app_spec.txt - * This is triggered when opening a project for the first time - */ - async analyzeProject({ projectPath, sendToRenderer }) { - console.log(`[AutoMode] Analyzing project at: ${projectPath}`); - - const analysisId = `project-analysis-${Date.now()}`; - - // Check if already analyzing this project - if (this.runningFeatures.has(analysisId)) { - throw new Error("Project analysis is already running"); - } - - // Register as running - const execution = this.createExecutionContext(analysisId); - execution.projectPath = projectPath; - execution.sendToRenderer = sendToRenderer; - this.runningFeatures.set(analysisId, execution); - - try { - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_start", - featureId: analysisId, - feature: { - id: analysisId, - category: "Project Analysis", - description: "Analyzing project structure and tech stack", - }, - }); - - // Perform the analysis - const result = await projectAnalyzer.runProjectAnalysis( - projectPath, - analysisId, - sendToRenderer, - execution - ); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_complete", - featureId: analysisId, - passes: result.success, - message: result.message, - }); - - return { success: true, message: result.message }; - } catch (error) { - console.error("[AutoMode] Error analyzing project:", error); - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_error", - error: error.message, - featureId: analysisId, - }); - throw error; - } finally { - this.runningFeatures.delete(analysisId); - } - } - - /** - * Stop a specific feature by ID - */ - async stopFeature({ featureId }) { - if (!this.runningFeatures.has(featureId)) { - return { success: false, error: `Feature ${featureId} is not running` }; - } - - console.log(`[AutoMode] Stopping feature: ${featureId}`); - - const execution = this.runningFeatures.get(featureId); - if (execution && execution.abortController) { - execution.abortController.abort(); - } - - // Clean up - this.runningFeatures.delete(featureId); - - return { success: true }; - } - - /** - * Follow-up on a feature with additional prompt - * This continues work on a feature that's in waiting_approval status - */ - async followUpFeature({ - projectPath, - featureId, - prompt, - imagePaths, - sendToRenderer, - }) { - // Check if this feature is already running - if (this.runningFeatures.has(featureId)) { - throw new Error(`Feature ${featureId} is already running`); - } - - console.log( - `[AutoMode] Follow-up on feature: ${featureId} with prompt: ${prompt}` - ); - - // Register this feature as running - const execution = this.createExecutionContext(featureId); - execution.projectPath = projectPath; - execution.sendToRenderer = sendToRenderer; - this.runningFeatures.set(featureId, execution); - - // Start the async work in the background (don't await) - // This allows the API to return immediately so the modal can close - this.runFollowUpWork({ - projectPath, - featureId, - prompt, - imagePaths, - sendToRenderer, - execution, - }).catch((error) => { - console.error("[AutoMode] Follow-up work error:", error); - this.runningFeatures.delete(featureId); - }); - - // Return immediately so the frontend can close the modal - return { success: true }; - } - - /** - * Internal method to run follow-up work asynchronously - */ - async runFollowUpWork({ - projectPath, - featureId, - prompt, - imagePaths, - sendToRenderer, - execution, - }) { - try { - // Load features - const features = await featureLoader.loadFeatures(projectPath); - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - throw new Error(`Feature ${featureId} not found`); - } - - console.log(`[AutoMode] Following up on feature: ${feature.description}`); - - // Update status to in_progress - await featureLoader.updateFeatureStatus( - featureId, - "in_progress", - projectPath - ); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_start", - featureId: feature.id, - feature: feature, - }); - - // Read existing context and append follow-up prompt - const previousContext = await contextManager.readContextFile( - projectPath, - featureId - ); - - // Append follow-up prompt to context - const followUpContext = `${previousContext}\n\n## Follow-up Instructions\n\n${prompt}`; - await contextManager.writeToContextFile( - projectPath, - featureId, - `\n\n## Follow-up Instructions\n\n${prompt}` - ); - - // Resume implementation with follow-up context and optional images - const result = await featureExecutor.resumeFeatureWithContext( - { ...feature, followUpPrompt: prompt, followUpImages: imagePaths }, - projectPath, - sendToRenderer, - followUpContext, - execution - ); - - // For skipTests features, go to waiting_approval on success instead of verified - // On failure, go to waiting_approval so user can review and decide next steps - const newStatus = result.passes - ? feature.skipTests - ? "waiting_approval" - : "verified" - : "waiting_approval"; - - await featureLoader.updateFeatureStatus( - feature.id, - newStatus, - projectPath - ); - - // Keep context file for viewing output later (deleted only when card is removed) - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_complete", - featureId: feature.id, - passes: result.passes, - message: result.message, - }); - } catch (error) { - console.error("[AutoMode] Error in follow-up:", error); - - // Write error to context file - try { - await contextManager.writeToContextFile( - projectPath, - featureId, - `\n\nāŒ ERROR: ${error.message}\n\n${error.stack || ''}\n` - ); - } catch (contextError) { - console.error("[AutoMode] Failed to write error to context:", contextError); - } - - // Update feature status to waiting_approval so user can review the error - try { - await featureLoader.updateFeatureStatus( - featureId, - "waiting_approval", - projectPath, - { error: error.message } - ); - } catch (statusError) { - console.error("[AutoMode] Failed to update feature status after error:", statusError); - } - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - } finally { - this.runningFeatures.delete(featureId); - } - } - - /** - * Commit changes for a feature without doing additional work - * This marks the feature as verified and commits the changes - */ - async commitFeature({ projectPath, featureId, sendToRenderer }) { - console.log(`[AutoMode] Committing feature: ${featureId}`); - - // Register briefly as running for the commit operation - const execution = this.createExecutionContext(featureId); - execution.projectPath = projectPath; - execution.sendToRenderer = sendToRenderer; - this.runningFeatures.set(featureId, execution); - - try { - // Load feature to get description for commit message - const features = await featureLoader.loadFeatures(projectPath); - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - throw new Error(`Feature ${featureId} not found`); - } - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_start", - featureId: feature.id, - feature: { ...feature, description: "Committing changes..." }, - }); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_phase", - featureId, - phase: "action", - message: "Committing changes to git...", - }); - - // Run git commit via the agent - await featureExecutor.commitChangesOnly( - feature, - projectPath, - sendToRenderer, - execution - ); - - // Update status to verified - await featureLoader.updateFeatureStatus( - featureId, - "verified", - projectPath - ); - - // Keep context file for viewing output later (deleted only when card is removed) - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_complete", - featureId: feature.id, - passes: true, - message: "Changes committed successfully", - }); - - return { success: true }; - } catch (error) { - console.error("[AutoMode] Error committing feature:", error); - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - throw error; - } finally { - this.runningFeatures.delete(featureId); - } - } - - /** - * Sleep helper - */ - sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - /** - * Revert feature changes by removing the worktree - * This effectively discards all changes made by the agent - */ - async revertFeature({ projectPath, featureId, sendToRenderer }) { - console.log(`[AutoMode] Reverting feature: ${featureId}`); - - try { - // Stop the feature if it's running - if (this.runningFeatures.has(featureId)) { - await this.stopFeature({ featureId }); - } - - // Remove the worktree and delete the branch - const result = await worktreeManager.removeWorktree(projectPath, featureId, true); - - if (!result.success) { - throw new Error(result.error || "Failed to remove worktree"); - } - - // Clear worktree info from feature - await featureLoader.updateFeatureWorktree(featureId, projectPath, null, null); - - // Update feature status back to backlog - await featureLoader.updateFeatureStatus(featureId, "backlog", projectPath); - - // Delete context file - await contextManager.deleteContextFile(projectPath, featureId); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_complete", - featureId: featureId, - passes: false, - message: "Feature reverted - all changes discarded", - }); - - console.log(`[AutoMode] Feature ${featureId} reverted successfully`); - return { success: true, removedPath: result.removedPath }; - } catch (error) { - console.error("[AutoMode] Error reverting feature:", error); - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - return { success: false, error: error.message }; - } - } - - /** - * Merge feature worktree changes back to main branch - */ - async mergeFeature({ projectPath, featureId, options = {}, sendToRenderer }) { - console.log(`[AutoMode] Merging feature: ${featureId}`); - - try { - // Load feature to get worktree info - const features = await featureLoader.loadFeatures(projectPath); - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - throw new Error(`Feature ${featureId} not found`); - } - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_progress", - featureId: featureId, - content: "Merging feature branch into main...\n", - }); - - // Merge the worktree - const result = await worktreeManager.mergeWorktree(projectPath, featureId, { - ...options, - cleanup: true, // Remove worktree after successful merge - }); - - if (!result.success) { - throw new Error(result.error || "Failed to merge worktree"); - } - - // Clear worktree info from feature - await featureLoader.updateFeatureWorktree(featureId, projectPath, null, null); - - // Update feature status to verified - await featureLoader.updateFeatureStatus(featureId, "verified", projectPath); - - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_feature_complete", - featureId: featureId, - passes: true, - message: `Feature merged into ${result.intoBranch}`, - }); - - console.log(`[AutoMode] Feature ${featureId} merged successfully`); - return { success: true, mergedBranch: result.mergedBranch }; - } catch (error) { - console.error("[AutoMode] Error merging feature:", error); - this.emitEvent(projectPath, sendToRenderer, { - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - return { success: false, error: error.message }; - } - } - - /** - * Get worktree info for a feature - */ - async getWorktreeInfo({ projectPath, featureId }) { - return await worktreeManager.getWorktreeInfo(projectPath, featureId); - } - - /** - * Get worktree status (changed files, commits, etc.) - */ - async getWorktreeStatus({ projectPath, featureId }) { - const worktreeInfo = await worktreeManager.getWorktreeInfo(projectPath, featureId); - if (!worktreeInfo.success) { - return { success: false, error: "Worktree not found" }; - } - return await worktreeManager.getWorktreeStatus(worktreeInfo.worktreePath); - } - - /** - * List all feature worktrees - */ - async listWorktrees({ projectPath }) { - const worktrees = await worktreeManager.getAllFeatureWorktrees(projectPath); - return { success: true, worktrees }; - } - - /** - * Get file diffs for a feature worktree - */ - async getFileDiffs({ projectPath, featureId }) { - const worktreeInfo = await worktreeManager.getWorktreeInfo(projectPath, featureId); - if (!worktreeInfo.success) { - return { success: false, error: "Worktree not found" }; - } - return await worktreeManager.getFileDiffs(worktreeInfo.worktreePath); - } - - /** - * Get diff for a specific file in a feature worktree - */ - async getFileDiff({ projectPath, featureId, filePath }) { - const worktreeInfo = await worktreeManager.getWorktreeInfo(projectPath, featureId); - if (!worktreeInfo.success) { - return { success: false, error: "Worktree not found" }; - } - return await worktreeManager.getFileDiff(worktreeInfo.worktreePath, filePath); - } -} - -// Export singleton instance -module.exports = new AutoModeService(); diff --git a/apps/app/electron/main-simplified.js b/apps/app/electron/main-simplified.js deleted file mode 100644 index f15e6946..00000000 --- a/apps/app/electron/main-simplified.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Simplified Electron main process - * - * This version spawns the backend server and uses HTTP API for most operations. - * Only native features (dialogs, shell) use IPC. - */ - -const path = require("path"); -const { spawn } = require("child_process"); - -// Load environment variables from .env file -require("dotenv").config({ path: path.join(__dirname, "../.env") }); - -const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron"); - -let mainWindow = null; -let serverProcess = null; -const SERVER_PORT = 3008; - -// Get icon path - works in both dev and production -function getIconPath() { - return app.isPackaged - ? path.join(process.resourcesPath, "app", "public", "logo.png") - : path.join(__dirname, "../public/logo.png"); -} - -/** - * Start the backend server - */ -async function startServer() { - const isDev = !app.isPackaged; - - // Server entry point - const serverPath = isDev - ? path.join(__dirname, "../../server/dist/index.js") - : path.join(process.resourcesPath, "server", "index.js"); - - // Set environment variables for server - const env = { - ...process.env, - PORT: SERVER_PORT.toString(), - DATA_DIR: app.getPath("userData"), - }; - - console.log("[Electron] Starting backend server..."); - - serverProcess = spawn("node", [serverPath], { - env, - stdio: ["ignore", "pipe", "pipe"], - }); - - serverProcess.stdout.on("data", (data) => { - console.log(`[Server] ${data.toString().trim()}`); - }); - - serverProcess.stderr.on("data", (data) => { - console.error(`[Server Error] ${data.toString().trim()}`); - }); - - serverProcess.on("close", (code) => { - console.log(`[Server] Process exited with code ${code}`); - serverProcess = null; - }); - - // Wait for server to be ready - await waitForServer(); -} - -/** - * Wait for server to be available - */ -async function waitForServer(maxAttempts = 30) { - const http = require("http"); - - for (let i = 0; i < maxAttempts; i++) { - try { - await new Promise((resolve, reject) => { - const req = http.get(`http://localhost:${SERVER_PORT}/api/health`, (res) => { - if (res.statusCode === 200) { - resolve(); - } else { - reject(new Error(`Status: ${res.statusCode}`)); - } - }); - req.on("error", reject); - req.setTimeout(1000, () => { - req.destroy(); - reject(new Error("Timeout")); - }); - }); - console.log("[Electron] Server is ready"); - return; - } catch { - await new Promise((r) => setTimeout(r, 500)); - } - } - - throw new Error("Server failed to start"); -} - -/** - * Create the main window - */ -function createWindow() { - mainWindow = new BrowserWindow({ - width: 1400, - height: 900, - minWidth: 1024, - minHeight: 700, - icon: getIconPath(), - webPreferences: { - preload: path.join(__dirname, "preload-simplified.js"), - contextIsolation: true, - nodeIntegration: false, - }, - titleBarStyle: "hiddenInset", - backgroundColor: "#0a0a0a", - }); - - // Load Next.js dev server in development or production build - const isDev = !app.isPackaged; - if (isDev) { - mainWindow.loadURL("http://localhost:3007"); - if (process.env.OPEN_DEVTOOLS === "true") { - mainWindow.webContents.openDevTools(); - } - } else { - mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html")); - } - - mainWindow.on("closed", () => { - mainWindow = null; - }); -} - -// App lifecycle -app.whenReady().then(async () => { - // Set app icon (dock icon on macOS) - if (process.platform === "darwin" && app.dock) { - app.dock.setIcon(getIconPath()); - } - - try { - // Start backend server - await startServer(); - - // Create window - createWindow(); - } catch (error) { - console.error("[Electron] Failed to start:", error); - app.quit(); - } - - app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } - }); -}); - -app.on("window-all-closed", () => { - if (process.platform !== "darwin") { - app.quit(); - } -}); - -app.on("before-quit", () => { - // Kill server process - if (serverProcess) { - console.log("[Electron] Stopping server..."); - serverProcess.kill(); - serverProcess = null; - } -}); - -// ============================================ -// IPC Handlers - Only native features -// ============================================ - -// Native file dialogs -ipcMain.handle("dialog:openDirectory", async () => { - const result = await dialog.showOpenDialog(mainWindow, { - properties: ["openDirectory", "createDirectory"], - }); - return result; -}); - -ipcMain.handle("dialog:openFile", async (_, options = {}) => { - const result = await dialog.showOpenDialog(mainWindow, { - properties: ["openFile"], - ...options, - }); - return result; -}); - -ipcMain.handle("dialog:saveFile", async (_, options = {}) => { - const result = await dialog.showSaveDialog(mainWindow, options); - return result; -}); - -// Shell operations -ipcMain.handle("shell:openExternal", async (_, url) => { - try { - await shell.openExternal(url); - return { success: true }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -ipcMain.handle("shell:openPath", async (_, filePath) => { - try { - await shell.openPath(filePath); - return { success: true }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -// App info -ipcMain.handle("app:getPath", async (_, name) => { - return app.getPath(name); -}); - -ipcMain.handle("app:getVersion", async () => { - return app.getVersion(); -}); - -ipcMain.handle("app:isPackaged", async () => { - return app.isPackaged; -}); - -// Ping - for connection check -ipcMain.handle("ping", async () => { - return "pong"; -}); - -// Get server URL for HTTP client -ipcMain.handle("server:getUrl", async () => { - return `http://localhost:${SERVER_PORT}`; -}); diff --git a/apps/app/electron/main.js b/apps/app/electron/main.js index 70e2511c..33d91e71 100644 --- a/apps/app/electron/main.js +++ b/apps/app/electron/main.js @@ -1,7 +1,21 @@ +/** + * Simplified Electron main process + * + * This version spawns the backend server and uses HTTP API for most operations. + * Only native features (dialogs, shell) use IPC. + */ + const path = require("path"); -const { app, BrowserWindow, shell } = require("electron"); +const { spawn } = require("child_process"); + +// Load environment variables from .env file +require("dotenv").config({ path: path.join(__dirname, "../.env") }); + +const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron"); let mainWindow = null; +let serverProcess = null; +const SERVER_PORT = 3008; // Get icon path - works in both dev and production function getIconPath() { @@ -10,6 +24,83 @@ function getIconPath() { : path.join(__dirname, "../public/logo.png"); } +/** + * Start the backend server + */ +async function startServer() { + const isDev = !app.isPackaged; + + // Server entry point + const serverPath = isDev + ? path.join(__dirname, "../../server/dist/index.js") + : path.join(process.resourcesPath, "server", "index.js"); + + // Set environment variables for server + const env = { + ...process.env, + PORT: SERVER_PORT.toString(), + DATA_DIR: app.getPath("userData"), + }; + + console.log("[Electron] Starting backend server..."); + + serverProcess = spawn("node", [serverPath], { + env, + stdio: ["ignore", "pipe", "pipe"], + }); + + serverProcess.stdout.on("data", (data) => { + console.log(`[Server] ${data.toString().trim()}`); + }); + + serverProcess.stderr.on("data", (data) => { + console.error(`[Server Error] ${data.toString().trim()}`); + }); + + serverProcess.on("close", (code) => { + console.log(`[Server] Process exited with code ${code}`); + serverProcess = null; + }); + + // Wait for server to be ready + await waitForServer(); +} + +/** + * Wait for server to be available + */ +async function waitForServer(maxAttempts = 30) { + const http = require("http"); + + for (let i = 0; i < maxAttempts; i++) { + try { + await new Promise((resolve, reject) => { + const req = http.get(`http://localhost:${SERVER_PORT}/api/health`, (res) => { + if (res.statusCode === 200) { + resolve(); + } else { + reject(new Error(`Status: ${res.statusCode}`)); + } + }); + req.on("error", reject); + req.setTimeout(1000, () => { + req.destroy(); + reject(new Error("Timeout")); + }); + }); + console.log("[Electron] Server is ready"); + return; + } catch { + await new Promise((r) => setTimeout(r, 500)); + } + } + + throw new Error("Server failed to start"); +} + +/** + * Create the main window + */ function createWindow() { mainWindow = new BrowserWindow({ width: 1400, @@ -30,7 +121,6 @@ function createWindow() { const isDev = !app.isPackaged; if (isDev) { mainWindow.loadURL("http://localhost:3007"); - // Open DevTools if OPEN_DEVTOOLS environment variable is set if (process.env.OPEN_DEVTOOLS === "true") { mainWindow.webContents.openDevTools(); } @@ -38,24 +128,28 @@ function createWindow() { mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html")); } - // Handle external links - open in default browser - mainWindow.webContents.setWindowOpenHandler(({ url }) => { - shell.openExternal(url); - return { action: "deny" }; - }); - mainWindow.on("closed", () => { mainWindow = null; }); } -app.whenReady().then(() => { +// App lifecycle +app.whenReady().then(async () => { // Set app icon (dock icon on macOS) if (process.platform === "darwin" && app.dock) { app.dock.setIcon(getIconPath()); } - createWindow(); + try { + // Start backend server + await startServer(); + + // Create window + createWindow(); + } catch (error) { + console.error("[Electron] Failed to start:", error); + app.quit(); + } app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { @@ -69,3 +163,79 @@ app.on("window-all-closed", () => { app.quit(); } }); + +app.on("before-quit", () => { + // Kill server process + if (serverProcess) { + console.log("[Electron] Stopping server..."); + serverProcess.kill(); + serverProcess = null; + } +}); + +// ============================================ +// IPC Handlers - Only native features +// ============================================ + +// Native file dialogs +ipcMain.handle("dialog:openDirectory", async () => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openDirectory", "createDirectory"], + }); + return result; +}); + +ipcMain.handle("dialog:openFile", async (_, options = {}) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result; +}); + +ipcMain.handle("dialog:saveFile", async (_, options = {}) => { + const result = await dialog.showSaveDialog(mainWindow, options); + return result; +}); + +// Shell operations +ipcMain.handle("shell:openExternal", async (_, url) => { + try { + await shell.openExternal(url); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle("shell:openPath", async (_, filePath) => { + try { + await shell.openPath(filePath); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +// App info +ipcMain.handle("app:getPath", async (_, name) => { + return app.getPath(name); +}); + +ipcMain.handle("app:getVersion", async () => { + return app.getVersion(); +}); + +ipcMain.handle("app:isPackaged", async () => { + return app.isPackaged; +}); + +// Ping - for connection check +ipcMain.handle("ping", async () => { + return "pong"; +}); + +// Get server URL for HTTP client +ipcMain.handle("server:getUrl", async () => { + return `http://localhost:${SERVER_PORT}`; +}); diff --git a/apps/app/electron/preload-simplified.js b/apps/app/electron/preload-simplified.js deleted file mode 100644 index 289d2cd7..00000000 --- a/apps/app/electron/preload-simplified.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Simplified Electron preload script - * - * Only exposes native features (dialogs, shell) and server URL. - * All other operations go through HTTP API. - */ - -const { contextBridge, ipcRenderer } = require("electron"); - -// Expose minimal API for native features -contextBridge.exposeInMainWorld("electronAPI", { - // Platform info - platform: process.platform, - isElectron: true, - - // Connection check - ping: () => ipcRenderer.invoke("ping"), - - // Get server URL for HTTP client - getServerUrl: () => ipcRenderer.invoke("server:getUrl"), - - // Native dialogs - better UX than prompt() - openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"), - openFile: (options) => ipcRenderer.invoke("dialog:openFile", options), - saveFile: (options) => ipcRenderer.invoke("dialog:saveFile", options), - - // Shell operations - openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url), - openPath: (filePath) => ipcRenderer.invoke("shell:openPath", filePath), - - // App info - getPath: (name) => ipcRenderer.invoke("app:getPath", name), - getVersion: () => ipcRenderer.invoke("app:getVersion"), - isPackaged: () => ipcRenderer.invoke("app:isPackaged"), -}); - -console.log("[Preload] Electron API exposed (simplified mode)"); diff --git a/apps/app/electron/preload.js b/apps/app/electron/preload.js index 4d802527..289d2cd7 100644 --- a/apps/app/electron/preload.js +++ b/apps/app/electron/preload.js @@ -1,10 +1,37 @@ -const { contextBridge } = require("electron"); +/** + * Simplified Electron preload script + * + * Only exposes native features (dialogs, shell) and server URL. + * All other operations go through HTTP API. + */ -// Only expose a flag to detect Electron environment -// All API calls go through HTTP to the backend server -contextBridge.exposeInMainWorld("isElectron", true); +const { contextBridge, ipcRenderer } = require("electron"); -// Expose platform info for UI purposes -contextBridge.exposeInMainWorld("electronPlatform", process.platform); +// Expose minimal API for native features +contextBridge.exposeInMainWorld("electronAPI", { + // Platform info + platform: process.platform, + isElectron: true, -console.log("[Preload] Electron flag exposed (HTTP-only mode)"); + // Connection check + ping: () => ipcRenderer.invoke("ping"), + + // Get server URL for HTTP client + getServerUrl: () => ipcRenderer.invoke("server:getUrl"), + + // Native dialogs - better UX than prompt() + openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"), + openFile: (options) => ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options) => ipcRenderer.invoke("dialog:saveFile", options), + + // Shell operations + openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url), + openPath: (filePath) => ipcRenderer.invoke("shell:openPath", filePath), + + // App info + getPath: (name) => ipcRenderer.invoke("app:getPath", name), + getVersion: () => ipcRenderer.invoke("app:getVersion"), + isPackaged: () => ipcRenderer.invoke("app:isPackaged"), +}); + +console.log("[Preload] Electron API exposed (simplified mode)"); diff --git a/apps/app/electron/services/claude-cli-detector.js b/apps/app/electron/services/claude-cli-detector.js deleted file mode 100644 index 2a8f193c..00000000 --- a/apps/app/electron/services/claude-cli-detector.js +++ /dev/null @@ -1,721 +0,0 @@ -const { execSync, spawn } = require("child_process"); -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -let runPtyCommand = null; -try { - ({ runPtyCommand } = require("./pty-runner")); -} catch (error) { - console.warn( - "[ClaudeCliDetector] node-pty unavailable, will fall back to external terminal:", - error?.message || error - ); -} - -const ANSI_REGEX = - // eslint-disable-next-line no-control-regex - /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b[@-_]|\u001b\][^\u0007]*\u0007/g; - -const stripAnsi = (text = "") => text.replace(ANSI_REGEX, ""); - -/** - * Claude CLI Detector - * - * Authentication options: - * 1. OAuth Token (Subscription): User runs `claude setup-token` and provides the token to the app - * 2. API Key (Pay-per-use): User provides their Anthropic API key directly - */ -class ClaudeCliDetector { - /** - * Check if Claude Code CLI is installed and accessible - * @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'none' } - */ - /** - * Try to get updated PATH from shell config files - * This helps detect CLI installations that modify shell config but haven't updated the current process PATH - */ - static getUpdatedPathFromShellConfig() { - const homeDir = os.homedir(); - const shell = process.env.SHELL || "/bin/bash"; - const shellName = path.basename(shell); - - const configFiles = []; - if (shellName.includes("zsh")) { - configFiles.push(path.join(homeDir, ".zshrc")); - configFiles.push(path.join(homeDir, ".zshenv")); - configFiles.push(path.join(homeDir, ".zprofile")); - } else if (shellName.includes("bash")) { - configFiles.push(path.join(homeDir, ".bashrc")); - configFiles.push(path.join(homeDir, ".bash_profile")); - configFiles.push(path.join(homeDir, ".profile")); - } - - const commonPaths = [ - path.join(homeDir, ".local", "bin"), - path.join(homeDir, ".cargo", "bin"), - "/usr/local/bin", - "/opt/homebrew/bin", - path.join(homeDir, "bin"), - ]; - - for (const configFile of configFiles) { - if (fs.existsSync(configFile)) { - try { - const content = fs.readFileSync(configFile, "utf-8"); - const pathMatches = content.match( - /export\s+PATH=["']?([^"'\n]+)["']?/g - ); - if (pathMatches) { - for (const match of pathMatches) { - const pathValue = match - .replace(/export\s+PATH=["']?/, "") - .replace(/["']?$/, ""); - const paths = pathValue - .split(":") - .filter((p) => p && !p.includes("$")); - commonPaths.push(...paths); - } - } - } catch (error) { - // Ignore errors reading config files - } - } - } - - return [...new Set(commonPaths)]; - } - - static detectClaudeInstallation() { - try { - // Check if 'claude' command is in PATH (Unix) - if (process.platform !== "win32") { - try { - const claudePath = execSync("which claude 2>/dev/null", { - encoding: "utf-8", - }).trim(); - if (claudePath) { - const version = this.getClaudeVersion(claudePath); - return { - installed: true, - path: claudePath, - version: version, - method: "cli", - }; - } - } catch (error) { - // CLI not in PATH - } - } - - // Check Windows path - if (process.platform === "win32") { - try { - const claudePath = execSync("where claude 2>nul", { - encoding: "utf-8", - }) - .trim() - .split("\n")[0]; - if (claudePath) { - const version = this.getClaudeVersion(claudePath); - return { - installed: true, - path: claudePath, - version: version, - method: "cli", - }; - } - } catch (error) { - // Not found on Windows - } - } - - // Check for local installation - const localClaudePath = path.join( - os.homedir(), - ".claude", - "local", - "claude" - ); - if (fs.existsSync(localClaudePath)) { - const version = this.getClaudeVersion(localClaudePath); - return { - installed: true, - path: localClaudePath, - version: version, - method: "cli-local", - }; - } - - // Check common installation locations - const commonPaths = this.getUpdatedPathFromShellConfig(); - const binaryNames = ["claude", "claude-code"]; - - for (const basePath of commonPaths) { - for (const binaryName of binaryNames) { - const claudePath = path.join(basePath, binaryName); - if (fs.existsSync(claudePath)) { - try { - const version = this.getClaudeVersion(claudePath); - return { - installed: true, - path: claudePath, - version: version, - method: "cli", - }; - } catch (error) { - // File exists but can't get version - } - } - } - } - - // Try to source shell config and check PATH again (Unix) - if (process.platform !== "win32") { - try { - const shell = process.env.SHELL || "/bin/bash"; - const shellName = path.basename(shell); - const homeDir = os.homedir(); - - let sourceCmd = ""; - if (shellName.includes("zsh")) { - sourceCmd = `source ${homeDir}/.zshrc 2>/dev/null && which claude`; - } else if (shellName.includes("bash")) { - sourceCmd = `source ${homeDir}/.bashrc 2>/dev/null && which claude`; - } - - if (sourceCmd) { - const claudePath = execSync(`bash -c "${sourceCmd}"`, { - encoding: "utf-8", - timeout: 2000, - }).trim(); - if (claudePath && claudePath.startsWith("/")) { - const version = this.getClaudeVersion(claudePath); - return { - installed: true, - path: claudePath, - version: version, - method: "cli", - }; - } - } - } catch (error) { - // Failed to source shell config - } - } - - return { - installed: false, - path: null, - version: null, - method: "none", - }; - } catch (error) { - return { - installed: false, - path: null, - version: null, - method: "none", - error: error.message, - }; - } - } - - /** - * Get Claude CLI version - * @param {string} claudePath Path to claude executable - * @returns {string|null} Version string or null - */ - static getClaudeVersion(claudePath) { - try { - const version = execSync(`"${claudePath}" --version 2>/dev/null`, { - encoding: "utf-8", - timeout: 5000, - }).trim(); - return version || null; - } catch (error) { - return null; - } - } - - /** - * Get authentication status - * Checks for: - * 1. OAuth token stored in app's credentials (from `claude setup-token`) - * 2. API key stored in app's credentials - * 3. API key in environment variable - * - * @param {string} appCredentialsPath Path to app's credentials.json - * @returns {Object} Authentication status - */ - static getAuthStatus(appCredentialsPath) { - const envApiKey = process.env.ANTHROPIC_API_KEY; - const envOAuthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN; - - let storedOAuthToken = null; - let storedApiKey = null; - - if (appCredentialsPath && fs.existsSync(appCredentialsPath)) { - try { - const content = fs.readFileSync(appCredentialsPath, "utf-8"); - const credentials = JSON.parse(content); - storedOAuthToken = credentials.anthropic_oauth_token || null; - storedApiKey = - credentials.anthropic || credentials.anthropic_api_key || null; - } catch (error) { - // Ignore credential read errors - } - } - - // Authentication priority (highest to lowest): - // 1. Environment OAuth Token (CLAUDE_CODE_OAUTH_TOKEN) - // 2. Stored OAuth Token (from credentials file) - // 3. Stored API Key (from credentials file) - // 4. Environment API Key (ANTHROPIC_API_KEY) - let authenticated = false; - let method = "none"; - - if (envOAuthToken) { - authenticated = true; - method = "oauth_token_env"; - } else if (storedOAuthToken) { - authenticated = true; - method = "oauth_token"; - } else if (storedApiKey) { - authenticated = true; - method = "api_key"; - } else if (envApiKey) { - authenticated = true; - method = "api_key_env"; - } - - return { - authenticated, - method, - hasStoredOAuthToken: !!storedOAuthToken, - hasStoredApiKey: !!storedApiKey, - hasEnvApiKey: !!envApiKey, - hasEnvOAuthToken: !!envOAuthToken, - }; - } - /** - * Get installation info (installation status only, no auth) - * @returns {Object} Installation info with status property - */ - static getInstallationInfo() { - const installation = this.detectClaudeInstallation(); - return { - status: installation.installed ? "installed" : "not_installed", - installed: installation.installed, - path: installation.path, - version: installation.version, - method: installation.method, - }; - } - - /** - * Get full status including installation and auth - * @param {string} appCredentialsPath Path to app's credentials.json - * @returns {Object} Full status - */ - static getFullStatus(appCredentialsPath) { - const installation = this.detectClaudeInstallation(); - const auth = this.getAuthStatus(appCredentialsPath); - - return { - success: true, - status: installation.installed ? "installed" : "not_installed", - installed: installation.installed, - path: installation.path, - version: installation.version, - method: installation.method, - auth, - }; - } - - /** - * Get installation info and recommendations - * @returns {Object} Installation status and recommendations - */ - static getInstallationInfo() { - const detection = this.detectClaudeInstallation(); - - if (detection.installed) { - return { - status: 'installed', - method: detection.method, - version: detection.version, - path: detection.path, - recommendation: 'Claude Code CLI is ready for ultrathink' - }; - } - - return { - status: 'not_installed', - recommendation: 'Install Claude Code CLI for optimal ultrathink performance', - installCommands: this.getInstallCommands() - }; - } - - /** - * Get installation commands for different platforms - * @returns {Object} Installation commands - */ - static getInstallCommands() { - return { - macos: "curl -fsSL https://claude.ai/install.sh | bash", - windows: "irm https://claude.ai/install.ps1 | iex", - linux: "curl -fsSL https://claude.ai/install.sh | bash", - }; - } - - /** - * Install Claude CLI using the official script - * @param {Function} onProgress Callback for progress updates - * @returns {Promise} Installation result - */ - static async installCli(onProgress) { - return new Promise((resolve, reject) => { - const platform = process.platform; - let command, args; - - if (platform === "win32") { - command = "powershell"; - args = ["-Command", "irm https://claude.ai/install.ps1 | iex"]; - } else { - command = "bash"; - args = ["-c", "curl -fsSL https://claude.ai/install.sh | bash"]; - } - - console.log("[ClaudeCliDetector] Installing Claude CLI..."); - - const proc = spawn(command, args, { - stdio: ["pipe", "pipe", "pipe"], - shell: false, - }); - - let output = ""; - let errorOutput = ""; - - proc.stdout.on("data", (data) => { - const text = data.toString(); - output += text; - if (onProgress) { - onProgress({ type: "stdout", data: text }); - } - }); - - proc.stderr.on("data", (data) => { - const text = data.toString(); - errorOutput += text; - if (onProgress) { - onProgress({ type: "stderr", data: text }); - } - }); - - proc.on("close", (code) => { - if (code === 0) { - console.log( - "[ClaudeCliDetector] Installation completed successfully" - ); - resolve({ - success: true, - output, - message: "Claude CLI installed successfully", - }); - } else { - console.error( - "[ClaudeCliDetector] Installation failed with code:", - code - ); - reject({ - success: false, - error: errorOutput || `Installation failed with code ${code}`, - output, - }); - } - }); - - proc.on("error", (error) => { - console.error("[ClaudeCliDetector] Installation error:", error); - reject({ - success: false, - error: error.message, - output, - }); - }); - }); - } - - /** - * Get instructions for setup-token command - * @returns {Object} Setup token instructions - */ - static getSetupTokenInstructions() { - const detection = this.detectClaudeInstallation(); - - if (!detection.installed) { - return { - success: false, - error: "Claude CLI is not installed. Please install it first.", - installCommands: this.getInstallCommands(), - }; - } - - return { - success: true, - command: "claude setup-token", - instructions: [ - "1. Open your terminal", - "2. Run: claude setup-token", - "3. Follow the prompts to authenticate", - "4. Copy the token that is displayed", - "5. Paste the token in the field below", - ], - note: "This token is from your Claude subscription and allows you to use Claude without API charges.", - }; - } - - /** - * Extract OAuth token from command output - * Tries multiple patterns to find the token - * @param {string} output The command output - * @returns {string|null} Extracted token or null - */ - static extractTokenFromOutput(output) { - // Pattern 1: CLAUDE_CODE_OAUTH_TOKEN= or CLAUDE_CODE_OAUTH_TOKEN: - const envMatch = output.match( - /CLAUDE_CODE_OAUTH_TOKEN[=:]\s*["']?([a-zA-Z0-9_\-\.]+)["']?/i - ); - if (envMatch) return envMatch[1]; - - // Pattern 2: "Token: " or "token: " - const tokenLabelMatch = output.match( - /\btoken[:\s]+["']?([a-zA-Z0-9_\-\.]{40,})["']?/i - ); - if (tokenLabelMatch) return tokenLabelMatch[1]; - - // Pattern 3: Look for token after success/authenticated message - const successMatch = output.match( - /(?:success|authenticated|generated|token is)[^\n]*\n\s*([a-zA-Z0-9_\-\.]{40,})/i - ); - if (successMatch) return successMatch[1]; - - // Pattern 4: Standalone long alphanumeric string on its own line (last resort) - // This catches tokens that are printed on their own line - const lines = output.split("\n"); - for (const line of lines) { - const trimmed = line.trim(); - // Token should be 40+ chars, alphanumeric with possible hyphens/underscores/dots - if (/^[a-zA-Z0-9_\-\.]{40,}$/.test(trimmed)) { - return trimmed; - } - } - - return null; - } - - /** - * Run claude setup-token command to generate OAuth token - * Opens an external terminal window since Claude CLI requires TTY for its Ink-based UI - * @param {Function} onProgress Callback for progress updates - * @returns {Promise} Result indicating terminal was opened - */ - static async runSetupToken(onProgress) { - const detection = this.detectClaudeInstallation(); - - if (!detection.installed) { - throw { - success: false, - error: "Claude CLI is not installed. Please install it first.", - requiresManualAuth: false, - }; - } - - const claudePath = detection.path; - const platform = process.platform; - const preferPty = - (platform === "win32" || - platform === "darwin" || - process.env.CLAUDE_AUTH_FORCE_PTY === "1") && - process.env.CLAUDE_AUTH_DISABLE_PTY !== "1"; - - const send = (data) => { - if (onProgress && data) { - onProgress({ type: "stdout", data }); - } - }; - - if (preferPty && runPtyCommand) { - try { - send("Starting in-app terminal session for Claude auth...\n"); - send("If your browser opens, complete sign-in and return here.\n\n"); - - const ptyResult = await runPtyCommand(claudePath, ["setup-token"], { - cols: 120, - rows: 30, - onData: (chunk) => send(chunk), - env: { - FORCE_COLOR: "1", - }, - }); - - const cleanedOutput = stripAnsi(ptyResult.output || ""); - const token = this.extractTokenFromOutput(cleanedOutput); - - if (ptyResult.success && token) { - send("\nCaptured token automatically.\n"); - return { - success: true, - token, - requiresManualAuth: false, - terminalOpened: false, - }; - } - - if (ptyResult.success && !token) { - send( - "\nCLI completed but token was not detected automatically. You can copy it above or retry.\n" - ); - return { - success: true, - requiresManualAuth: true, - terminalOpened: false, - error: "Could not capture token automatically", - output: cleanedOutput, - }; - } - - send( - `\nClaude CLI exited with code ${ptyResult.exitCode}. Falling back to manual copy.\n` - ); - return { - success: false, - error: `Claude CLI exited with code ${ptyResult.exitCode}`, - requiresManualAuth: true, - output: cleanedOutput, - }; - } catch (error) { - console.error("[ClaudeCliDetector] PTY auth failed, falling back:", error); - send( - `In-app terminal failed (${error?.message || "unknown error"}). Falling back to external terminal...\n` - ); - } - } - - // Fallback: external terminal window - if (preferPty && !runPtyCommand) { - send("In-app terminal unavailable (node-pty not loaded)."); - } else if (!preferPty) { - send("Using system terminal for authentication on this platform."); - } - send("Opening system terminal for authentication...\n"); - - // Helper function to check if a command exists asynchronously - const commandExists = (cmd) => { - return new Promise((resolve) => { - require("child_process").exec( - `which ${cmd}`, - { timeout: 1000 }, - (error) => { - resolve(!error); - } - ); - }); - }; - - // For Linux, find available terminal first (async) - let linuxTerminal = null; - if (platform !== "win32" && platform !== "darwin") { - const terminals = [ - ["gnome-terminal", ["--", claudePath, "setup-token"]], - ["konsole", ["-e", claudePath, "setup-token"]], - ["xterm", ["-e", claudePath, "setup-token"]], - ["x-terminal-emulator", ["-e", `${claudePath} setup-token`]], - ]; - - for (const [term, termArgs] of terminals) { - const exists = await commandExists(term); - if (exists) { - linuxTerminal = { command: term, args: termArgs }; - break; - } - } - } - - return new Promise((resolve, reject) => { - // Open command in external terminal since Claude CLI requires TTY - let command, args; - - if (platform === "win32") { - // Windows: Open new cmd window that stays open - command = "cmd"; - args = ["/c", "start", "cmd", "/k", `"${claudePath}" setup-token`]; - } else if (platform === "darwin") { - // macOS: Open Terminal.app - command = "osascript"; - args = [ - "-e", - `tell application "Terminal" to do script "${claudePath} setup-token"`, - "-e", - 'tell application "Terminal" to activate', - ]; - } else { - // Linux: Use the terminal we found earlier - if (!linuxTerminal) { - reject({ - success: false, - error: - "Could not find a terminal emulator. Please run 'claude setup-token' manually in your terminal.", - requiresManualAuth: true, - }); - return; - } - command = linuxTerminal.command; - args = linuxTerminal.args; - } - - console.log( - "[ClaudeCliDetector] Spawning terminal:", - command, - args.join(" ") - ); - - const proc = spawn(command, args, { - detached: true, - stdio: "ignore", - shell: platform === "win32", - }); - - proc.unref(); - - proc.on("error", (error) => { - console.error("[ClaudeCliDetector] Failed to open terminal:", error); - reject({ - success: false, - error: `Failed to open terminal: ${error.message}`, - requiresManualAuth: true, - }); - }); - - // Give the terminal a moment to open - setTimeout(() => { - send("Terminal window opened!\n\n"); - send("1. Complete the sign-in in your browser\n"); - send("2. Copy the token from the terminal\n"); - send("3. Paste it below\n"); - - // Resolve with manual auth required since we can't capture from external terminal - resolve({ - success: true, - requiresManualAuth: true, - terminalOpened: true, - message: - "Terminal opened. Complete authentication and paste the token below.", - }); - }, 500); - }); - } -} - -module.exports = ClaudeCliDetector; diff --git a/apps/app/electron/services/codex-cli-detector.js b/apps/app/electron/services/codex-cli-detector.js deleted file mode 100644 index 42a47d0e..00000000 --- a/apps/app/electron/services/codex-cli-detector.js +++ /dev/null @@ -1,566 +0,0 @@ -const { execSync, spawn } = require('child_process'); -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -/** - * Codex CLI Detector - Checks if OpenAI Codex CLI is installed - * - * Codex CLI is OpenAI's agent CLI tool that allows users to use - * GPT-5.1 Codex models (gpt-5.1-codex-max, gpt-5.1-codex, etc.) - * for code generation and agentic tasks. - */ -class CodexCliDetector { - /** - * Get the path to Codex config directory - * @returns {string} Path to .codex directory - */ - static getConfigDir() { - return path.join(os.homedir(), '.codex'); - } - - /** - * Get the path to Codex auth file - * @returns {string} Path to auth.json - */ - static getAuthPath() { - return path.join(this.getConfigDir(), 'auth.json'); - } - - /** - * Check Codex authentication status - * @returns {Object} Authentication status - */ - static checkAuth() { - try { - const authPath = this.getAuthPath(); - const envApiKey = process.env.OPENAI_API_KEY; - - // Try to verify authentication using codex CLI command if available - try { - const detection = this.detectCodexInstallation(); - if (detection.installed) { - try { - const statusOutput = execSync(`"${detection.path || 'codex'}" login status 2>/dev/null`, { - encoding: 'utf-8', - timeout: 5000 - }); - - if (statusOutput && (statusOutput.includes('Logged in') || statusOutput.includes('Authenticated'))) { - return { - authenticated: true, - method: 'cli_verified', - hasAuthFile: fs.existsSync(authPath), - hasEnvKey: !!envApiKey, - authPath - }; - } - } catch (statusError) { - // status command failed, continue with file-based check - } - } - } catch (verifyError) { - // CLI verification failed, continue with file-based check - } - - // Check if auth file exists - if (fs.existsSync(authPath)) { - let auth = null; - try { - const content = fs.readFileSync(authPath, 'utf-8'); - auth = JSON.parse(content); - - // Check for token object structure - if (auth.token && typeof auth.token === 'object') { - const token = auth.token; - if (token.Id_token || token.access_token || token.refresh_token || token.id_token) { - return { - authenticated: true, - method: 'cli_tokens', - hasAuthFile: true, - hasEnvKey: !!envApiKey, - authPath - }; - } - } - - // Check for tokens at root level - if (auth.access_token || auth.refresh_token || auth.Id_token || auth.id_token) { - return { - authenticated: true, - method: 'cli_tokens', - hasAuthFile: true, - hasEnvKey: !!envApiKey, - authPath - }; - } - - // Check for API key fields - if (auth.api_key || auth.openai_api_key || auth.apiKey) { - return { - authenticated: true, - method: 'auth_file', - hasAuthFile: true, - hasEnvKey: !!envApiKey, - authPath - }; - } - } catch (error) { - return { - authenticated: false, - method: 'none', - hasAuthFile: false, - hasEnvKey: !!envApiKey, - authPath - }; - } - - if (!auth) { - return { - authenticated: false, - method: 'none', - hasAuthFile: true, - hasEnvKey: !!envApiKey, - authPath - }; - } - - const keys = Object.keys(auth); - if (keys.length > 0) { - const hasTokens = keys.some(key => - key.toLowerCase().includes('token') || - key.toLowerCase().includes('refresh') || - (auth[key] && typeof auth[key] === 'object' && ( - auth[key].access_token || auth[key].refresh_token || auth[key].Id_token || auth[key].id_token - )) - ); - - if (hasTokens) { - return { - authenticated: true, - method: 'cli_tokens', - hasAuthFile: true, - hasEnvKey: !!envApiKey, - authPath - }; - } - - // File exists and has content - check if it's tokens or API key - const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh')); - return { - authenticated: true, - method: likelyTokens ? 'cli_tokens' : 'auth_file', - hasAuthFile: true, - hasEnvKey: !!envApiKey, - authPath - }; - } - } - - // Check environment variable - if (envApiKey) { - return { - authenticated: true, - method: 'env_var', - hasAuthFile: false, - hasEnvKey: true, - authPath - }; - } - - return { - authenticated: false, - method: 'none', - hasAuthFile: false, - hasEnvKey: false, - authPath - }; - } catch (error) { - return { - authenticated: false, - method: 'none', - error: error.message - }; - } - } - /** - * Check if Codex CLI is installed and accessible - * @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'npm'|'brew'|'none' } - */ - static detectCodexInstallation() { - try { - // Method 1: Check if 'codex' command is in PATH - try { - const codexPath = execSync('which codex 2>/dev/null', { encoding: 'utf-8' }).trim(); - if (codexPath) { - const version = this.getCodexVersion(codexPath); - return { - installed: true, - path: codexPath, - version: version, - method: 'cli' - }; - } - } catch (error) { - // CLI not in PATH, continue checking other methods - } - - // Method 2: Check for npm global installation - try { - const npmListOutput = execSync('npm list -g @openai/codex --depth=0 2>/dev/null', { encoding: 'utf-8' }); - if (npmListOutput && npmListOutput.includes('@openai/codex')) { - // Get the path from npm bin - const npmBinPath = execSync('npm bin -g', { encoding: 'utf-8' }).trim(); - const codexPath = path.join(npmBinPath, 'codex'); - const version = this.getCodexVersion(codexPath); - return { - installed: true, - path: codexPath, - version: version, - method: 'npm' - }; - } - } catch (error) { - // npm global not found - } - - // Method 3: Check for Homebrew installation on macOS - if (process.platform === 'darwin') { - try { - const brewList = execSync('brew list --formula 2>/dev/null', { encoding: 'utf-8' }); - if (brewList.includes('codex')) { - const brewPrefixOutput = execSync('brew --prefix codex 2>/dev/null', { encoding: 'utf-8' }).trim(); - const codexPath = path.join(brewPrefixOutput, 'bin', 'codex'); - const version = this.getCodexVersion(codexPath); - return { - installed: true, - path: codexPath, - version: version, - method: 'brew' - }; - } - } catch (error) { - // Homebrew not found or codex not installed via brew - } - } - - // Method 4: Check Windows path - if (process.platform === 'win32') { - try { - const codexPath = execSync('where codex 2>nul', { encoding: 'utf-8' }).trim().split('\n')[0]; - if (codexPath) { - const version = this.getCodexVersion(codexPath); - return { - installed: true, - path: codexPath, - version: version, - method: 'cli' - }; - } - } catch (error) { - // Not found on Windows - } - } - - // Method 5: Check common installation paths - const commonPaths = [ - path.join(os.homedir(), '.local', 'bin', 'codex'), - path.join(os.homedir(), '.npm-global', 'bin', 'codex'), - '/usr/local/bin/codex', - '/opt/homebrew/bin/codex', - ]; - - for (const checkPath of commonPaths) { - if (fs.existsSync(checkPath)) { - const version = this.getCodexVersion(checkPath); - return { - installed: true, - path: checkPath, - version: version, - method: 'cli' - }; - } - } - - // Method 6: Check if OPENAI_API_KEY is set (can use Codex API directly) - if (process.env.OPENAI_API_KEY) { - return { - installed: false, - path: null, - version: null, - method: 'api-key-only', - hasApiKey: true - }; - } - - return { - installed: false, - path: null, - version: null, - method: 'none' - }; - } catch (error) { - // Error detecting Codex installation - return { - installed: false, - path: null, - version: null, - method: 'none', - error: error.message - }; - } - } - - /** - * Get Codex CLI version from executable path - * @param {string} codexPath Path to codex executable - * @returns {string|null} Version string or null - */ - static getCodexVersion(codexPath) { - try { - const version = execSync(`"${codexPath}" --version 2>/dev/null`, { encoding: 'utf-8' }).trim(); - return version || null; - } catch (error) { - return null; - } - } - - /** - * Get installation info and recommendations - * @returns {Object} Installation status and recommendations - */ - static getInstallationInfo() { - const detection = this.detectCodexInstallation(); - - if (detection.installed) { - return { - status: 'installed', - method: detection.method, - version: detection.version, - path: detection.path, - recommendation: detection.method === 'cli' - ? 'Using Codex CLI - ready for GPT-5.1 Codex models' - : `Using Codex CLI via ${detection.method} - ready for GPT-5.1 Codex models` - }; - } - - // Not installed but has API key - if (detection.method === 'api-key-only') { - return { - status: 'api_key_only', - method: 'api-key-only', - recommendation: 'OPENAI_API_KEY detected but Codex CLI not installed. Install Codex CLI for full agentic capabilities.', - installCommands: this.getInstallCommands() - }; - } - - return { - status: 'not_installed', - recommendation: 'Install OpenAI Codex CLI to use GPT-5.1 Codex models for agentic tasks', - installCommands: this.getInstallCommands() - }; - } - - /** - * Get installation commands for different platforms - * @returns {Object} Installation commands by platform - */ - static getInstallCommands() { - return { - npm: 'npm install -g @openai/codex@latest', - macos: 'brew install codex', - linux: 'npm install -g @openai/codex@latest', - windows: 'npm install -g @openai/codex@latest' - }; - } - - /** - * Check if Codex CLI supports a specific model - * @param {string} model Model name to check - * @returns {boolean} Whether the model is supported - */ - static isModelSupported(model) { - const supportedModels = [ - 'gpt-5.1-codex-max', - 'gpt-5.1-codex', - 'gpt-5.1-codex-mini', - 'gpt-5.1' - ]; - return supportedModels.includes(model); - } - - /** - * Get default model for Codex CLI - * @returns {string} Default model name - */ - static getDefaultModel() { - return 'gpt-5.1-codex-max'; - } - - /** - * Get comprehensive installation info including auth status - * @returns {Object} Full status object - */ - static getFullStatus() { - const installation = this.detectCodexInstallation(); - const auth = this.checkAuth(); - const info = this.getInstallationInfo(); - - return { - ...info, - auth, - installation - }; - } - - /** - * Install Codex CLI using npm - * @param {Function} onProgress Callback for progress updates - * @returns {Promise} Installation result - */ - static async installCli(onProgress) { - return new Promise((resolve, reject) => { - const command = 'npm'; - const args = ['install', '-g', '@openai/codex@latest']; - - const proc = spawn(command, args, { - stdio: ['pipe', 'pipe', 'pipe'], - shell: true - }); - - let output = ''; - let errorOutput = ''; - - proc.stdout.on('data', (data) => { - const text = data.toString(); - output += text; - if (onProgress) { - onProgress({ type: 'stdout', data: text }); - } - }); - - proc.stderr.on('data', (data) => { - const text = data.toString(); - errorOutput += text; - // npm often outputs progress to stderr - if (onProgress) { - onProgress({ type: 'stderr', data: text }); - } - }); - - proc.on('close', (code) => { - if (code === 0) { - resolve({ - success: true, - output, - message: 'Codex CLI installed successfully' - }); - } else { - reject({ - success: false, - error: errorOutput || `Installation failed with code ${code}`, - output - }); - } - }); - - proc.on('error', (error) => { - reject({ - success: false, - error: error.message, - output - }); - }); - }); - } - - /** - * Authenticate Codex CLI - opens browser for OAuth or stores API key - * @param {string} apiKey Optional API key to store - * @param {Function} onProgress Callback for progress updates - * @returns {Promise} Authentication result - */ - static async authenticate(apiKey, onProgress) { - return new Promise((resolve, reject) => { - const detection = this.detectCodexInstallation(); - - if (!detection.installed) { - reject({ - success: false, - error: 'Codex CLI is not installed' - }); - return; - } - - const codexPath = detection.path || 'codex'; - - if (apiKey) { - // Store API key directly using codex auth command - const proc = spawn(codexPath, ['auth', 'login', '--api-key', apiKey], { - stdio: ['pipe', 'pipe', 'pipe'], - shell: false - }); - - let output = ''; - let errorOutput = ''; - - proc.stdout.on('data', (data) => { - const text = data.toString(); - output += text; - if (onProgress) { - onProgress({ type: 'stdout', data: text }); - } - }); - - proc.stderr.on('data', (data) => { - const text = data.toString(); - errorOutput += text; - if (onProgress) { - onProgress({ type: 'stderr', data: text }); - } - }); - - proc.on('close', (code) => { - if (code === 0) { - resolve({ - success: true, - output, - message: 'Codex CLI authenticated successfully' - }); - } else { - reject({ - success: false, - error: errorOutput || `Authentication failed with code ${code}`, - output - }); - } - }); - - proc.on('error', (error) => { - reject({ - success: false, - error: error.message, - output - }); - }); - } else { - // Require manual authentication - if (onProgress) { - onProgress({ - type: 'info', - data: 'Please run the following command in your terminal to authenticate:\n\ncodex auth login\n\nThen return here to continue setup.' - }); - } - - resolve({ - success: true, - requiresManualAuth: true, - command: `${codexPath} auth login`, - message: 'Please authenticate Codex CLI manually' - }); - } - }); - } -} - -module.exports = CodexCliDetector; diff --git a/apps/app/electron/services/codex-config-manager.js b/apps/app/electron/services/codex-config-manager.js deleted file mode 100644 index ed324917..00000000 --- a/apps/app/electron/services/codex-config-manager.js +++ /dev/null @@ -1,353 +0,0 @@ -/** - * Codex TOML Configuration Manager - * - * Manages Codex CLI's TOML configuration file to add/update MCP server settings. - * Codex CLI looks for config at: - * - ~/.codex/config.toml (user-level) - * - .codex/config.toml (project-level, takes precedence) - */ - -const fs = require('fs/promises'); -const path = require('path'); -const os = require('os'); - -class CodexConfigManager { - constructor() { - this.userConfigPath = path.join(os.homedir(), '.codex', 'config.toml'); - this.projectConfigPath = null; // Will be set per project - } - - /** - * Set the project path for project-level config - */ - setProjectPath(projectPath) { - this.projectConfigPath = path.join(projectPath, '.codex', 'config.toml'); - } - - /** - * Get the effective config path (project-level if exists, otherwise user-level) - */ - async getConfigPath() { - if (this.projectConfigPath) { - try { - await fs.access(this.projectConfigPath); - return this.projectConfigPath; - } catch (e) { - // Project config doesn't exist, fall back to user config - } - } - - // Ensure user config directory exists - const userConfigDir = path.dirname(this.userConfigPath); - try { - await fs.mkdir(userConfigDir, { recursive: true }); - } catch (e) { - // Directory might already exist - } - - return this.userConfigPath; - } - - /** - * Read existing TOML config (simple parser for our needs) - */ - async readConfig(configPath) { - try { - const content = await fs.readFile(configPath, 'utf-8'); - return this.parseToml(content); - } catch (e) { - if (e.code === 'ENOENT') { - return {}; - } - throw e; - } - } - - /** - * Simple TOML parser for our specific use case - * This is a minimal parser that handles the MCP server config structure - */ - parseToml(content) { - const config = {}; - let currentSection = null; - let currentSubsection = null; - - const lines = content.split('\n'); - - for (const line of lines) { - const trimmed = line.trim(); - - // Skip comments and empty lines - if (!trimmed || trimmed.startsWith('#')) { - continue; - } - - // Section header: [section] - const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/); - if (sectionMatch) { - const sectionName = sectionMatch[1]; - const parts = sectionName.split('.'); - - if (parts.length === 1) { - currentSection = parts[0]; - currentSubsection = null; - if (!config[currentSection]) { - config[currentSection] = {}; - } - } else if (parts.length === 2) { - currentSection = parts[0]; - currentSubsection = parts[1]; - if (!config[currentSection]) { - config[currentSection] = {}; - } - if (!config[currentSection][currentSubsection]) { - config[currentSection][currentSubsection] = {}; - } - } - continue; - } - - // Key-value pair: key = value - const kvMatch = trimmed.match(/^([^=]+)=(.+)$/); - if (kvMatch) { - const key = kvMatch[1].trim(); - let value = kvMatch[2].trim(); - - // Remove quotes if present - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - - // Parse boolean - if (value === 'true') value = true; - else if (value === 'false') value = false; - // Parse number - else if (/^-?\d+$/.test(value)) value = parseInt(value, 10); - else if (/^-?\d+\.\d+$/.test(value)) value = parseFloat(value); - - if (currentSubsection) { - if (!config[currentSection][currentSubsection]) { - config[currentSection][currentSubsection] = {}; - } - config[currentSection][currentSubsection][key] = value; - } else if (currentSection) { - if (!config[currentSection]) { - config[currentSection] = {}; - } - config[currentSection][key] = value; - } else { - config[key] = value; - } - } - } - - return config; - } - - /** - * Convert config object back to TOML format - */ - stringifyToml(config, indent = 0) { - const indentStr = ' '.repeat(indent); - let result = ''; - - for (const [key, value] of Object.entries(config)) { - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - // Section - result += `${indentStr}[${key}]\n`; - result += this.stringifyToml(value, indent); - } else { - // Key-value - let valueStr = value; - if (typeof value === 'string') { - // Escape quotes and wrap in quotes if needed - if (value.includes('"') || value.includes("'") || value.includes(' ')) { - valueStr = `"${value.replace(/"/g, '\\"')}"`; - } - } else if (typeof value === 'boolean') { - valueStr = value.toString(); - } - result += `${indentStr}${key} = ${valueStr}\n`; - } - } - - return result; - } - - /** - * Configure the automaker-tools MCP server - */ - async configureMcpServer(projectPath, mcpServerScriptPath) { - this.setProjectPath(projectPath); - const configPath = await this.getConfigPath(); - - // Read existing config - const config = await this.readConfig(configPath); - - // Ensure mcp_servers section exists - if (!config.mcp_servers) { - config.mcp_servers = {}; - } - - // Configure automaker-tools server - config.mcp_servers['automaker-tools'] = { - command: 'node', - args: [mcpServerScriptPath], - env: { - AUTOMAKER_PROJECT_PATH: projectPath - }, - startup_timeout_sec: 10, - tool_timeout_sec: 60, - enabled_tools: ['UpdateFeatureStatus'] - }; - - // Ensure experimental_use_rmcp_client is enabled (if needed) - if (!config.experimental_use_rmcp_client) { - config.experimental_use_rmcp_client = true; - } - - // Write config back - await this.writeConfig(configPath, config); - - console.log(`[CodexConfigManager] Configured automaker-tools MCP server in ${configPath}`); - return configPath; - } - - /** - * Write config to TOML file - */ - async writeConfig(configPath, config) { - let content = ''; - - // Write top-level keys first (preserve existing non-MCP config) - for (const [key, value] of Object.entries(config)) { - if (key === 'mcp_servers' || key === 'experimental_use_rmcp_client') { - continue; // Handle these separately - } - if (typeof value !== 'object') { - content += `${key} = ${this.formatValue(value)}\n`; - } - } - - // Write experimental flag if enabled - if (config.experimental_use_rmcp_client) { - if (content && !content.endsWith('\n\n')) { - content += '\n'; - } - content += `experimental_use_rmcp_client = true\n`; - } - - // Write mcp_servers section - if (config.mcp_servers && Object.keys(config.mcp_servers).length > 0) { - if (content && !content.endsWith('\n\n')) { - content += '\n'; - } - - for (const [serverName, serverConfig] of Object.entries(config.mcp_servers)) { - content += `\n[mcp_servers.${serverName}]\n`; - - // Write command first - if (serverConfig.command) { - content += `command = "${this.escapeTomlString(serverConfig.command)}"\n`; - } - - // Write args - if (serverConfig.args && Array.isArray(serverConfig.args)) { - const argsStr = serverConfig.args.map(a => `"${this.escapeTomlString(a)}"`).join(', '); - content += `args = [${argsStr}]\n`; - } - - // Write timeouts (must be before env subsection) - if (serverConfig.startup_timeout_sec !== undefined) { - content += `startup_timeout_sec = ${serverConfig.startup_timeout_sec}\n`; - } - - if (serverConfig.tool_timeout_sec !== undefined) { - content += `tool_timeout_sec = ${serverConfig.tool_timeout_sec}\n`; - } - - // Write enabled_tools (must be before env subsection - at server level, not env level) - if (serverConfig.enabled_tools && Array.isArray(serverConfig.enabled_tools)) { - const toolsStr = serverConfig.enabled_tools.map(t => `"${this.escapeTomlString(t)}"`).join(', '); - content += `enabled_tools = [${toolsStr}]\n`; - } - - // Write env section last (as a separate subsection) - // IMPORTANT: In TOML, once we start [mcp_servers.server_name.env], - // everything after belongs to that subsection until a new section starts - if (serverConfig.env && typeof serverConfig.env === 'object' && Object.keys(serverConfig.env).length > 0) { - content += `\n[mcp_servers.${serverName}.env]\n`; - for (const [envKey, envValue] of Object.entries(serverConfig.env)) { - content += `${envKey} = "${this.escapeTomlString(String(envValue))}"\n`; - } - } - } - } - - // Ensure directory exists - const configDir = path.dirname(configPath); - await fs.mkdir(configDir, { recursive: true }); - - // Write file - await fs.writeFile(configPath, content, 'utf-8'); - } - - /** - * Escape special characters in TOML strings - */ - escapeTomlString(str) { - return str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\t/g, '\\t'); - } - - /** - * Format a value for TOML output - */ - formatValue(value) { - if (typeof value === 'string') { - // Escape quotes - const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); - return `"${escaped}"`; - } else if (typeof value === 'boolean') { - return value.toString(); - } else if (typeof value === 'number') { - return value.toString(); - } - return `"${String(value)}"`; - } - - /** - * Remove automaker-tools MCP server configuration - */ - async removeMcpServer(projectPath) { - this.setProjectPath(projectPath); - const configPath = await this.getConfigPath(); - - try { - const config = await this.readConfig(configPath); - - if (config.mcp_servers && config.mcp_servers['automaker-tools']) { - delete config.mcp_servers['automaker-tools']; - - // If no more MCP servers, remove the section - if (Object.keys(config.mcp_servers).length === 0) { - delete config.mcp_servers; - } - - await this.writeConfig(configPath, config); - console.log(`[CodexConfigManager] Removed automaker-tools MCP server from ${configPath}`); - } - } catch (e) { - console.error(`[CodexConfigManager] Error removing MCP server config:`, e); - } - } -} - -module.exports = new CodexConfigManager(); - - diff --git a/apps/app/electron/services/codex-executor.js b/apps/app/electron/services/codex-executor.js deleted file mode 100644 index b051c170..00000000 --- a/apps/app/electron/services/codex-executor.js +++ /dev/null @@ -1,610 +0,0 @@ -/** - * Codex CLI Execution Wrapper - * - * This module handles spawning and managing Codex CLI processes - * for executing OpenAI model queries. - */ - -const { spawn } = require('child_process'); -const { EventEmitter } = require('events'); -const readline = require('readline'); -const path = require('path'); -const CodexCliDetector = require('./codex-cli-detector'); -const codexConfigManager = require('./codex-config-manager'); - -/** - * Message types from Codex CLI JSON output - */ -const CODEX_EVENT_TYPES = { - THREAD_STARTED: 'thread.started', - ITEM_STARTED: 'item.started', - ITEM_COMPLETED: 'item.completed', - THREAD_COMPLETED: 'thread.completed', - ERROR: 'error' -}; - -/** - * Codex Executor - Manages Codex CLI process execution - */ -class CodexExecutor extends EventEmitter { - constructor() { - super(); - this.currentProcess = null; - this.codexPath = null; - } - - /** - * Find and cache the Codex CLI path - * @returns {string|null} Path to codex executable - */ - findCodexPath() { - if (this.codexPath) { - return this.codexPath; - } - - const installation = CodexCliDetector.detectCodexInstallation(); - if (installation.installed && installation.path) { - this.codexPath = installation.path; - return this.codexPath; - } - - return null; - } - - /** - * Execute a Codex CLI query - * @param {Object} options Execution options - * @param {string} options.prompt The prompt to execute - * @param {string} options.model Model to use (default: gpt-5.1-codex-max) - * @param {string} options.cwd Working directory - * @param {string} options.systemPrompt System prompt (optional, will be prepended to prompt) - * @param {number} options.maxTurns Not used - Codex CLI doesn't support this parameter - * @param {string[]} options.allowedTools Not used - Codex CLI doesn't support this parameter - * @param {Object} options.env Environment variables - * @param {Object} options.mcpServers MCP servers configuration (for configuring Codex TOML) - * @returns {AsyncGenerator} Generator yielding messages - */ - async *execute(options) { - const { - prompt, - model = 'gpt-5.1-codex-max', - cwd = process.cwd(), - systemPrompt, - maxTurns, // Not used by Codex CLI - allowedTools, // Not used by Codex CLI - env = {}, - mcpServers = null - } = options; - - const codexPath = this.findCodexPath(); - if (!codexPath) { - yield { - type: 'error', - error: 'Codex CLI not found. Please install it with: npm install -g @openai/codex@latest' - }; - return; - } - - // Configure MCP server if provided - if (mcpServers && mcpServers['automaker-tools']) { - try { - // Get the absolute path to the MCP server script - const mcpServerScriptPath = path.resolve(__dirname, 'mcp-server-stdio.js'); - - // Verify the script exists - const fs = require('fs'); - if (!fs.existsSync(mcpServerScriptPath)) { - console.warn(`[CodexExecutor] MCP server script not found at ${mcpServerScriptPath}, skipping MCP configuration`); - } else { - // Configure Codex TOML to use the MCP server - await codexConfigManager.configureMcpServer(cwd, mcpServerScriptPath); - console.log('[CodexExecutor] Configured automaker-tools MCP server for Codex CLI'); - } - } catch (error) { - console.error('[CodexExecutor] Failed to configure MCP server:', error); - // Continue execution even if MCP config fails - Codex will work without MCP tools - } - } - - // Combine system prompt with main prompt if provided - // Codex CLI doesn't support --system-prompt argument, so we prepend it to the prompt - let combinedPrompt = prompt; - console.log('[CodexExecutor] Original prompt length:', prompt?.length || 0); - if (systemPrompt) { - combinedPrompt = `${systemPrompt}\n\n---\n\n${prompt}`; - console.log('[CodexExecutor] System prompt prepended to main prompt'); - console.log('[CodexExecutor] System prompt length:', systemPrompt.length); - console.log('[CodexExecutor] Combined prompt length:', combinedPrompt.length); - } - - // Build command arguments - // Note: maxTurns and allowedTools are not supported by Codex CLI - console.log('[CodexExecutor] Building command arguments...'); - const args = this.buildArgs({ - prompt: combinedPrompt, - model - }); - - console.log('[CodexExecutor] Executing command:', codexPath); - console.log('[CodexExecutor] Number of args:', args.length); - console.log('[CodexExecutor] Args (without prompt):', args.slice(0, -1).join(' ')); - console.log('[CodexExecutor] Prompt length in args:', args[args.length - 1]?.length || 0); - console.log('[CodexExecutor] Prompt preview (first 200 chars):', args[args.length - 1]?.substring(0, 200)); - console.log('[CodexExecutor] Working directory:', cwd); - - // Spawn the process - const processEnv = { - ...process.env, - ...env, - // Ensure OPENAI_API_KEY is available - OPENAI_API_KEY: env.OPENAI_API_KEY || process.env.OPENAI_API_KEY - }; - - // Log API key status (without exposing the key) - if (processEnv.OPENAI_API_KEY) { - console.log('[CodexExecutor] OPENAI_API_KEY is set (length:', processEnv.OPENAI_API_KEY.length, ')'); - } else { - console.warn('[CodexExecutor] WARNING: OPENAI_API_KEY is not set!'); - } - - console.log('[CodexExecutor] Spawning process...'); - const proc = spawn(codexPath, args, { - cwd, - env: processEnv, - stdio: ['pipe', 'pipe', 'pipe'] - }); - - this.currentProcess = proc; - console.log('[CodexExecutor] Process spawned with PID:', proc.pid); - - // Track process events - proc.on('error', (error) => { - console.error('[CodexExecutor] Process error:', error); - }); - - proc.on('spawn', () => { - console.log('[CodexExecutor] Process spawned successfully'); - }); - - // Collect stderr output as it comes in - let stderr = ''; - let hasOutput = false; - let stdoutChunks = []; - let stderrChunks = []; - - proc.stderr.on('data', (data) => { - const errorText = data.toString(); - stderr += errorText; - stderrChunks.push(errorText); - hasOutput = true; - console.error('[CodexExecutor] stderr chunk received (', data.length, 'bytes):', errorText.substring(0, 200)); - }); - - proc.stderr.on('end', () => { - console.log('[CodexExecutor] stderr stream ended. Total chunks:', stderrChunks.length, 'Total length:', stderr.length); - }); - - proc.stdout.on('data', (data) => { - const text = data.toString(); - stdoutChunks.push(text); - hasOutput = true; - console.log('[CodexExecutor] stdout chunk received (', data.length, 'bytes):', text.substring(0, 200)); - }); - - proc.stdout.on('end', () => { - console.log('[CodexExecutor] stdout stream ended. Total chunks:', stdoutChunks.length); - }); - - // Create readline interface for parsing JSONL output - console.log('[CodexExecutor] Creating readline interface...'); - const rl = readline.createInterface({ - input: proc.stdout, - crlfDelay: Infinity - }); - - // Track accumulated content for converting to Claude format - let accumulatedText = ''; - let toolUses = []; - let lastOutputTime = Date.now(); - const OUTPUT_TIMEOUT = 30000; // 30 seconds timeout for no output - let lineCount = 0; - let jsonParseErrors = 0; - - // Set up timeout check - const checkTimeout = setInterval(() => { - const timeSinceLastOutput = Date.now() - lastOutputTime; - if (timeSinceLastOutput > OUTPUT_TIMEOUT && !hasOutput) { - console.warn('[CodexExecutor] No output received for', timeSinceLastOutput, 'ms. Process still alive:', !proc.killed); - } - }, 5000); - - console.log('[CodexExecutor] Starting to read lines from stdout...'); - - // Process stdout line by line (JSONL format) - try { - for await (const line of rl) { - hasOutput = true; - lastOutputTime = Date.now(); - lineCount++; - - console.log('[CodexExecutor] Line', lineCount, 'received (length:', line.length, '):', line.substring(0, 100)); - - if (!line.trim()) { - console.log('[CodexExecutor] Skipping empty line'); - continue; - } - - try { - const event = JSON.parse(line); - console.log('[CodexExecutor] Successfully parsed JSON event. Type:', event.type, 'Keys:', Object.keys(event)); - - const convertedMsg = this.convertToClaudeFormat(event); - console.log('[CodexExecutor] Converted message:', convertedMsg ? { type: convertedMsg.type } : 'null'); - - if (convertedMsg) { - // Accumulate text content - if (convertedMsg.type === 'assistant' && convertedMsg.message?.content) { - for (const block of convertedMsg.message.content) { - if (block.type === 'text') { - accumulatedText += block.text; - console.log('[CodexExecutor] Accumulated text block (total length:', accumulatedText.length, ')'); - } else if (block.type === 'tool_use') { - toolUses.push(block); - console.log('[CodexExecutor] Tool use detected:', block.name); - } - } - } - console.log('[CodexExecutor] Yielding message of type:', convertedMsg.type); - yield convertedMsg; - } else { - console.log('[CodexExecutor] Converted message is null, skipping'); - } - } catch (parseError) { - jsonParseErrors++; - // Non-JSON output, yield as text - console.log('[CodexExecutor] JSON parse error (', jsonParseErrors, 'total):', parseError.message); - console.log('[CodexExecutor] Non-JSON line content:', line.substring(0, 200)); - yield { - type: 'assistant', - message: { - content: [{ type: 'text', text: line + '\n' }] - } - }; - } - } - - console.log('[CodexExecutor] Finished reading all lines. Total lines:', lineCount, 'JSON errors:', jsonParseErrors); - } catch (readError) { - console.error('[CodexExecutor] Error reading from readline:', readError); - throw readError; - } finally { - clearInterval(checkTimeout); - console.log('[CodexExecutor] Cleaned up timeout checker'); - } - - // Handle process completion - console.log('[CodexExecutor] Waiting for process to close...'); - const exitCode = await new Promise((resolve) => { - proc.on('close', (code, signal) => { - console.log('[CodexExecutor] Process closed with code:', code, 'signal:', signal); - resolve(code); - }); - }); - - this.currentProcess = null; - console.log('[CodexExecutor] Process completed. Exit code:', exitCode, 'Has output:', hasOutput, 'Stderr length:', stderr.length); - - // Wait a bit for any remaining stderr data to be collected - console.log('[CodexExecutor] Waiting 200ms for any remaining stderr data...'); - await new Promise(resolve => setTimeout(resolve, 200)); - console.log('[CodexExecutor] Final stderr length:', stderr.length, 'Final stdout chunks:', stdoutChunks.length); - - if (exitCode !== 0) { - const errorMessage = stderr.trim() - ? `Codex CLI exited with code ${exitCode}.\n\nError output:\n${stderr}` - : `Codex CLI exited with code ${exitCode}. No error output captured.`; - - console.error('[CodexExecutor] Process failed with exit code', exitCode); - console.error('[CodexExecutor] Error message:', errorMessage); - console.error('[CodexExecutor] Stderr chunks:', stderrChunks.length, 'Stdout chunks:', stdoutChunks.length); - - yield { - type: 'error', - error: errorMessage - }; - } else if (!hasOutput && !stderr) { - // Process exited successfully but produced no output - might be API key issue - const warningMessage = 'Codex CLI completed but produced no output. This might indicate:\n' + - '- Missing or invalid OPENAI_API_KEY\n' + - '- Codex CLI configuration issue\n' + - '- The process completed without generating any response\n\n' + - `Debug info: Exit code ${exitCode}, stdout chunks: ${stdoutChunks.length}, stderr chunks: ${stderrChunks.length}, lines read: ${lineCount}`; - - console.warn('[CodexExecutor] No output detected:', warningMessage); - console.warn('[CodexExecutor] Stdout chunks:', stdoutChunks); - console.warn('[CodexExecutor] Stderr chunks:', stderrChunks); - - yield { - type: 'error', - error: warningMessage - }; - } else { - console.log('[CodexExecutor] Process completed successfully. Exit code:', exitCode, 'Lines processed:', lineCount); - } - } - - /** - * Build command arguments for Codex CLI - * Only includes supported arguments based on Codex CLI help: - * - --model: Model to use - * - --json: JSON output format - * - --full-auto: Non-interactive automatic execution - * - * Note: Codex CLI does NOT support: - * - --system-prompt (system prompt is prepended to main prompt) - * - --max-turns (not available in CLI) - * - --tools (not available in CLI) - * - * @param {Object} options Options - * @returns {string[]} Command arguments - */ - buildArgs(options) { - const { prompt, model } = options; - - console.log('[CodexExecutor] buildArgs called with model:', model, 'prompt length:', prompt?.length || 0); - - const args = ['exec']; - - // Add model (required for most use cases) - if (model) { - args.push('--model', model); - console.log('[CodexExecutor] Added model argument:', model); - } - - // Add JSON output flag for structured parsing - args.push('--json'); - console.log('[CodexExecutor] Added --json flag'); - - // Add full-auto mode (non-interactive) - // This enables automatic execution with workspace-write sandbox - args.push('--full-auto'); - console.log('[CodexExecutor] Added --full-auto flag'); - - // Add the prompt at the end - args.push(prompt); - console.log('[CodexExecutor] Added prompt (length:', prompt?.length || 0, ')'); - - console.log('[CodexExecutor] Final args count:', args.length); - return args; - } - - /** - * Map Claude tool names to Codex tool names - * @param {string[]} tools Array of tool names - * @returns {string[]} Mapped tool names - */ - mapToolsToCodex(tools) { - const toolMap = { - 'Read': 'read', - 'Write': 'write', - 'Edit': 'edit', - 'Bash': 'bash', - 'Glob': 'glob', - 'Grep': 'grep', - 'WebSearch': 'web-search', - 'WebFetch': 'web-fetch' - }; - - return tools - .map(tool => toolMap[tool] || tool.toLowerCase()) - .filter(tool => tool); // Remove undefined - } - - /** - * Convert Codex JSONL event to Claude SDK message format - * @param {Object} event Codex event object - * @returns {Object|null} Claude-format message or null - */ - convertToClaudeFormat(event) { - console.log('[CodexExecutor] Converting event:', JSON.stringify(event).substring(0, 200)); - const { type, data, item, thread_id } = event; - - switch (type) { - case CODEX_EVENT_TYPES.THREAD_STARTED: - case 'thread.started': - // Session initialization - return { - type: 'session_start', - sessionId: thread_id || data?.thread_id || event.thread_id - }; - - case CODEX_EVENT_TYPES.ITEM_COMPLETED: - case 'item.completed': - // Codex uses 'item' field, not 'data' - return this.convertItemCompleted(item || data); - - case CODEX_EVENT_TYPES.ITEM_STARTED: - case 'item.started': - // Convert item.started events - these indicate tool/command usage - const startedItem = item || data; - if (startedItem?.type === 'command_execution' && startedItem?.command) { - return { - type: 'assistant', - message: { - content: [{ - type: 'tool_use', - name: 'bash', - input: { command: startedItem.command } - }] - } - }; - } - // For other item.started types, return null (we'll show the completed version) - return null; - - case CODEX_EVENT_TYPES.THREAD_COMPLETED: - case 'thread.completed': - return { - type: 'complete', - sessionId: thread_id || data?.thread_id || event.thread_id - }; - - case CODEX_EVENT_TYPES.ERROR: - case 'error': - return { - type: 'error', - error: data?.message || item?.message || event.message || 'Unknown error from Codex CLI' - }; - - case 'turn.started': - // Turn started - just a marker, no need to convert - return null; - - default: - // Pass through other events - console.log('[CodexExecutor] Unhandled event type:', type); - return null; - } - } - - /** - * Convert item.completed event to Claude format - * @param {Object} item Event item data - * @returns {Object|null} Claude-format message - */ - convertItemCompleted(item) { - if (!item) { - console.log('[CodexExecutor] convertItemCompleted: item is null/undefined'); - return null; - } - - const itemType = item.type || item.item_type; - console.log('[CodexExecutor] convertItemCompleted: itemType =', itemType, 'item keys:', Object.keys(item)); - - switch (itemType) { - case 'reasoning': - // Thinking/reasoning output - Codex uses 'text' field - const reasoningText = item.text || item.content || ''; - console.log('[CodexExecutor] Converting reasoning, text length:', reasoningText.length); - return { - type: 'assistant', - message: { - content: [{ - type: 'thinking', - thinking: reasoningText - }] - } - }; - - case 'agent_message': - case 'message': - // Assistant text message - const messageText = item.content || item.text || ''; - console.log('[CodexExecutor] Converting message, text length:', messageText.length); - return { - type: 'assistant', - message: { - content: [{ - type: 'text', - text: messageText - }] - } - }; - - case 'command_execution': - // Command execution - show both the command and its output - const command = item.command || ''; - const output = item.aggregated_output || item.output || ''; - console.log('[CodexExecutor] Converting command_execution, command:', command.substring(0, 50), 'output length:', output.length); - - // Return as text message showing the command and output - return { - type: 'assistant', - message: { - content: [{ - type: 'text', - text: `\`\`\`bash\n${command}\n\`\`\`\n\n${output}` - }] - } - }; - - case 'tool_use': - // Tool use - return { - type: 'assistant', - message: { - content: [{ - type: 'tool_use', - name: item.tool || item.command || 'unknown', - input: item.input || item.args || {} - }] - } - }; - - case 'tool_result': - // Tool result - return { - type: 'tool_result', - tool_use_id: item.tool_use_id, - content: item.output || item.result - }; - - case 'todo_list': - // Todo list - convert to text format - const todos = item.items || []; - const todoText = todos.map((t, i) => `${i + 1}. ${t.text || t}`).join('\n'); - console.log('[CodexExecutor] Converting todo_list, items:', todos.length); - return { - type: 'assistant', - message: { - content: [{ - type: 'text', - text: `**Todo List:**\n${todoText}` - }] - } - }; - - default: - // Generic text output - const text = item.text || item.content || item.aggregated_output; - if (text) { - console.log('[CodexExecutor] Converting default item type, text length:', text.length); - return { - type: 'assistant', - message: { - content: [{ - type: 'text', - text: String(text) - }] - } - }; - } - console.log('[CodexExecutor] convertItemCompleted: No text content found, returning null'); - return null; - } - } - - /** - * Abort current execution - */ - abort() { - if (this.currentProcess) { - console.log('[CodexExecutor] Aborting current process'); - this.currentProcess.kill('SIGTERM'); - this.currentProcess = null; - } - } - - /** - * Check if execution is in progress - * @returns {boolean} Whether execution is in progress - */ - isRunning() { - return this.currentProcess !== null; - } -} - -// Singleton instance -const codexExecutor = new CodexExecutor(); - -module.exports = codexExecutor; diff --git a/apps/app/electron/services/context-manager.js b/apps/app/electron/services/context-manager.js deleted file mode 100644 index c9f08ab4..00000000 --- a/apps/app/electron/services/context-manager.js +++ /dev/null @@ -1,452 +0,0 @@ -const path = require("path"); -const fs = require("fs/promises"); - -/** - * Context Manager - Handles reading, writing, and deleting context files for features - */ -class ContextManager { - /** - * Write output to feature context file - */ - async writeToContextFile(projectPath, featureId, content) { - if (!projectPath) return; - - try { - const featureDir = path.join( - projectPath, - ".automaker", - "features", - featureId - ); - - // Ensure feature directory exists - try { - await fs.access(featureDir); - } catch { - await fs.mkdir(featureDir, { recursive: true }); - } - - const filePath = path.join(featureDir, "agent-output.md"); - - // Append to existing file or create new one - try { - const existing = await fs.readFile(filePath, "utf-8"); - await fs.writeFile(filePath, existing + content, "utf-8"); - } catch { - await fs.writeFile(filePath, content, "utf-8"); - } - } catch (error) { - console.error("[ContextManager] Failed to write to context file:", error); - } - } - - /** - * Read context file for a feature - */ - async readContextFile(projectPath, featureId) { - try { - const contextPath = path.join( - projectPath, - ".automaker", - "features", - featureId, - "agent-output.md" - ); - const content = await fs.readFile(contextPath, "utf-8"); - return content; - } catch (error) { - console.log(`[ContextManager] No context file found for ${featureId}`); - return null; - } - } - - /** - * Delete agent context file for a feature - */ - async deleteContextFile(projectPath, featureId) { - if (!projectPath) return; - - try { - const contextPath = path.join( - projectPath, - ".automaker", - "features", - featureId, - "agent-output.md" - ); - await fs.unlink(contextPath); - console.log( - `[ContextManager] Deleted agent context for feature ${featureId}` - ); - } catch (error) { - // File might not exist, which is fine - if (error.code !== "ENOENT") { - console.error("[ContextManager] Failed to delete context file:", error); - } - } - } - - /** - * Read the memory.md file containing lessons learned and common issues - * Returns formatted string to inject into prompts - */ - async getMemoryContent(projectPath) { - if (!projectPath) return ""; - - try { - const memoryPath = path.join(projectPath, ".automaker", "memory.md"); - - // Check if file exists - try { - await fs.access(memoryPath); - } catch { - // File doesn't exist, return empty string - return ""; - } - - const content = await fs.readFile(memoryPath, "utf-8"); - - if (!content.trim()) { - return ""; - } - - return ` -**🧠 Agent Memory - Previous Lessons Learned:** - -The following memory file contains lessons learned from previous agent runs, including common issues and their solutions. Review this carefully to avoid repeating past mistakes. - - -${content} - - -**IMPORTANT:** If you encounter a new issue that took significant debugging effort to resolve, add it to the memory file at \`.automaker/memory.md\` in a concise format: -- Issue title -- Problem description (1-2 sentences) -- Solution/fix (with code example if helpful) - -This helps future agent runs avoid the same pitfalls. -`; - } catch (error) { - console.error("[ContextManager] Failed to read memory file:", error); - return ""; - } - } - - /** - * List context files from .automaker/context/ directory and get previews - * Returns a formatted string with file names and first 50 lines of each file - */ - async getContextFilesPreview(projectPath) { - if (!projectPath) return ""; - - try { - const contextDir = path.join(projectPath, ".automaker", "context"); - - // Check if directory exists - try { - await fs.access(contextDir); - } catch { - // Directory doesn't exist, return empty string - return ""; - } - - // Read directory contents - const entries = await fs.readdir(contextDir, { withFileTypes: true }); - const files = entries - .filter((entry) => entry.isFile()) - .map((entry) => entry.name) - .sort(); - - if (files.length === 0) { - return ""; - } - - // Build preview string - const previews = []; - previews.push(`\n**šŸ“ Context Files Available:**\n`); - previews.push( - `The following context files are available in \`.automaker/context/\` directory.` - ); - previews.push( - `These files contain additional context that may be relevant to your work.` - ); - previews.push( - `You can read them in full using the Read tool if needed.\n` - ); - - for (const fileName of files) { - try { - const filePath = path.join(contextDir, fileName); - const content = await fs.readFile(filePath, "utf-8"); - const lines = content.split("\n"); - const previewLines = lines.slice(0, 50); - const preview = previewLines.join("\n"); - const hasMore = lines.length > 50; - - previews.push(`\n**File: ${fileName}**`); - if (hasMore) { - previews.push( - `(Showing first 50 of ${lines.length} lines - use Read tool to see full content)` - ); - } - previews.push(`\`\`\``); - previews.push(preview); - previews.push(`\`\`\`\n`); - } catch (error) { - console.error( - `[ContextManager] Failed to read context file ${fileName}:`, - error - ); - previews.push(`\n**File: ${fileName}** (Error reading file)\n`); - } - } - - return previews.join("\n"); - } catch (error) { - console.error("[ContextManager] Failed to list context files:", error); - return ""; - } - } - - /** - * Save the initial git state before a feature starts executing - * This captures all files that were already modified before the AI agent started - * @param {string} projectPath - Path to the project - * @param {string} featureId - Feature ID - * @returns {Promise<{modifiedFiles: string[], untrackedFiles: string[]}>} - */ - async saveInitialGitState(projectPath, featureId) { - if (!projectPath) return { modifiedFiles: [], untrackedFiles: [] }; - - try { - const { execSync } = require("child_process"); - const featureDir = path.join( - projectPath, - ".automaker", - "features", - featureId - ); - - // Ensure feature directory exists - try { - await fs.access(featureDir); - } catch { - await fs.mkdir(featureDir, { recursive: true }); - } - - // Get list of modified files (both staged and unstaged) - let modifiedFiles = []; - try { - const modifiedOutput = execSync("git diff --name-only HEAD", { - cwd: projectPath, - encoding: "utf-8", - }).trim(); - if (modifiedOutput) { - modifiedFiles = modifiedOutput.split("\n").filter(Boolean); - } - } catch (error) { - console.log( - "[ContextManager] No modified files or git error:", - error.message - ); - } - - // Get list of untracked files - let untrackedFiles = []; - try { - const untrackedOutput = execSync( - "git ls-files --others --exclude-standard", - { - cwd: projectPath, - encoding: "utf-8", - } - ).trim(); - if (untrackedOutput) { - untrackedFiles = untrackedOutput.split("\n").filter(Boolean); - } - } catch (error) { - console.log( - "[ContextManager] Error getting untracked files:", - error.message - ); - } - - // Save the initial state to a JSON file - const stateFile = path.join(featureDir, "git-state.json"); - const state = { - timestamp: new Date().toISOString(), - modifiedFiles, - untrackedFiles, - }; - - await fs.writeFile(stateFile, JSON.stringify(state, null, 2), "utf-8"); - console.log( - `[ContextManager] Saved initial git state for ${featureId}:`, - { - modifiedCount: modifiedFiles.length, - untrackedCount: untrackedFiles.length, - } - ); - - return state; - } catch (error) { - console.error( - "[ContextManager] Failed to save initial git state:", - error - ); - return { modifiedFiles: [], untrackedFiles: [] }; - } - } - - /** - * Get the initial git state saved before a feature started executing - * @param {string} projectPath - Path to the project - * @param {string} featureId - Feature ID - * @returns {Promise<{modifiedFiles: string[], untrackedFiles: string[], timestamp: string} | null>} - */ - async getInitialGitState(projectPath, featureId) { - if (!projectPath) return null; - - try { - const stateFile = path.join( - projectPath, - ".automaker", - "features", - featureId, - "git-state.json" - ); - const content = await fs.readFile(stateFile, "utf-8"); - return JSON.parse(content); - } catch (error) { - console.log( - `[ContextManager] No initial git state found for ${featureId}` - ); - return null; - } - } - - /** - * Delete the git state file for a feature - * @param {string} projectPath - Path to the project - * @param {string} featureId - Feature ID - */ - async deleteGitStateFile(projectPath, featureId) { - if (!projectPath) return; - - try { - const stateFile = path.join( - projectPath, - ".automaker", - "features", - featureId, - "git-state.json" - ); - await fs.unlink(stateFile); - console.log(`[ContextManager] Deleted git state file for ${featureId}`); - } catch (error) { - // File might not exist, which is fine - if (error.code !== "ENOENT") { - console.error( - "[ContextManager] Failed to delete git state file:", - error - ); - } - } - } - - /** - * Calculate which files were changed during the AI session - * by comparing current git state with the saved initial state - * @param {string} projectPath - Path to the project - * @param {string} featureId - Feature ID - * @returns {Promise<{newFiles: string[], modifiedFiles: string[]}>} - */ - async getFilesChangedDuringSession(projectPath, featureId) { - if (!projectPath) return { newFiles: [], modifiedFiles: [] }; - - try { - const { execSync } = require("child_process"); - - // Get initial state - const initialState = await this.getInitialGitState( - projectPath, - featureId - ); - - // Get current state - let currentModified = []; - try { - const modifiedOutput = execSync("git diff --name-only HEAD", { - cwd: projectPath, - encoding: "utf-8", - }).trim(); - if (modifiedOutput) { - currentModified = modifiedOutput.split("\n").filter(Boolean); - } - } catch (error) { - console.log("[ContextManager] No modified files or git error"); - } - - let currentUntracked = []; - try { - const untrackedOutput = execSync( - "git ls-files --others --exclude-standard", - { - cwd: projectPath, - encoding: "utf-8", - } - ).trim(); - if (untrackedOutput) { - currentUntracked = untrackedOutput.split("\n").filter(Boolean); - } - } catch (error) { - console.log("[ContextManager] Error getting untracked files"); - } - - if (!initialState) { - // No initial state - all current changes are considered from this session - console.log( - "[ContextManager] No initial state found, returning all current changes" - ); - return { - newFiles: currentUntracked, - modifiedFiles: currentModified, - }; - } - - // Calculate files that are new since the session started - const initialModifiedSet = new Set(initialState.modifiedFiles || []); - const initialUntrackedSet = new Set(initialState.untrackedFiles || []); - - // New files = current untracked - initial untracked - const newFiles = currentUntracked.filter( - (f) => !initialUntrackedSet.has(f) - ); - - // Modified files = current modified - initial modified - const modifiedFiles = currentModified.filter( - (f) => !initialModifiedSet.has(f) - ); - - console.log( - `[ContextManager] Files changed during session for ${featureId}:`, - { - newFilesCount: newFiles.length, - modifiedFilesCount: modifiedFiles.length, - newFiles, - modifiedFiles, - } - ); - - return { newFiles, modifiedFiles }; - } catch (error) { - console.error( - "[ContextManager] Failed to calculate changed files:", - error - ); - return { newFiles: [], modifiedFiles: [] }; - } - } -} - -module.exports = new ContextManager(); diff --git a/apps/app/electron/services/feature-executor.js b/apps/app/electron/services/feature-executor.js deleted file mode 100644 index d41d72bf..00000000 --- a/apps/app/electron/services/feature-executor.js +++ /dev/null @@ -1,1269 +0,0 @@ -const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); -const promptBuilder = require("./prompt-builder"); -const contextManager = require("./context-manager"); -const featureLoader = require("./feature-loader"); -const mcpServerFactory = require("./mcp-server-factory"); -const { ModelRegistry } = require("./model-registry"); -const { ModelProviderFactory } = require("./model-provider"); - -// Model name mappings for Claude (legacy - kept for backwards compatibility) -const MODEL_MAP = { - haiku: "claude-haiku-4-5", - sonnet: "claude-sonnet-4-20250514", - opus: "claude-opus-4-5-20251101", -}; - -// Thinking level to budget_tokens mapping -// These values control how much "thinking time" the model gets for extended thinking -const THINKING_BUDGET_MAP = { - none: null, // No extended thinking - low: 4096, // Light thinking - medium: 16384, // Moderate thinking - high: 65536, // Deep thinking - ultrathink: 262144, // Ultra-deep thinking (maximum reasoning) -}; - -/** - * Feature Executor - Handles feature implementation using Claude Agent SDK - * Now supports multiple model providers (Claude, Codex/OpenAI) - */ -class FeatureExecutor { - /** - * Get the model string based on feature's model setting - * Supports both Claude and Codex/OpenAI models - */ - getModelString(feature) { - const modelKey = feature.model || "opus"; // Default to opus - - // First check if this is a Codex model - they use the model key directly as the string - if (ModelRegistry.isCodexModel(modelKey)) { - const model = ModelRegistry.getModel(modelKey); - if (model && model.modelString) { - console.log( - `[FeatureExecutor] getModelString: modelKey=${modelKey}, modelString=${model.modelString} (Codex model)` - ); - return model.modelString; - } - // If model exists in registry but somehow no modelString, use the key itself - console.log( - `[FeatureExecutor] getModelString: modelKey=${modelKey}, modelString=${modelKey} (Codex fallback)` - ); - return modelKey; - } - - // For Claude models, use the registry lookup - let modelString = ModelRegistry.getModelString(modelKey); - - // Fallback to MODEL_MAP if registry doesn't have it (legacy support) - if (!modelString) { - modelString = MODEL_MAP[modelKey]; - } - - // Final fallback to opus for Claude models only - if (!modelString) { - modelString = MODEL_MAP.opus; - } - - // Validate model string format - ensure it's not incorrectly constructed - // Prevent incorrect formats like "claude-haiku-4-20250514" (mixing haiku with sonnet date) - if (modelString.includes("haiku") && modelString.includes("20250514")) { - console.error( - `[FeatureExecutor] Invalid model string detected: ${modelString}, using correct format` - ); - modelString = MODEL_MAP.haiku || "claude-haiku-4-5"; - } - - console.log( - `[FeatureExecutor] getModelString: modelKey=${modelKey}, modelString=${modelString}` - ); - return modelString; - } - - /** - * Determine if the feature uses a Codex/OpenAI model - */ - isCodexModel(feature) { - const modelKey = feature.model || "opus"; - return ModelRegistry.isCodexModel(modelKey); - } - - /** - * Get the appropriate provider for the feature's model - */ - getProvider(feature) { - const modelKey = feature.model || "opus"; - return ModelProviderFactory.getProviderForModel(modelKey); - } - - /** - * Get thinking configuration based on feature's thinkingLevel - */ - getThinkingConfig(feature) { - const modelId = feature.model || "opus"; - // Skip thinking config for models that don't support it (e.g., Codex CLI) - if (!ModelRegistry.modelSupportsThinking(modelId)) { - return null; - } - - const level = feature.thinkingLevel || "none"; - const budgetTokens = THINKING_BUDGET_MAP[level]; - - if (budgetTokens === null) { - return null; // No extended thinking - } - - return { - type: "enabled", - budget_tokens: budgetTokens, - }; - } - - /** - * Prepare for ultrathink execution - validate and warn - */ - prepareForUltrathink(feature, thinkingConfig) { - if (feature.thinkingLevel !== "ultrathink") { - return { ready: true }; - } - - const warnings = []; - const recommendations = []; - - // Check CLI installation - const claudeCliDetector = require("./claude-cli-detector"); - const cliInfo = claudeCliDetector.getInstallationInfo(); - - if (cliInfo.status === "not_installed") { - warnings.push( - "Claude Code CLI not detected - ultrathink may have timeout issues" - ); - recommendations.push( - "Install Claude Code CLI for optimal ultrathink performance" - ); - } - - // Validate budget tokens - if (thinkingConfig && thinkingConfig.budget_tokens > 32000) { - warnings.push( - `Ultrathink budget (${thinkingConfig.budget_tokens} tokens) exceeds recommended 32K - may cause long-running requests` - ); - recommendations.push( - "Consider using batch processing for budgets above 32K" - ); - } - - // Cost estimate (rough) - const estimatedCost = ((thinkingConfig?.budget_tokens || 0) / 1000) * 0.015; // Rough estimate - if (estimatedCost > 1.0) { - warnings.push( - `Estimated cost: ~$${estimatedCost.toFixed(2)} per execution` - ); - } - - // Time estimate - warnings.push("Ultrathink tasks typically take 45-180 seconds"); - - return { - ready: true, - warnings, - recommendations, - estimatedCost, - estimatedTime: "45-180 seconds", - cliInfo, - }; - } - - /** - * Sleep helper - */ - sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - /** - * Implement a single feature using Claude Agent SDK - * Uses a Plan-Act-Verify loop with detailed phase logging - */ - async implementFeature(feature, projectPath, sendToRenderer, execution) { - console.log(`[FeatureExecutor] Implementing: ${feature.description}`); - - // Declare variables outside try block so they're available in catch - let modelString; - let providerName; - let isCodex; - - try { - // Save the initial git state before starting implementation - // This allows us to track only files changed during this session when committing - await contextManager.saveInitialGitState(projectPath, feature.id); - - // ======================================== - // PHASE 1: PLANNING - // ======================================== - const planningMessage = `šŸ“‹ Planning implementation for: ${feature.description}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - planningMessage - ); - - sendToRenderer({ - type: "auto_mode_phase", - featureId: feature.id, - phase: "planning", - message: `Planning implementation for: ${feature.description}`, - }); - console.log( - `[FeatureExecutor] Phase: PLANNING for ${feature.description}` - ); - - const abortController = new AbortController(); - execution.abortController = abortController; - - // Create custom MCP server with UpdateFeatureStatus tool - const featureToolsServer = mcpServerFactory.createFeatureToolsServer( - featureLoader.updateFeatureStatus.bind(featureLoader), - projectPath - ); - - // Ensure feature has a model set (for backward compatibility with old features) - if (!feature.model) { - console.warn( - `[FeatureExecutor] Feature ${feature.id} missing model property, defaulting to 'opus'` - ); - feature.model = "opus"; - } - - // Get model and thinking configuration from feature settings - const modelString = this.getModelString(feature); - const thinkingConfig = this.getThinkingConfig(feature); - - // Prepare for ultrathink if needed - if (feature.thinkingLevel === "ultrathink") { - const preparation = this.prepareForUltrathink(feature, thinkingConfig); - - console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation); - - // Log warnings - if (preparation.warnings && preparation.warnings.length > 0) { - preparation.warnings.forEach((warning) => { - console.warn(`[FeatureExecutor] āš ļø ${warning}`); - }); - } - - // Send preparation info to renderer - sendToRenderer({ - type: "auto_mode_ultrathink_preparation", - featureId: feature.id, - warnings: preparation.warnings || [], - recommendations: preparation.recommendations || [], - estimatedCost: preparation.estimatedCost, - estimatedTime: preparation.estimatedTime, - }); - } - - providerName = this.isCodexModel(feature) ? "Codex/OpenAI" : "Claude"; - console.log( - `[FeatureExecutor] Using provider: ${providerName}, model: ${modelString}, thinking: ${ - feature.thinkingLevel || "none" - }` - ); - - // Note: Claude Agent SDK handles authentication automatically - it can use: - // 1. CLAUDE_CODE_OAUTH_TOKEN env var (for SDK mode) - // 2. Claude CLI's own authentication (if CLI is installed) - // 3. ANTHROPIC_API_KEY (fallback) - // We don't need to validate here - let the SDK/CLI handle auth errors - - // Configure options for the SDK query - const options = { - model: modelString, - systemPrompt: promptBuilder.getCodingPrompt(), - maxTurns: 1000, - cwd: projectPath, - mcpServers: { - "automaker-tools": featureToolsServer, - }, - allowedTools: [ - "Read", - "Write", - "Edit", - "Glob", - "Grep", - "Bash", - "WebSearch", - "WebFetch", - "mcp__automaker-tools__UpdateFeatureStatus", - ], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - // Add thinking configuration if enabled - if (thinkingConfig) { - options.thinking = thinkingConfig; - } - - // Build the prompt for this specific feature - let prompt = await promptBuilder.buildFeaturePrompt(feature, projectPath); - - // Add images to prompt if feature has imagePaths - if (feature.imagePaths && feature.imagePaths.length > 0) { - const contentBlocks = []; - - // Add text block - contentBlocks.push({ - type: "text", - text: prompt, - }); - - // Add image blocks - const fs = require("fs"); - const path = require("path"); - for (const imagePathObj of feature.imagePaths) { - try { - const imagePath = imagePathObj.path; - const imageBuffer = fs.readFileSync(imagePath); - const base64Data = imageBuffer.toString("base64"); - const ext = path.extname(imagePath).toLowerCase(); - const mimeTypeMap = { - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".png": "image/png", - ".gif": "image/gif", - ".webp": "image/webp", - }; - const mediaType = - mimeTypeMap[ext] || imagePathObj.mimeType || "image/png"; - - contentBlocks.push({ - type: "image", - source: { - type: "base64", - media_type: mediaType, - data: base64Data, - }, - }); - - console.log( - `[FeatureExecutor] Added image to prompt: ${imagePath}` - ); - } catch (error) { - console.error( - `[FeatureExecutor] Failed to load image ${imagePathObj.path}:`, - error - ); - } - } - - // Wrap content blocks in async generator for SDK (required format for multimodal prompts) - prompt = (async function* () { - yield { - type: "user", - session_id: "", - message: { - role: "user", - content: contentBlocks, - }, - parent_tool_use_id: null, - }; - })(); - } - - // Planning: Analyze the codebase and create implementation plan - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: - "Analyzing codebase structure and creating implementation plan...", - }); - - // Small delay to show planning phase - await this.sleep(500); - - // ======================================== - // PHASE 2: ACTION - // ======================================== - const actionMessage = `⚔ Executing implementation for: ${feature.description}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - actionMessage - ); - - sendToRenderer({ - type: "auto_mode_phase", - featureId: feature.id, - phase: "action", - message: `Executing implementation for: ${feature.description}`, - }); - console.log(`[FeatureExecutor] Phase: ACTION for ${feature.description}`); - - // Send query - use appropriate provider based on model - let currentQuery; - isCodex = this.isCodexModel(feature); - - // Ensure provider auth is available (especially for Claude SDK) - const provider = this.getProvider(feature); - if (provider?.ensureAuthEnv && !provider.ensureAuthEnv()) { - // Check if CLI is installed to provide better error message - let authMsg = - "Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication."; - try { - const claudeCliDetector = require("./claude-cli-detector"); - const detection = claudeCliDetector.detectClaudeInstallation(); - if (detection.installed && detection.method === "cli") { - authMsg = - "Claude CLI is installed but not authenticated. Go to Settings > Setup to provide your subscription token (from `claude setup-token`) or API key."; - } else { - authMsg = - "Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication, or set ANTHROPIC_API_KEY environment variable."; - } - } catch (err) { - // Fallback to default message - } - console.error(`[FeatureExecutor] ${authMsg}`); - throw new Error(authMsg); - } - - // Validate that model string matches the provider - if (isCodex) { - // Ensure model string is actually a Codex model, not a Claude model - if (modelString.startsWith("claude-")) { - console.error( - `[FeatureExecutor] ERROR: Codex provider selected but Claude model string detected: ${modelString}` - ); - console.error( - `[FeatureExecutor] Feature model: ${ - feature.model || "not set" - }, modelString: ${modelString}` - ); - throw new Error( - `Invalid model configuration: Codex provider cannot use Claude model '${modelString}'. Please check feature model setting.` - ); - } - - // Use Codex provider for OpenAI models - console.log( - `[FeatureExecutor] Using Codex provider for model: ${modelString}` - ); - // Pass MCP server config to Codex provider so it can configure Codex CLI TOML - currentQuery = provider.executeQuery({ - prompt, - model: modelString, - cwd: projectPath, - systemPrompt: promptBuilder.getCodingPrompt(), - maxTurns: 20, // Codex CLI typically uses fewer turns - allowedTools: options.allowedTools, - mcpServers: { - "automaker-tools": featureToolsServer, - }, - abortController: abortController, - env: { - OPENAI_API_KEY: process.env.OPENAI_API_KEY, - }, - }); - } else { - // Ensure model string is actually a Claude model, not a Codex model - if ( - !modelString.startsWith("claude-") && - !modelString.match(/^(gpt-|o\d)/) - ) { - console.warn( - `[FeatureExecutor] WARNING: Claude provider selected but unexpected model string: ${modelString}` - ); - } - - // Use Claude SDK (original implementation) - currentQuery = query({ prompt, options }); - } - - execution.query = currentQuery; - - // Stream responses - let responseText = ""; - let hasStartedToolUse = false; - for await (const msg of currentQuery) { - // Check if this specific feature was aborted - if (!execution.isActive()) break; - - // Handle error messages - if (msg.type === "error") { - const errorMsg = `\nāŒ Error: ${msg.error}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - errorMsg - ); - sendToRenderer({ - type: "auto_mode_error", - featureId: feature.id, - error: msg.error, - }); - throw new Error(msg.error); - } - - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - - // Write to context file - await contextManager.writeToContextFile( - projectPath, - feature.id, - block.text - ); - - // Stream progress to renderer - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: block.text, - }); - } else if (block.type === "thinking") { - // Handle thinking output from Codex O-series models - const thinkingMsg = `\nšŸ’­ Thinking: ${block.thinking?.substring( - 0, - 200 - )}...\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - thinkingMsg - ); - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: thinkingMsg, - }); - } else if (block.type === "tool_use") { - // First tool use indicates we're actively implementing - if (!hasStartedToolUse) { - hasStartedToolUse = true; - const startMsg = "Starting code implementation...\n"; - await contextManager.writeToContextFile( - projectPath, - feature.id, - startMsg - ); - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: startMsg, - }); - } - - // Write tool use to context file - const toolMsg = `\nšŸ”§ Tool: ${block.name}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - toolMsg - ); - - // Notify about tool use - sendToRenderer({ - type: "auto_mode_tool", - featureId: feature.id, - tool: block.name, - input: block.input, - }); - } - } - } - } - - execution.query = null; - execution.abortController = null; - - // ======================================== - // PHASE 3: VERIFICATION - // ======================================== - const verificationMessage = `āœ… Verifying implementation for: ${feature.description}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - verificationMessage - ); - - sendToRenderer({ - type: "auto_mode_phase", - featureId: feature.id, - phase: "verification", - message: `Verifying implementation for: ${feature.description}`, - }); - console.log( - `[FeatureExecutor] Phase: VERIFICATION for ${feature.description}` - ); - - const checkingMsg = - "Verifying implementation and checking test results...\n"; - await contextManager.writeToContextFile( - projectPath, - feature.id, - checkingMsg - ); - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: checkingMsg, - }); - - // Re-load features to check if it was marked as verified or waiting_approval (for skipTests) - const updatedFeatures = await featureLoader.loadFeatures(projectPath); - const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); - // For skipTests features, waiting_approval is also considered a success - const passes = - updatedFeature?.status === "verified" || - (updatedFeature?.skipTests && - updatedFeature?.status === "waiting_approval"); - - // Send verification result - const resultMsg = passes - ? "āœ“ Verification successful: All tests passed\n" - : "āœ— Verification: Tests need attention\n"; - - await contextManager.writeToContextFile( - projectPath, - feature.id, - resultMsg - ); - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: resultMsg, - }); - - return { - passes, - message: responseText.substring(0, 500), // First 500 chars - }; - } catch (error) { - if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[FeatureExecutor] Feature run aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - passes: false, - message: "Auto mode aborted", - }; - } - - console.error("[FeatureExecutor] Error implementing feature:", error); - - // Safely get model info for error logging (may not be set if error occurred early) - const modelInfo = modelString - ? { - message: error.message, - stack: error.stack, - name: error.name, - code: error.code, - model: modelString, - provider: providerName || "unknown", - isCodex: isCodex !== undefined ? isCodex : "unknown", - } - : { - message: error.message, - stack: error.stack, - name: error.name, - code: error.code, - model: "not initialized", - provider: "unknown", - isCodex: "unknown", - }; - - console.error("[FeatureExecutor] Error details:", modelInfo); - - // Check if this is a Claude CLI process error - if (error.message && error.message.includes("process exited with code")) { - const modelDisplay = modelString - ? `Model: ${modelString}` - : "Model: not initialized"; - const errorMsg = - `Claude Code CLI failed with exit code 1. This might be due to:\n` + - `- Invalid or unsupported model (${modelDisplay})\n` + - `- Missing or invalid CLAUDE_CODE_OAUTH_TOKEN\n` + - `- Claude CLI configuration issue\n` + - `- Model not available in your Claude account\n\n` + - `Original error: ${error.message}`; - - await contextManager.writeToContextFile( - projectPath, - feature.id, - `\nāŒ ${errorMsg}\n` - ); - sendToRenderer({ - type: "auto_mode_error", - featureId: feature.id, - error: errorMsg, - }); - } - - // Clean up - if (execution) { - execution.abortController = null; - execution.query = null; - } - - throw error; - } - } - - /** - * Resume feature implementation with previous context - */ - async resumeFeatureWithContext( - feature, - projectPath, - sendToRenderer, - previousContext, - execution - ) { - console.log( - `[FeatureExecutor] Resuming with context for: ${feature.description}` - ); - - try { - const resumeMessage = `\nšŸ”„ Resuming implementation for: ${feature.description}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - resumeMessage - ); - - sendToRenderer({ - type: "auto_mode_phase", - featureId: feature.id, - phase: "action", - message: `Resuming implementation for: ${feature.description}`, - }); - - const abortController = new AbortController(); - execution.abortController = abortController; - - // Determine if we're in TDD mode (skipTests=false means TDD mode) - const isTDD = !feature.skipTests; - - // Create custom MCP server with UpdateFeatureStatus tool - const featureToolsServer = mcpServerFactory.createFeatureToolsServer( - featureLoader.updateFeatureStatus.bind(featureLoader), - projectPath - ); - - // Ensure feature has a model set (for backward compatibility with old features) - if (!feature.model) { - console.warn( - `[FeatureExecutor] Feature ${feature.id} missing model property, defaulting to 'opus'` - ); - feature.model = "opus"; - } - - // Get model and thinking configuration from feature settings - const modelString = this.getModelString(feature); - const thinkingConfig = this.getThinkingConfig(feature); - - // Prepare for ultrathink if needed - if (feature.thinkingLevel === "ultrathink") { - const preparation = this.prepareForUltrathink(feature, thinkingConfig); - - console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation); - - // Log warnings - if (preparation.warnings && preparation.warnings.length > 0) { - preparation.warnings.forEach((warning) => { - console.warn(`[FeatureExecutor] āš ļø ${warning}`); - }); - } - - // Send preparation info to renderer - sendToRenderer({ - type: "auto_mode_ultrathink_preparation", - featureId: feature.id, - warnings: preparation.warnings || [], - recommendations: preparation.recommendations || [], - estimatedCost: preparation.estimatedCost, - estimatedTime: preparation.estimatedTime, - }); - } - - const isCodex = this.isCodexModel(feature); - const providerName = isCodex ? "Codex/OpenAI" : "Claude"; - console.log( - `[FeatureExecutor] Resuming with provider: ${providerName}, model: ${modelString}, thinking: ${ - feature.thinkingLevel || "none" - }` - ); - - const options = { - model: modelString, - systemPrompt: promptBuilder.getVerificationPrompt(), - maxTurns: 1000, - cwd: projectPath, - mcpServers: { - "automaker-tools": featureToolsServer, - }, - allowedTools: [ - "Read", - "Write", - "Edit", - "Glob", - "Grep", - "Bash", - "WebSearch", - "WebFetch", - "mcp__automaker-tools__UpdateFeatureStatus", - ], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - // Add thinking configuration if enabled - if (thinkingConfig) { - options.thinking = thinkingConfig; - } - - // Build prompt with previous context - let prompt = await promptBuilder.buildResumePrompt( - feature, - previousContext, - projectPath - ); - - // Add images to prompt if feature has imagePaths or followUpImages - const imagePaths = feature.followUpImages || feature.imagePaths; - if (imagePaths && imagePaths.length > 0) { - const contentBlocks = []; - - // Add text block - contentBlocks.push({ - type: "text", - text: prompt, - }); - - // Add image blocks - const fs = require("fs"); - const path = require("path"); - for (const imagePathObj of imagePaths) { - try { - // Handle both string paths and FeatureImagePath objects - const imagePath = - typeof imagePathObj === "string" - ? imagePathObj - : imagePathObj.path; - const imageBuffer = fs.readFileSync(imagePath); - const base64Data = imageBuffer.toString("base64"); - const ext = path.extname(imagePath).toLowerCase(); - const mimeTypeMap = { - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".png": "image/png", - ".gif": "image/gif", - ".webp": "image/webp", - }; - const mediaType = - typeof imagePathObj === "string" - ? mimeTypeMap[ext] || "image/png" - : mimeTypeMap[ext] || imagePathObj.mimeType || "image/png"; - - contentBlocks.push({ - type: "image", - source: { - type: "base64", - media_type: mediaType, - data: base64Data, - }, - }); - - console.log( - `[FeatureExecutor] Added image to resume prompt: ${imagePath}` - ); - } catch (error) { - const errorPath = - typeof imagePathObj === "string" - ? imagePathObj - : imagePathObj.path; - console.error( - `[FeatureExecutor] Failed to load image ${errorPath}:`, - error - ); - } - } - - // Wrap content blocks in async generator for SDK (required format for multimodal prompts) - prompt = (async function* () { - yield { - type: "user", - session_id: "", - message: { - role: "user", - content: contentBlocks, - }, - parent_tool_use_id: null, - }; - })(); - } - - // Use appropriate provider based on model type - let currentQuery; - if (isCodex) { - // Validate that model string is actually a Codex model - if (modelString.startsWith("claude-")) { - console.error( - `[FeatureExecutor] ERROR: Codex provider selected but Claude model string detected: ${modelString}` - ); - throw new Error( - `Invalid model configuration: Codex provider cannot use Claude model '${modelString}'. Please check feature model setting.` - ); - } - - console.log( - `[FeatureExecutor] Using Codex provider for resume with model: ${modelString}` - ); - const provider = this.getProvider(feature); - currentQuery = provider.executeQuery({ - prompt, - model: modelString, - cwd: projectPath, - systemPrompt: promptBuilder.getVerificationPrompt(), - maxTurns: 20, - allowedTools: options.allowedTools, - abortController: abortController, - env: { - OPENAI_API_KEY: process.env.OPENAI_API_KEY, - }, - }); - } else { - // Use Claude SDK - currentQuery = query({ prompt, options }); - } - execution.query = currentQuery; - - let responseText = ""; - for await (const msg of currentQuery) { - // Check if this specific feature was aborted - if (!execution.isActive()) break; - - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - - await contextManager.writeToContextFile( - projectPath, - feature.id, - block.text - ); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: block.text, - }); - } else if (block.type === "tool_use") { - const toolMsg = `\nšŸ”§ Tool: ${block.name}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - toolMsg - ); - - sendToRenderer({ - type: "auto_mode_tool", - featureId: feature.id, - tool: block.name, - input: block.input, - }); - } - } - } - } - - execution.query = null; - execution.abortController = null; - - // Check if feature was marked as verified or waiting_approval (for skipTests) - const updatedFeatures = await featureLoader.loadFeatures(projectPath); - const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); - // For skipTests features, waiting_approval is also considered a success - const passes = - updatedFeature?.status === "verified" || - (updatedFeature?.skipTests && - updatedFeature?.status === "waiting_approval"); - - const finalMsg = passes - ? "āœ“ Feature successfully verified and completed\n" - : "⚠ Feature still in progress - may need additional work\n"; - - await contextManager.writeToContextFile( - projectPath, - feature.id, - finalMsg - ); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: finalMsg, - }); - - return { - passes, - message: responseText.substring(0, 500), - }; - } catch (error) { - if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[FeatureExecutor] Resume aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - passes: false, - message: "Resume aborted", - }; - } - - console.error("[FeatureExecutor] Error resuming feature:", error); - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } - - /** - * Commit changes for a feature without doing additional work - * Analyzes changes and creates a proper conventional commit message - */ - async commitChangesOnly(feature, projectPath, sendToRenderer, execution) { - console.log( - `[FeatureExecutor] Committing changes for: ${feature.description}` - ); - - try { - const commitMessage = `\nšŸ“ Committing changes for: ${feature.description}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - commitMessage - ); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: "Analyzing changes and creating commit...", - }); - - // Get the files that were changed during this AI session - const changedFiles = await contextManager.getFilesChangedDuringSession( - projectPath, - feature.id - ); - - // Combine new files and modified files into a single list of files to commit - const sessionFiles = [ - ...changedFiles.newFiles, - ...changedFiles.modifiedFiles, - ]; - - console.log( - `[FeatureExecutor] Files changed during session: ${sessionFiles.length}`, - sessionFiles - ); - - const abortController = new AbortController(); - execution.abortController = abortController; - - // Create custom MCP server with UpdateFeatureStatus tool - const featureToolsServer = mcpServerFactory.createFeatureToolsServer( - featureLoader.updateFeatureStatus.bind(featureLoader), - projectPath - ); - - const options = { - model: "claude-sonnet-4-20250514", // Use sonnet for commit task - systemPrompt: `You are a git commit assistant that creates professional conventional commit messages. - -IMPORTANT RULES: -- DO NOT modify any code -- DO NOT write tests -- DO NOT do anything except analyzing changes and committing them -- Use the git command line tools via Bash -- Create proper conventional commit messages based on what was actually changed -- ONLY commit the specific files that were changed during the AI session (provided in the prompt) -- DO NOT use 'git add .' - only add the specific files listed`, - maxTurns: 15, // Allow some turns to analyze and commit - cwd: projectPath, - mcpServers: { - "automaker-tools": featureToolsServer, - }, - allowedTools: ["Bash", "mcp__automaker-tools__UpdateFeatureStatus"], - permissionMode: "acceptEdits", - sandbox: { - enabled: false, // Need to run git commands - }, - abortController: abortController, - }; - - // Build the file list section for the prompt - let fileListSection = ""; - if (sessionFiles.length > 0) { - fileListSection = ` -**Files Changed During This AI Session:** -The following files were modified or created during this feature implementation: -${sessionFiles.map((f) => `- ${f}`).join("\n")} - -**CRITICAL:** Only commit these specific files listed above. Do NOT use \`git add .\` or \`git add -A\`. -Instead, add each file individually or use: \`git add ${sessionFiles.map((f) => `"${f}"`).join(" ")}\` -`; - } else { - fileListSection = ` -**Note:** No specific files were tracked for this session. Please run \`git status\` to see what files have been modified, and only stage files that appear to be related to this feature implementation. Be conservative - if a file doesn't seem related to this feature, don't include it. -`; - } - - // Prompt that guides the agent to create a proper conventional commit - const prompt = `Please commit the changes for this feature with a proper conventional commit message. - -**Feature Context:** -Category: ${feature.category} -Description: ${feature.description} -${fileListSection} -**Your Task:** - -1. First, run \`git status\` to see the current state of the repository -2. Run \`git diff\` on the specific files listed above to see the actual changes -3. Run \`git log --oneline -5\` to see recent commit message styles in this repo -4. Analyze the changes in the files and draft a proper conventional commit message: - - Use conventional commit format: \`type(scope): description\` - - Types: feat, fix, refactor, style, docs, test, chore - - The description should be concise (under 72 chars) and focus on "what" was done - - Summarize the nature of the changes (new feature, enhancement, bug fix, etc.) - - Make sure the commit message accurately reflects the actual code changes -5. Stage ONLY the specific files that were changed during this session (listed above) - - DO NOT use \`git add .\` or \`git add -A\` - - Add files individually: \`git add "path/to/file1" "path/to/file2"\` -6. Create the commit with a message ending with: - šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) - - Co-Authored-By: Claude Sonnet 4 - -Use a HEREDOC for the commit message to ensure proper formatting: -\`\`\`bash -git commit -m "$(cat <<'EOF' -type(scope): Short description here - -Optional longer description if needed. - -šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) - -Co-Authored-By: Claude Sonnet 4 -EOF -)" -\`\`\` - -**IMPORTANT:** -- DO NOT use the feature description verbatim as the commit message -- Analyze the actual code changes to determine the appropriate commit message -- The commit message should be professional and follow conventional commit standards -- DO NOT modify any code or run tests - ONLY commit the existing changes -- ONLY stage the specific files listed above - do not commit unrelated changes`; - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - let responseText = ""; - for await (const msg of currentQuery) { - if (!execution.isActive()) break; - - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - - await contextManager.writeToContextFile( - projectPath, - feature.id, - block.text - ); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: block.text, - }); - } else if (block.type === "tool_use") { - const toolMsg = `\nšŸ”§ Tool: ${block.name}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - toolMsg - ); - - sendToRenderer({ - type: "auto_mode_tool", - featureId: feature.id, - tool: block.name, - input: block.input, - }); - } - } - } - } - - execution.query = null; - execution.abortController = null; - - const finalMsg = "āœ“ Changes committed successfully\n"; - await contextManager.writeToContextFile( - projectPath, - feature.id, - finalMsg - ); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: finalMsg, - }); - - return { - passes: true, - message: responseText.substring(0, 500), - }; - } catch (error) { - if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[FeatureExecutor] Commit aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - passes: false, - message: "Commit aborted", - }; - } - - console.error("[FeatureExecutor] Error committing feature:", error); - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } -} - -module.exports = new FeatureExecutor(); diff --git a/apps/app/electron/services/feature-loader.js b/apps/app/electron/services/feature-loader.js deleted file mode 100644 index d95ba08c..00000000 --- a/apps/app/electron/services/feature-loader.js +++ /dev/null @@ -1,500 +0,0 @@ -const path = require("path"); -const fs = require("fs/promises"); - -/** - * Feature Loader - Handles loading and managing features from individual feature folders - * Each feature is stored in .automaker/features/{featureId}/feature.json - */ -class FeatureLoader { - /** - * Get the features directory path - */ - getFeaturesDir(projectPath) { - return path.join(projectPath, ".automaker", "features"); - } - - /** - * Get the path to a specific feature folder - */ - getFeatureDir(projectPath, featureId) { - return path.join(this.getFeaturesDir(projectPath), featureId); - } - - /** - * Get the path to a feature's feature.json file - */ - getFeatureJsonPath(projectPath, featureId) { - return path.join( - this.getFeatureDir(projectPath, featureId), - "feature.json" - ); - } - - /** - * Get the path to a feature's agent-output.md file - */ - getAgentOutputPath(projectPath, featureId) { - return path.join( - this.getFeatureDir(projectPath, featureId), - "agent-output.md" - ); - } - - /** - * Generate a new feature ID - */ - generateFeatureId() { - return `feature-${Date.now()}-${Math.random() - .toString(36) - .substring(2, 11)}`; - } - - /** - * Ensure all image paths for a feature are stored within the feature directory - */ - async ensureFeatureImages(projectPath, featureId, feature) { - if ( - !feature || - !Array.isArray(feature.imagePaths) || - feature.imagePaths.length === 0 - ) { - return; - } - - const featureDir = this.getFeatureDir(projectPath, featureId); - const featureImagesDir = path.join(featureDir, "images"); - await fs.mkdir(featureImagesDir, { recursive: true }); - - const updatedImagePaths = []; - - for (const entry of feature.imagePaths) { - const isStringEntry = typeof entry === "string"; - const currentPathValue = isStringEntry ? entry : entry.path; - - if (!currentPathValue) { - updatedImagePaths.push(entry); - continue; - } - - let resolvedCurrentPath = currentPathValue; - if (!path.isAbsolute(resolvedCurrentPath)) { - resolvedCurrentPath = path.join(projectPath, resolvedCurrentPath); - } - resolvedCurrentPath = path.normalize(resolvedCurrentPath); - - // Skip if file doesn't exist - try { - await fs.access(resolvedCurrentPath); - } catch { - console.warn( - `[FeatureLoader] Image file missing for ${featureId}: ${resolvedCurrentPath}` - ); - updatedImagePaths.push(entry); - continue; - } - - const relativeToFeatureImages = path.relative( - featureImagesDir, - resolvedCurrentPath - ); - const alreadyInFeatureDir = - relativeToFeatureImages === "" || - (!relativeToFeatureImages.startsWith("..") && - !path.isAbsolute(relativeToFeatureImages)); - - let finalPath = resolvedCurrentPath; - - if (!alreadyInFeatureDir) { - const originalName = path.basename(resolvedCurrentPath); - let targetPath = path.join(featureImagesDir, originalName); - - // Avoid overwriting files by appending a counter if needed - let counter = 1; - while (true) { - try { - await fs.access(targetPath); - const parsed = path.parse(originalName); - targetPath = path.join( - featureImagesDir, - `${parsed.name}-${counter}${parsed.ext}` - ); - counter += 1; - } catch { - break; - } - } - - try { - await fs.rename(resolvedCurrentPath, targetPath); - finalPath = targetPath; - } catch (error) { - console.warn( - `[FeatureLoader] Failed to move image ${resolvedCurrentPath}: ${error.message}` - ); - updatedImagePaths.push(entry); - continue; - } - } - - updatedImagePaths.push( - isStringEntry ? finalPath : { ...entry, path: finalPath } - ); - } - - feature.imagePaths = updatedImagePaths; - } - - /** - * Get all features for a project - */ - async getAll(projectPath) { - try { - const featuresDir = this.getFeaturesDir(projectPath); - - // Check if features directory exists - try { - await fs.access(featuresDir); - } catch { - // Directory doesn't exist, return empty array - return []; - } - - // Read all feature directories - const entries = await fs.readdir(featuresDir, { withFileTypes: true }); - const featureDirs = entries.filter((entry) => entry.isDirectory()); - - // Load each feature - const features = []; - for (const dir of featureDirs) { - const featureId = dir.name; - const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); - - try { - // Read feature.json directly - handle ENOENT in catch block - // This avoids TOCTOU race condition from checking with fs.access first - const content = await fs.readFile(featureJsonPath, "utf-8"); - const feature = JSON.parse(content); - - // Validate that the feature has required fields - if (!feature.id) { - console.warn( - `[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping` - ); - continue; - } - - features.push(feature); - } catch (error) { - // Handle different error types appropriately - if (error.code === "ENOENT") { - // File doesn't exist - this is expected for incomplete feature directories - // Skip silently (feature.json not yet created or was removed) - continue; - } else if (error instanceof SyntaxError) { - // JSON parse error - log as warning since file exists but is malformed - console.warn( - `[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}` - ); - } else { - // Other errors - log as error - console.error( - `[FeatureLoader] Failed to load feature ${featureId}:`, - error.message || error - ); - } - // Continue loading other features - } - } - - // Sort by creation order (feature IDs contain timestamp) - features.sort((a, b) => { - const aTime = a.id ? parseInt(a.id.split("-")[1] || "0") : 0; - const bTime = b.id ? parseInt(b.id.split("-")[1] || "0") : 0; - return aTime - bTime; - }); - - return features; - } catch (error) { - console.error("[FeatureLoader] Failed to get all features:", error); - return []; - } - } - - /** - * Get a single feature by ID - */ - async get(projectPath, featureId) { - try { - const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); - const content = await fs.readFile(featureJsonPath, "utf-8"); - return JSON.parse(content); - } catch (error) { - if (error.code === "ENOENT") { - return null; - } - console.error( - `[FeatureLoader] Failed to get feature ${featureId}:`, - error - ); - throw error; - } - } - - /** - * Create a new feature - */ - async create(projectPath, featureData) { - const featureId = featureData.id || this.generateFeatureId(); - const featureDir = this.getFeatureDir(projectPath, featureId); - const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); - - // Ensure features directory exists - const featuresDir = this.getFeaturesDir(projectPath); - await fs.mkdir(featuresDir, { recursive: true }); - - // Create feature directory - await fs.mkdir(featureDir, { recursive: true }); - - // Ensure feature has an ID - const feature = { ...featureData, id: featureId }; - - // Move any uploaded images into the feature directory - await this.ensureFeatureImages(projectPath, featureId, feature); - - // Write feature.json - await fs.writeFile( - featureJsonPath, - JSON.stringify(feature, null, 2), - "utf-8" - ); - - console.log(`[FeatureLoader] Created feature ${featureId}`); - return feature; - } - - /** - * Update a feature (partial updates supported) - */ - async update(projectPath, featureId, updates) { - try { - const feature = await this.get(projectPath, featureId); - if (!feature) { - throw new Error(`Feature ${featureId} not found`); - } - - // Merge updates - const updatedFeature = { ...feature, ...updates }; - - // Move any new images into the feature directory - await this.ensureFeatureImages(projectPath, featureId, updatedFeature); - - // Write back to file - const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); - await fs.writeFile( - featureJsonPath, - JSON.stringify(updatedFeature, null, 2), - "utf-8" - ); - - console.log(`[FeatureLoader] Updated feature ${featureId}`); - return updatedFeature; - } catch (error) { - console.error( - `[FeatureLoader] Failed to update feature ${featureId}:`, - error - ); - throw error; - } - } - - /** - * Delete a feature and its entire folder - */ - async delete(projectPath, featureId) { - try { - const featureDir = this.getFeatureDir(projectPath, featureId); - await fs.rm(featureDir, { recursive: true, force: true }); - console.log(`[FeatureLoader] Deleted feature ${featureId}`); - } catch (error) { - if (error.code === "ENOENT") { - // Feature doesn't exist, that's fine - return; - } - console.error( - `[FeatureLoader] Failed to delete feature ${featureId}:`, - error - ); - throw error; - } - } - - /** - * Get agent output for a feature - */ - async getAgentOutput(projectPath, featureId) { - try { - const agentOutputPath = this.getAgentOutputPath(projectPath, featureId); - const content = await fs.readFile(agentOutputPath, "utf-8"); - return content; - } catch (error) { - if (error.code === "ENOENT") { - return null; - } - console.error( - `[FeatureLoader] Failed to get agent output for ${featureId}:`, - error - ); - return null; - } - } - - // ============================================================================ - // Legacy methods for backward compatibility (used by backend services) - // ============================================================================ - - /** - * Load all features for a project (legacy API) - * Features are stored in .automaker/features/{id}/feature.json - */ - async loadFeatures(projectPath) { - return await this.getAll(projectPath); - } - - /** - * Update feature status (legacy API) - * Features are stored in .automaker/features/{id}/feature.json - * Creates the feature if it doesn't exist. - * @param {string} featureId - The ID of the feature to update - * @param {string} status - The new status - * @param {string} projectPath - Path to the project - * @param {Object} options - Options object for optional parameters - * @param {string} [options.summary] - Optional summary of what was done - * @param {string} [options.error] - Optional error message if feature errored - * @param {string} [options.description] - Optional detailed description - * @param {string} [options.category] - Optional category/phase - * @param {string[]} [options.steps] - Optional array of implementation steps - */ - async updateFeatureStatus(featureId, status, projectPath, options = {}) { - const { summary, error, description, category, steps } = options; - // Check if feature exists - const existingFeature = await this.get(projectPath, featureId); - - if (!existingFeature) { - // Feature doesn't exist - create it with all required fields - console.log(`[FeatureLoader] Feature ${featureId} not found - creating new feature`); - const newFeature = { - id: featureId, - title: featureId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '), - description: description || summary || '', // Use provided description, fall back to summary - category: category || "Uncategorized", - steps: steps || [], - status: status, - images: [], - imagePaths: [], - skipTests: false, // Auto-generated features should run tests by default - model: "sonnet", - thinkingLevel: "none", - summary: summary || description || '', - createdAt: new Date().toISOString(), - }; - if (error !== undefined) { - newFeature.error = error; - } - await this.create(projectPath, newFeature); - console.log( - `[FeatureLoader] Created feature ${featureId}: status=${status}, category=${category || "Uncategorized"}, steps=${steps?.length || 0}${ - summary ? `, summary="${summary}"` : "" - }` - ); - return; - } - - // Feature exists - update it - const updates = { status }; - if (summary !== undefined) { - updates.summary = summary; - // Also update description if it's empty or not set - if (!existingFeature.description) { - updates.description = summary; - } - } - if (description !== undefined) { - updates.description = description; - } - if (category !== undefined) { - updates.category = category; - } - if (steps !== undefined && Array.isArray(steps)) { - updates.steps = steps; - } - if (error !== undefined) { - updates.error = error; - } else { - // Clear error if not provided - if (existingFeature.error) { - updates.error = undefined; - } - } - - // Ensure required fields exist (for features created before this fix) - if (!existingFeature.category && !updates.category) updates.category = "Uncategorized"; - if (!existingFeature.steps && !updates.steps) updates.steps = []; - if (!existingFeature.images) updates.images = []; - if (!existingFeature.imagePaths) updates.imagePaths = []; - if (existingFeature.skipTests === undefined) updates.skipTests = false; - if (!existingFeature.model) updates.model = "sonnet"; - if (!existingFeature.thinkingLevel) updates.thinkingLevel = "none"; - - await this.update(projectPath, featureId, updates); - console.log( - `[FeatureLoader] Updated feature ${featureId}: status=${status}${ - category ? `, category="${category}"` : "" - }${steps ? `, steps=${steps.length}` : ""}${ - summary ? `, summary="${summary}"` : "" - }` - ); - } - - /** - * Select the next feature to implement - * Prioritizes: earlier features in the list that are not verified or waiting_approval - */ - selectNextFeature(features) { - // Find first feature that is in backlog or in_progress status - // Skip verified and waiting_approval (which needs user input) - return features.find( - (f) => f.status !== "verified" && f.status !== "waiting_approval" - ); - } - - /** - * Update worktree info for a feature (legacy API) - * Features are stored in .automaker/features/{id}/feature.json - * @param {string} featureId - The ID of the feature to update - * @param {string} projectPath - Path to the project - * @param {string|null} worktreePath - Path to the worktree (null to clear) - * @param {string|null} branchName - Name of the feature branch (null to clear) - */ - async updateFeatureWorktree( - featureId, - projectPath, - worktreePath, - branchName - ) { - const updates = {}; - if (worktreePath) { - updates.worktreePath = worktreePath; - updates.branchName = branchName; - } else { - updates.worktreePath = null; - updates.branchName = null; - } - - await this.update(projectPath, featureId, updates); - console.log( - `[FeatureLoader] Updated feature ${featureId}: worktreePath=${worktreePath}, branchName=${branchName}` - ); - } -} - -module.exports = new FeatureLoader(); diff --git a/apps/app/electron/services/feature-suggestions-service.js b/apps/app/electron/services/feature-suggestions-service.js deleted file mode 100644 index 2241e9b3..00000000 --- a/apps/app/electron/services/feature-suggestions-service.js +++ /dev/null @@ -1,379 +0,0 @@ -const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); -const promptBuilder = require("./prompt-builder"); - -/** - * Feature Suggestions Service - Analyzes project and generates feature suggestions - */ -class FeatureSuggestionsService { - constructor() { - this.runningAnalysis = null; - } - - /** - * Generate feature suggestions by analyzing the project - * @param {string} projectPath - Path to the project - * @param {Function} sendToRenderer - Function to send events to renderer - * @param {Object} execution - Execution context with abort controller - * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" - */ - async generateSuggestions(projectPath, sendToRenderer, execution, suggestionType = "features") { - console.log( - `[FeatureSuggestions] Generating ${suggestionType} suggestions for: ${projectPath}` - ); - - try { - const abortController = new AbortController(); - execution.abortController = abortController; - - const options = { - model: "claude-sonnet-4-20250514", - systemPrompt: this.getSystemPrompt(suggestionType), - maxTurns: 50, - cwd: projectPath, - allowedTools: ["Read", "Glob", "Grep", "Bash"], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - const prompt = this.buildAnalysisPrompt(suggestionType); - - sendToRenderer({ - type: "suggestions_progress", - content: "Starting project analysis...\n", - }); - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - let fullResponse = ""; - for await (const msg of currentQuery) { - if (!execution.isActive()) break; - - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - fullResponse += block.text; - sendToRenderer({ - type: "suggestions_progress", - content: block.text, - }); - } else if (block.type === "tool_use") { - sendToRenderer({ - type: "suggestions_tool", - tool: block.name, - input: block.input, - }); - } - } - } - } - - execution.query = null; - execution.abortController = null; - - // Parse the suggestions from the response - const suggestions = this.parseSuggestions(fullResponse); - - sendToRenderer({ - type: "suggestions_complete", - suggestions: suggestions, - }); - - return { - success: true, - suggestions: suggestions, - }; - } catch (error) { - if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[FeatureSuggestions] Analysis aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - success: false, - message: "Analysis aborted", - suggestions: [], - }; - } - - console.error( - "[FeatureSuggestions] Error generating suggestions:", - error - ); - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } - - /** - * Parse suggestions from the LLM response - * Looks for JSON array in the response - */ - parseSuggestions(response) { - try { - // Try to find JSON array in the response - // Look for ```json ... ``` blocks first - const jsonBlockMatch = response.match(/```json\s*([\s\S]*?)```/); - if (jsonBlockMatch) { - const parsed = JSON.parse(jsonBlockMatch[1].trim()); - if (Array.isArray(parsed)) { - return this.validateSuggestions(parsed); - } - } - - // Try to find a raw JSON array - const jsonArrayMatch = response.match(/\[\s*\{[\s\S]*\}\s*\]/); - if (jsonArrayMatch) { - const parsed = JSON.parse(jsonArrayMatch[0]); - if (Array.isArray(parsed)) { - return this.validateSuggestions(parsed); - } - } - - console.warn( - "[FeatureSuggestions] Could not parse suggestions from response" - ); - return []; - } catch (error) { - console.error("[FeatureSuggestions] Error parsing suggestions:", error); - return []; - } - } - - /** - * Validate and normalize suggestions - */ - validateSuggestions(suggestions) { - return suggestions - .filter((s) => s && typeof s === "object") - .map((s, index) => ({ - id: `suggestion-${Date.now()}-${index}`, - category: s.category || "Uncategorized", - description: s.description || s.title || "No description", - steps: Array.isArray(s.steps) ? s.steps : [], - priority: typeof s.priority === "number" ? s.priority : index + 1, - reasoning: s.reasoning || "", - })) - .sort((a, b) => a.priority - b.priority); - } - - /** - * Get the system prompt for feature suggestion analysis - * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" - */ - getSystemPrompt(suggestionType = "features") { - const basePrompt = `You are an expert software architect. Your job is to analyze a codebase and provide actionable suggestions. - -You have access to file reading and search tools. Use them to understand the codebase. - -When analyzing, look at: -- README files and documentation -- Package.json, cargo.toml, or similar config files for tech stack -- Source code structure and organization -- Existing code patterns and implementation styles`; - - switch (suggestionType) { - case "refactoring": - return `${basePrompt} - -Your specific focus is on **refactoring suggestions**. You should: -1. Identify code smells and areas that need cleanup -2. Find duplicated code that could be consolidated -3. Spot overly complex functions or classes that should be broken down -4. Look for inconsistent naming conventions or coding patterns -5. Find opportunities to improve code organization and modularity -6. Identify violations of SOLID principles or common design patterns -7. Look for dead code or unused dependencies - -Prioritize suggestions by: -- Impact on maintainability -- Risk level (lower risk refactorings first) -- Complexity of the refactoring`; - - case "security": - return `${basePrompt} - -Your specific focus is on **security vulnerabilities and improvements**. You should: -1. Identify potential security vulnerabilities (OWASP Top 10) -2. Look for hardcoded secrets, API keys, or credentials -3. Check for proper input validation and sanitization -4. Identify SQL injection, XSS, or command injection risks -5. Review authentication and authorization patterns -6. Check for secure communication (HTTPS, encryption) -7. Look for insecure dependencies or outdated packages -8. Review error handling that might leak sensitive information -9. Check for proper session management -10. Identify insecure file handling or path traversal risks - -Prioritize by severity: -- Critical: Exploitable vulnerabilities with high impact -- High: Security issues that could lead to data exposure -- Medium: Best practice violations that weaken security -- Low: Minor improvements to security posture`; - - case "performance": - return `${basePrompt} - -Your specific focus is on **performance issues and optimizations**. You should: -1. Identify N+1 query problems or inefficient database access -2. Look for unnecessary re-renders in React/frontend code -3. Find opportunities for caching or memoization -4. Identify large bundle sizes or unoptimized imports -5. Look for blocking operations that could be async -6. Find memory leaks or inefficient memory usage -7. Identify slow algorithms or data structure choices -8. Look for missing indexes in database schemas -9. Find opportunities for lazy loading or code splitting -10. Identify unnecessary network requests or API calls - -Prioritize by: -- Impact on user experience -- Frequency of the slow path -- Ease of implementation`; - - default: // "features" - return `${basePrompt} - -Your specific focus is on **missing features and improvements**. You should: -1. Identify what the application does and what features it currently has -2. Look at the .automaker/app_spec.txt file if it exists -3. Generate a comprehensive list of missing features that would be valuable to users -4. Consider user experience improvements -5. Consider developer experience improvements -6. Look at common patterns in similar applications - -Prioritize features by: -- Impact on users -- Alignment with project goals -- Complexity of implementation`; - } - } - - /** - * Build the prompt for analyzing the project - * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" - */ - buildAnalysisPrompt(suggestionType = "features") { - const commonIntro = `Analyze this project and generate a list of actionable suggestions. - -**Your Task:** - -1. First, explore the project structure: - - Read README.md, package.json, or similar config files - - Scan the source code directory structure - - Identify the tech stack and frameworks used - - Look at existing code and how it's implemented - -2. Identify what the application does: - - What is the main purpose? - - What patterns and conventions are used? -`; - - const commonOutput = ` -**CRITICAL: Output your suggestions as a JSON array** at the end of your response, formatted like this: - -\`\`\`json -[ - { - "category": "Category Name", - "description": "Clear description of the suggestion", - "steps": [ - "Step 1 to implement", - "Step 2 to implement", - "Step 3 to implement" - ], - "priority": 1, - "reasoning": "Why this is important" - } -] -\`\`\` - -**Important Guidelines:** -- Generate at least 10-15 suggestions -- Order them by priority (1 = highest priority) -- Each suggestion should have clear, actionable steps -- Be specific about what files might need to be modified -- Consider the existing tech stack and patterns - -Begin by exploring the project structure.`; - - switch (suggestionType) { - case "refactoring": - return `${commonIntro} -3. Look for refactoring opportunities: - - Find code duplication across the codebase - - Identify functions or classes that are too long or complex - - Look for inconsistent patterns or naming conventions - - Find tightly coupled code that should be decoupled - - Identify opportunities to extract reusable utilities - - Look for dead code or unused exports - - Check for proper separation of concerns - -Categories to use: "Code Smell", "Duplication", "Complexity", "Architecture", "Naming", "Dead Code", "Coupling", "Testing" -${commonOutput}`; - - case "security": - return `${commonIntro} -3. Look for security issues: - - Check for hardcoded secrets or API keys - - Look for potential injection vulnerabilities (SQL, XSS, command) - - Review authentication and authorization code - - Check input validation and sanitization - - Look for insecure dependencies - - Review error handling for information leakage - - Check for proper HTTPS/TLS usage - - Look for insecure file operations - -Categories to use: "Critical", "High", "Medium", "Low" (based on severity) -${commonOutput}`; - - case "performance": - return `${commonIntro} -3. Look for performance issues: - - Find N+1 queries or inefficient database access patterns - - Look for unnecessary re-renders in React components - - Identify missing memoization opportunities - - Check bundle size and import patterns - - Look for synchronous operations that could be async - - Find potential memory leaks - - Identify slow algorithms or data structures - - Look for missing caching opportunities - - Check for unnecessary network requests - -Categories to use: "Database", "Rendering", "Memory", "Bundle Size", "Caching", "Algorithm", "Network" -${commonOutput}`; - - default: // "features" - return `${commonIntro} -3. Generate feature suggestions: - - Think about what's missing compared to similar applications - - Consider user experience improvements - - Consider developer experience improvements - - Think about performance, security, and reliability - - Consider testing and documentation improvements - -Categories to use: "User Experience", "Performance", "Security", "Testing", "Documentation", "Developer Experience", "Accessibility", etc. -${commonOutput}`; - } - } - - /** - * Stop the current analysis - */ - stop() { - if (this.runningAnalysis && this.runningAnalysis.abortController) { - this.runningAnalysis.abortController.abort(); - } - this.runningAnalysis = null; - } -} - -module.exports = new FeatureSuggestionsService(); diff --git a/apps/app/electron/services/feature-verifier.js b/apps/app/electron/services/feature-verifier.js deleted file mode 100644 index dea98038..00000000 --- a/apps/app/electron/services/feature-verifier.js +++ /dev/null @@ -1,185 +0,0 @@ -const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); -const promptBuilder = require("./prompt-builder"); -const contextManager = require("./context-manager"); -const featureLoader = require("./feature-loader"); -const mcpServerFactory = require("./mcp-server-factory"); - -/** - * Feature Verifier - Handles feature verification by running tests - */ -class FeatureVerifier { - /** - * Verify feature tests (runs tests and checks if they pass) - */ - async verifyFeatureTests(feature, projectPath, sendToRenderer, execution) { - console.log( - `[FeatureVerifier] Verifying tests for: ${feature.description}` - ); - - try { - const verifyMsg = `\nāœ… Verifying tests for: ${feature.description}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - verifyMsg - ); - - sendToRenderer({ - type: "auto_mode_phase", - featureId: feature.id, - phase: "verification", - message: `Verifying tests for: ${feature.description}`, - }); - - const abortController = new AbortController(); - execution.abortController = abortController; - - // Create custom MCP server with UpdateFeatureStatus tool - const featureToolsServer = mcpServerFactory.createFeatureToolsServer( - featureLoader.updateFeatureStatus.bind(featureLoader), - projectPath - ); - - const options = { - model: "claude-opus-4-5-20251101", - systemPrompt: await promptBuilder.getVerificationPrompt(projectPath), - maxTurns: 1000, - cwd: projectPath, - mcpServers: { - "automaker-tools": featureToolsServer, - }, - allowedTools: [ - "Read", - "Write", - "Edit", - "Glob", - "Grep", - "Bash", - "mcp__automaker-tools__UpdateFeatureStatus", - ], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - const prompt = await promptBuilder.buildVerificationPrompt( - feature, - projectPath - ); - - const runningTestsMsg = - "Running Playwright tests to verify feature implementation...\n"; - await contextManager.writeToContextFile( - projectPath, - feature.id, - runningTestsMsg - ); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: runningTestsMsg, - }); - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - let responseText = ""; - for await (const msg of currentQuery) { - // Check if this specific feature was aborted - if (!execution.isActive()) break; - - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - - await contextManager.writeToContextFile( - projectPath, - feature.id, - block.text - ); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: block.text, - }); - } else if (block.type === "tool_use") { - const toolMsg = `\nšŸ”§ Tool: ${block.name}\n`; - await contextManager.writeToContextFile( - projectPath, - feature.id, - toolMsg - ); - - sendToRenderer({ - type: "auto_mode_tool", - featureId: feature.id, - tool: block.name, - input: block.input, - }); - } - } - } - } - - execution.query = null; - execution.abortController = null; - - // Re-load features to check if it was marked as verified or waiting_approval (for skipTests) - const updatedFeatures = await featureLoader.loadFeatures(projectPath); - const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); - // For skipTests features, waiting_approval is also considered a success - const passes = - updatedFeature?.status === "verified" || - (updatedFeature?.skipTests && - updatedFeature?.status === "waiting_approval"); - - const finalMsg = passes - ? "āœ“ Verification successful: All tests passed\n" - : "āœ— Tests failed or not all passing - feature remains in progress\n"; - - await contextManager.writeToContextFile( - projectPath, - feature.id, - finalMsg - ); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: feature.id, - content: finalMsg, - }); - - return { - passes, - message: responseText.substring(0, 500), - }; - } catch (error) { - if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[FeatureVerifier] Verification aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - passes: false, - message: "Verification aborted", - }; - } - - console.error("[FeatureVerifier] Error verifying feature:", error); - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } -} - -module.exports = new FeatureVerifier(); diff --git a/apps/app/electron/services/mcp-server-factory.js b/apps/app/electron/services/mcp-server-factory.js deleted file mode 100644 index 7af5e951..00000000 --- a/apps/app/electron/services/mcp-server-factory.js +++ /dev/null @@ -1,109 +0,0 @@ -const { createSdkMcpServer, tool } = require("@anthropic-ai/claude-agent-sdk"); -const { z } = require("zod"); -const featureLoader = require("./feature-loader"); - -/** - * MCP Server Factory - Creates custom MCP servers with tools - */ -class McpServerFactory { - /** - * Create a custom MCP server with the UpdateFeatureStatus tool - * This tool allows Claude Code to safely update feature status without - * directly modifying feature files, preventing race conditions - * and accidental state corruption. - */ - createFeatureToolsServer(updateFeatureStatusCallback, projectPath) { - return createSdkMcpServer({ - name: "automaker-tools", - version: "1.0.0", - tools: [ - tool( - "UpdateFeatureStatus", - "Create or update a feature. Use this tool to create new features with detailed information or update existing feature status. When creating features, provide comprehensive description, category, and implementation steps.", - { - featureId: z.string().describe("The ID of the feature (lowercase, hyphens for spaces). Example: 'user-authentication', 'budget-tracking'"), - status: z.enum(["backlog", "todo", "in_progress", "verified"]).describe("The status for the feature. For NEW features, ONLY use 'backlog' or 'verified'. NEVER use 'in_progress' for new features - the user will manually start them."), - summary: z.string().optional().describe("A brief summary of what was implemented/changed or what the feature does."), - description: z.string().optional().describe("A detailed description of the feature. Be comprehensive - explain what the feature does, its purpose, and key functionality."), - category: z.string().optional().describe("The category/phase for this feature. Example: 'Phase 1: Foundation', 'Phase 2: Core Logic', 'Phase 3: Polish', 'Authentication', 'UI/UX'"), - steps: z.array(z.string()).optional().describe("Array of implementation steps. Each step should be a clear, actionable task. Example: ['Set up database schema', 'Create API endpoints', 'Build UI components', 'Add validation']") - }, - async (args) => { - try { - console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}, category=${args.category || "(none)"}, steps=${args.steps?.length || 0}`); - console.log(`[Feature Creation] Creating/updating feature "${args.featureId}" with status "${args.status}"`); - - // Load the feature to check skipTests flag - const features = await featureLoader.loadFeatures(projectPath); - const feature = features.find((f) => f.id === args.featureId); - - if (!feature) { - console.log(`[Feature Creation] Feature ${args.featureId} not found - this is a new feature being created`); - // This is a new feature - enforce backlog status for any non-verified features - } - - // If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval - let finalStatus = args.status; - // For NEW features: Convert 'todo' or 'in_progress' to 'backlog' for consistency - // New features should ALWAYS go to backlog first, user must manually start them - if (!feature && (finalStatus === "todo" || finalStatus === "in_progress")) { - console.log(`[Feature Creation] New feature ${args.featureId} - converting "${finalStatus}" to "backlog" (user must manually start features)`); - finalStatus = "backlog"; - } - if (feature && args.status === "verified" && feature.skipTests === true) { - console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`); - finalStatus = "waiting_approval"; - } - - // IMPORTANT: Prevent agent from moving an in_progress feature back to backlog - // When a feature is being worked on, the agent should only be able to mark it as verified - // (which may be converted to waiting_approval for skipTests features) - // This prevents the agent from incorrectly putting completed work back in the backlog - if (feature && feature.status === "in_progress" && (args.status === "backlog" || args.status === "todo")) { - console.log(`[McpServerFactory] Feature ${args.featureId} is in_progress - preventing move to ${args.status}, converting to waiting_approval instead`); - finalStatus = "waiting_approval"; - } - - // Call the provided callback to update feature status - await updateFeatureStatusCallback( - args.featureId, - finalStatus, - projectPath, - { - summary: args.summary, - description: args.description, - category: args.category, - steps: args.steps, - } - ); - - const statusMessage = finalStatus !== args.status - ? `Successfully created/updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}")${args.summary ? ` - ${args.summary}` : ""}` - : `Successfully created/updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` - ${args.summary}` : ""}`; - - console.log(`[Feature Creation] āœ“ ${statusMessage}`); - - return { - content: [{ - type: "text", - text: statusMessage - }] - }; - } catch (error) { - console.error("[McpServerFactory] UpdateFeatureStatus tool error:", error); - console.error(`[Feature Creation] āœ— Failed to create/update feature ${args.featureId}: ${error.message}`); - return { - content: [{ - type: "text", - text: `Failed to update feature status: ${error.message}` - }] - }; - } - } - ) - ] - }); - } -} - -module.exports = new McpServerFactory(); diff --git a/apps/app/electron/services/mcp-server-stdio.js b/apps/app/electron/services/mcp-server-stdio.js deleted file mode 100644 index cb72ed49..00000000 --- a/apps/app/electron/services/mcp-server-stdio.js +++ /dev/null @@ -1,358 +0,0 @@ -#!/usr/bin/env node -/** - * Standalone STDIO MCP Server for Automaker Tools - * - * This script runs as a standalone process and communicates via JSON-RPC 2.0 - * over stdin/stdout. It implements the MCP protocol to expose the UpdateFeatureStatus - * tool to Codex CLI. - * - * Environment variables: - * - AUTOMAKER_PROJECT_PATH: Path to the project directory - * - AUTOMAKER_IPC_CHANNEL: IPC channel name for callback communication (optional, uses default) - */ - -const readline = require('readline'); -const path = require('path'); - -// Redirect all console.log output to stderr to avoid polluting MCP stdout -const originalConsoleLog = console.log; -console.log = (...args) => { - console.error(...args); -}; - -// Set up readline interface for line-by-line JSON-RPC input -// IMPORTANT: Use a separate output stream for readline to avoid interfering with JSON-RPC stdout -// We'll write JSON-RPC responses directly to stdout, not through readline -const rl = readline.createInterface({ - input: process.stdin, - output: null, // Don't use stdout for readline output - terminal: false -}); - -let initialized = false; -let projectPath = null; -let ipcChannel = null; - -// Get configuration from environment -projectPath = process.env.AUTOMAKER_PROJECT_PATH || process.cwd(); -ipcChannel = process.env.AUTOMAKER_IPC_CHANNEL || 'mcp:update-feature-status'; - -// Load dependencies (these will be available in the Electron app context) -let featureLoader; -let electron; - -// Try to load Electron IPC if available (when running from Electron app) -try { - // In Electron, we can use IPC directly - if (typeof require !== 'undefined') { - // Check if we're in Electron context - const electronModule = require('electron'); - if (electronModule && electronModule.ipcMain) { - electron = electronModule; - } - } -} catch (e) { - // Not in Electron context, will use alternative method -} - -// Load feature loader -// Try multiple paths since this script might be run from different contexts -try { - // First try relative path (when run from electron/services/) - featureLoader = require('./feature-loader'); -} catch (e) { - try { - // Try absolute path resolution - const featureLoaderPath = path.resolve(__dirname, 'feature-loader.js'); - delete require.cache[require.resolve(featureLoaderPath)]; - featureLoader = require(featureLoaderPath); - } catch (e2) { - // If still fails, try from parent directory - try { - featureLoader = require(path.join(__dirname, '..', 'services', 'feature-loader')); - } catch (e3) { - console.error('[McpServerStdio] Error loading feature-loader:', e3.message); - console.error('[McpServerStdio] Tried paths:', [ - './feature-loader', - path.resolve(__dirname, 'feature-loader.js'), - path.join(__dirname, '..', 'services', 'feature-loader') - ]); - process.exit(1); - } - } -} - -/** - * Send JSON-RPC response - * CRITICAL: Must write directly to stdout, not via console.log - * MCP protocol requires ONLY JSON-RPC messages on stdout - */ -function sendResponse(id, result, error = null) { - const response = { - jsonrpc: '2.0', - id - }; - - if (error) { - response.error = error; - } else { - response.result = result; - } - - // Write directly to stdout with newline (MCP uses line-delimited JSON) - process.stdout.write(JSON.stringify(response) + '\n'); -} - -/** - * Send JSON-RPC notification - * CRITICAL: Must write directly to stdout, not via console.log - */ -function sendNotification(method, params) { - const notification = { - jsonrpc: '2.0', - method, - params - }; - - // Write directly to stdout with newline (MCP uses line-delimited JSON) - process.stdout.write(JSON.stringify(notification) + '\n'); -} - -/** - * Handle MCP initialize request - */ -async function handleInitialize(params, id) { - initialized = true; - - sendResponse(id, { - protocolVersion: '2024-11-05', - capabilities: { - tools: {} - }, - serverInfo: { - name: 'automaker-tools', - version: '1.0.0' - } - }); -} - -/** - * Handle tools/list request - */ -async function handleToolsList(params, id) { - sendResponse(id, { - tools: [ - { - name: 'UpdateFeatureStatus', - description: 'Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.', - inputSchema: { - type: 'object', - properties: { - featureId: { - type: 'string', - description: 'The ID of the feature to update' - }, - status: { - type: 'string', - enum: ['backlog', 'in_progress', 'verified'], - description: 'The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically.' - }, - summary: { - type: 'string', - description: 'A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: "Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx"' - } - }, - required: ['featureId', 'status'] - } - } - ] - }); -} - -/** - * Handle tools/call request - */ -async function handleToolsCall(params, id) { - const { name, arguments: args } = params; - - if (name !== 'UpdateFeatureStatus') { - sendResponse(id, null, { - code: -32601, - message: `Unknown tool: ${name}` - }); - return; - } - - try { - const { featureId, status, summary } = args; - - if (!featureId || !status) { - sendResponse(id, null, { - code: -32602, - message: 'Missing required parameters: featureId and status are required' - }); - return; - } - - // Load the feature to check skipTests flag - const features = await featureLoader.loadFeatures(projectPath); - const feature = features.find((f) => f.id === featureId); - - if (!feature) { - sendResponse(id, null, { - code: -32602, - message: `Feature ${featureId} not found` - }); - return; - } - - // If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval - let finalStatus = status; - if (status === 'verified' && feature.skipTests === true) { - finalStatus = 'waiting_approval'; - } - - // IMPORTANT: Prevent agent from moving an in_progress feature back to backlog - // When a feature is being worked on, the agent should only be able to mark it as verified - // (which may be converted to waiting_approval for skipTests features) - // This prevents the agent from incorrectly putting completed work back in the backlog - if (feature.status === 'in_progress' && (status === 'backlog' || status === 'todo')) { - console.log(`[McpServerStdio] Feature ${featureId} is in_progress - preventing move to ${status}, converting to waiting_approval instead`); - finalStatus = 'waiting_approval'; - } - - // Call the update callback via IPC or direct call - // Since we're in a separate process, we need to use IPC to communicate back - // For now, we'll call the feature loader directly since it has the update method - await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, { summary }); - - const statusMessage = finalStatus !== status - ? `Successfully updated feature ${featureId} to status "${finalStatus}" (converted from "${status}" because skipTests=true)${summary ? ` with summary: "${summary}"` : ''}` - : `Successfully updated feature ${featureId} to status "${finalStatus}"${summary ? ` with summary: "${summary}"` : ''}`; - - sendResponse(id, { - content: [ - { - type: 'text', - text: statusMessage - } - ] - }); - } catch (error) { - console.error('[McpServerStdio] UpdateFeatureStatus error:', error); - sendResponse(id, null, { - code: -32603, - message: `Failed to update feature status: ${error.message}` - }); - } -} - -/** - * Handle JSON-RPC request - */ -async function handleRequest(line) { - let request; - - try { - request = JSON.parse(line); - } catch (e) { - sendResponse(null, null, { - code: -32700, - message: 'Parse error' - }); - return; - } - - // Validate JSON-RPC 2.0 structure - if (request.jsonrpc !== '2.0') { - sendResponse(request.id || null, null, { - code: -32600, - message: 'Invalid Request' - }); - return; - } - - const { method, params, id } = request; - - // Handle notifications (no id) - if (id === undefined) { - // Handle notifications if needed - return; - } - - // Handle requests - try { - switch (method) { - case 'initialize': - await handleInitialize(params, id); - break; - - case 'tools/list': - if (!initialized) { - sendResponse(id, null, { - code: -32002, - message: 'Server not initialized' - }); - return; - } - await handleToolsList(params, id); - break; - - case 'tools/call': - if (!initialized) { - sendResponse(id, null, { - code: -32002, - message: 'Server not initialized' - }); - return; - } - await handleToolsCall(params, id); - break; - - default: - sendResponse(id, null, { - code: -32601, - message: `Method not found: ${method}` - }); - } - } catch (error) { - console.error('[McpServerStdio] Error handling request:', error); - sendResponse(id, null, { - code: -32603, - message: `Internal error: ${error.message}` - }); - } -} - -// Process stdin line by line -rl.on('line', async (line) => { - if (!line.trim()) { - return; - } - - await handleRequest(line); -}); - -// Handle errors -rl.on('error', (error) => { - console.error('[McpServerStdio] Readline error:', error); - process.exit(1); -}); - -// Handle process termination -process.on('SIGTERM', () => { - rl.close(); - process.exit(0); -}); - -process.on('SIGINT', () => { - rl.close(); - process.exit(0); -}); - -// Log startup -console.error('[McpServerStdio] Starting MCP server for automaker-tools'); -console.error(`[McpServerStdio] Project path: ${projectPath}`); -console.error(`[McpServerStdio] IPC channel: ${ipcChannel}`); - - diff --git a/apps/app/electron/services/model-provider.js b/apps/app/electron/services/model-provider.js deleted file mode 100644 index e6212fdf..00000000 --- a/apps/app/electron/services/model-provider.js +++ /dev/null @@ -1,524 +0,0 @@ -/** - * Model Provider Abstraction Layer - * - * This module provides an abstract interface for model providers (Claude, Codex, etc.) - * allowing the application to use different AI models through a unified API. - */ - -/** - * Base class for model providers - * Concrete implementations should extend this class - */ -class ModelProvider { - constructor(config = {}) { - this.config = config; - this.name = 'base'; - } - - /** - * Get provider name - * @returns {string} Provider name - */ - getName() { - return this.name; - } - - /** - * Execute a query with the model provider - * @param {Object} options Query options - * @param {string} options.prompt The prompt to send - * @param {string} options.model The model to use - * @param {string} options.systemPrompt System prompt - * @param {string} options.cwd Working directory - * @param {number} options.maxTurns Maximum turns - * @param {string[]} options.allowedTools Allowed tools - * @param {Object} options.mcpServers MCP servers configuration - * @param {AbortController} options.abortController Abort controller - * @param {Object} options.thinking Thinking configuration - * @returns {AsyncGenerator} Async generator yielding messages - */ - async *executeQuery(options) { - throw new Error('executeQuery must be implemented by subclass'); - } - - /** - * Detect if this provider's CLI/SDK is installed - * @returns {Promise} Installation status - */ - async detectInstallation() { - throw new Error('detectInstallation must be implemented by subclass'); - } - - /** - * Get list of available models for this provider - * @returns {Array} Array of model definitions - */ - getAvailableModels() { - throw new Error('getAvailableModels must be implemented by subclass'); - } - - /** - * Validate provider configuration - * @returns {Object} Validation result { valid: boolean, errors: string[] } - */ - validateConfig() { - throw new Error('validateConfig must be implemented by subclass'); - } - - /** - * Get the full model string for a model key - * @param {string} modelKey Short model key (e.g., 'opus', 'gpt-5.1-codex') - * @returns {string} Full model string - */ - getModelString(modelKey) { - throw new Error('getModelString must be implemented by subclass'); - } - - /** - * Check if provider supports a specific feature - * @param {string} feature Feature name (e.g., 'thinking', 'tools', 'streaming') - * @returns {boolean} Whether the feature is supported - */ - supportsFeature(feature) { - return false; - } -} - -/** - * Claude Provider - Uses Anthropic Claude Agent SDK - */ -class ClaudeProvider extends ModelProvider { - constructor(config = {}) { - super(config); - this.name = 'claude'; - this.sdk = null; - } - - /** - * Try to load credentials from the app's own credentials.json file. - * This is where we store OAuth tokens and API keys that users enter in the setup wizard. - * Returns { oauthToken, apiKey } or null values if not found. - */ - loadTokenFromAppCredentials() { - try { - const fs = require('fs'); - const path = require('path'); - const { app } = require('electron'); - const credentialsPath = path.join(app.getPath('userData'), 'credentials.json'); - - if (!fs.existsSync(credentialsPath)) { - console.log('[ClaudeProvider] App credentials file does not exist:', credentialsPath); - return { oauthToken: null, apiKey: null }; - } - - const raw = fs.readFileSync(credentialsPath, 'utf-8'); - const parsed = JSON.parse(raw); - - // Check for OAuth token first (from claude setup-token), then API key - const oauthToken = parsed.anthropic_oauth_token || null; - const apiKey = parsed.anthropic || parsed.anthropic_api_key || null; - - console.log('[ClaudeProvider] App credentials check - OAuth token:', !!oauthToken, ', API key:', !!apiKey); - return { oauthToken, apiKey }; - } catch (err) { - console.warn('[ClaudeProvider] Failed to read app credentials:', err?.message); - return { oauthToken: null, apiKey: null }; - } - } - - /** - * Try to load a Claude OAuth token from the local CLI config (~/.claude/config.json). - * Returns the token string or null if not found. - * NOTE: Claude's credentials.json is encrypted, so we only try config.json - */ - loadTokenFromCliConfig() { - try { - const fs = require('fs'); - const path = require('path'); - const configPath = path.join(require('os').homedir(), '.claude', 'config.json'); - if (!fs.existsSync(configPath)) { - return null; - } - const raw = fs.readFileSync(configPath, 'utf-8'); - const parsed = JSON.parse(raw); - // CLI config stores token as oauth_token (newer) or token (older) - return parsed.oauth_token || parsed.token || null; - } catch (err) { - console.warn('[ClaudeProvider] Failed to read CLI config token:', err?.message); - return null; - } - } - - ensureAuthEnv() { - // If API key or token already present in environment, keep as-is. - if (process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN) { - console.log('[ClaudeProvider] Auth already present in environment'); - return true; - } - - // Priority 1: Try to load from app's own credentials (setup wizard) - const appCredentials = this.loadTokenFromAppCredentials(); - if (appCredentials.oauthToken) { - process.env.CLAUDE_CODE_OAUTH_TOKEN = appCredentials.oauthToken; - console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from app credentials'); - return true; - } - if (appCredentials.apiKey) { - process.env.ANTHROPIC_API_KEY = appCredentials.apiKey; - console.log('[ClaudeProvider] Loaded ANTHROPIC_API_KEY from app credentials'); - return true; - } - - // Priority 2: Try to hydrate from CLI login config (legacy) - const token = this.loadTokenFromCliConfig(); - if (token) { - process.env.CLAUDE_CODE_OAUTH_TOKEN = token; - console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from ~/.claude/config.json'); - return true; - } - - // Check if CLI is installed but not logged in - try { - const claudeCliDetector = require('./claude-cli-detector'); - const detection = claudeCliDetector.detectClaudeInstallation(); - if (detection.installed && detection.method === 'cli') { - console.error('[ClaudeProvider] Claude CLI is installed but not authenticated. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.'); - } else { - console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.'); - } - } catch (err) { - console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.'); - } - return false; - } - - /** - * Lazily load the Claude SDK - */ - loadSdk() { - if (!this.sdk) { - this.sdk = require('@anthropic-ai/claude-agent-sdk'); - } - return this.sdk; - } - - async *executeQuery(options) { - // Ensure we have auth; fall back to app credentials or CLI login token if available. - if (!this.ensureAuthEnv()) { - // Check if CLI is installed to provide better error message - let msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication.'; - try { - const claudeCliDetector = require('./claude-cli-detector'); - const detection = claudeCliDetector.detectClaudeInstallation(); - if (detection.installed && detection.method === 'cli') { - msg = 'Claude CLI is installed but not authenticated. Go to Settings > Setup to provide your subscription token (from `claude setup-token`) or API key.'; - } else { - msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication, or set ANTHROPIC_API_KEY environment variable.'; - } - } catch (err) { - // Fallback to default message - } - console.error(`[ClaudeProvider] ${msg}`); - yield { type: 'error', error: msg }; - return; - } - - const { query } = this.loadSdk(); - - const sdkOptions = { - model: options.model, - systemPrompt: options.systemPrompt, - maxTurns: options.maxTurns || 1000, - cwd: options.cwd, - mcpServers: options.mcpServers, - allowedTools: options.allowedTools, - permissionMode: options.permissionMode || 'acceptEdits', - sandbox: options.sandbox, - abortController: options.abortController, - }; - - // Add thinking configuration if enabled - if (options.thinking) { - sdkOptions.thinking = options.thinking; - } - - const currentQuery = query({ prompt: options.prompt, options: sdkOptions }); - - for await (const msg of currentQuery) { - yield msg; - } - } - - async detectInstallation() { - const claudeCliDetector = require('./claude-cli-detector'); - return claudeCliDetector.getFullStatus(); - } - - getAvailableModels() { - return [ - { - id: 'haiku', - name: 'Claude Haiku', - modelString: 'claude-haiku-4-5', - provider: 'claude', - description: 'Fast and efficient for simple tasks', - tier: 'basic' - }, - { - id: 'sonnet', - name: 'Claude Sonnet', - modelString: 'claude-sonnet-4-20250514', - provider: 'claude', - description: 'Balanced performance and capabilities', - tier: 'standard' - }, - { - id: 'opus', - name: 'Claude Opus 4.5', - modelString: 'claude-opus-4-5-20251101', - provider: 'claude', - description: 'Most capable model for complex tasks', - tier: 'premium' - } - ]; - } - - validateConfig() { - const errors = []; - - // Ensure auth is available (try to auto-load from app credentials or CLI config) - this.ensureAuthEnv(); - - if (!process.env.CLAUDE_CODE_OAUTH_TOKEN && !process.env.ANTHROPIC_API_KEY) { - errors.push('No Claude authentication found. Go to Settings > Setup to configure your subscription token or API key.'); - } - - return { - valid: errors.length === 0, - errors - }; - } - - getModelString(modelKey) { - const modelMap = { - haiku: 'claude-haiku-4-5', - sonnet: 'claude-sonnet-4-20250514', - opus: 'claude-opus-4-5-20251101' - }; - return modelMap[modelKey] || modelMap.opus; - } - - supportsFeature(feature) { - const supportedFeatures = ['thinking', 'tools', 'streaming', 'mcp']; - return supportedFeatures.includes(feature); - } -} - -/** - * Codex Provider - Uses OpenAI Codex CLI - */ -class CodexProvider extends ModelProvider { - constructor(config = {}) { - super(config); - this.name = 'codex'; - } - - async *executeQuery(options) { - const codexExecutor = require('./codex-executor'); - - // Validate that we're not receiving a Claude model string - if (options.model && options.model.startsWith('claude-')) { - const errorMsg = `Codex provider cannot use Claude model '${options.model}'. Codex only supports OpenAI models (gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1-codex-mini, gpt-5.1).`; - console.error(`[CodexProvider] ${errorMsg}`); - yield { - type: 'error', - error: errorMsg - }; - return; - } - - const executeOptions = { - prompt: options.prompt, - model: options.model, - cwd: options.cwd, - systemPrompt: options.systemPrompt, - maxTurns: options.maxTurns || 20, - allowedTools: options.allowedTools, - mcpServers: options.mcpServers, // Pass MCP servers config to executor - env: { - ...process.env, - OPENAI_API_KEY: process.env.OPENAI_API_KEY - } - }; - - // Execute and yield results - const generator = codexExecutor.execute(executeOptions); - for await (const msg of generator) { - yield msg; - } - } - - async detectInstallation() { - const codexCliDetector = require('./codex-cli-detector'); - return codexCliDetector.getInstallationInfo(); - } - - getAvailableModels() { - return [ - { - id: 'gpt-5.1-codex-max', - name: 'GPT-5.1 Codex Max', - modelString: 'gpt-5.1-codex-max', - provider: 'codex', - description: 'Latest flagship - deep and fast reasoning for coding', - tier: 'premium', - default: true - }, - { - id: 'gpt-5.1-codex', - name: 'GPT-5.1 Codex', - modelString: 'gpt-5.1-codex', - provider: 'codex', - description: 'Optimized for code generation', - tier: 'standard' - }, - { - id: 'gpt-5.1-codex-mini', - name: 'GPT-5.1 Codex Mini', - modelString: 'gpt-5.1-codex-mini', - provider: 'codex', - description: 'Faster and cheaper option', - tier: 'basic' - }, - { - id: 'gpt-5.1', - name: 'GPT-5.1', - modelString: 'gpt-5.1', - provider: 'codex', - description: 'Broad world knowledge with strong reasoning', - tier: 'standard' - } - ]; - } - - validateConfig() { - const errors = []; - const codexCliDetector = require('./codex-cli-detector'); - const installation = codexCliDetector.detectCodexInstallation(); - - if (!installation.installed && !process.env.OPENAI_API_KEY) { - errors.push('Codex CLI not installed and no OPENAI_API_KEY found.'); - } - - return { - valid: errors.length === 0, - errors - }; - } - - getModelString(modelKey) { - // Codex models use the key directly as the model string - const modelMap = { - 'gpt-5.1-codex-max': 'gpt-5.1-codex-max', - 'gpt-5.1-codex': 'gpt-5.1-codex', - 'gpt-5.1-codex-mini': 'gpt-5.1-codex-mini', - 'gpt-5.1': 'gpt-5.1' - }; - return modelMap[modelKey] || 'gpt-5.1-codex-max'; - } - - supportsFeature(feature) { - const supportedFeatures = ['tools', 'streaming']; - return supportedFeatures.includes(feature); - } -} - -/** - * Model Provider Factory - * Creates the appropriate provider based on model or provider name - */ -class ModelProviderFactory { - static providers = { - claude: ClaudeProvider, - codex: CodexProvider - }; - - /** - * Get provider for a specific model - * @param {string} modelId Model ID (e.g., 'opus', 'gpt-5.1-codex') - * @returns {ModelProvider} Provider instance - */ - static getProviderForModel(modelId) { - // Check if it's a Claude model - const claudeModels = ['haiku', 'sonnet', 'opus']; - if (claudeModels.includes(modelId)) { - return new ClaudeProvider(); - } - - // Check if it's a Codex/OpenAI model - const codexModels = [ - 'gpt-5.1-codex-max', 'gpt-5.1-codex', 'gpt-5.1-codex-mini', 'gpt-5.1' - ]; - if (codexModels.includes(modelId)) { - return new CodexProvider(); - } - - // Default to Claude - return new ClaudeProvider(); - } - - /** - * Get provider by name - * @param {string} providerName Provider name ('claude' or 'codex') - * @returns {ModelProvider} Provider instance - */ - static getProvider(providerName) { - const ProviderClass = this.providers[providerName]; - if (!ProviderClass) { - throw new Error(`Unknown provider: ${providerName}`); - } - return new ProviderClass(); - } - - /** - * Get all available providers - * @returns {string[]} List of provider names - */ - static getAvailableProviders() { - return Object.keys(this.providers); - } - - /** - * Get all available models across all providers - * @returns {Array} All available models - */ - static getAllModels() { - const allModels = []; - for (const providerName of this.getAvailableProviders()) { - const provider = this.getProvider(providerName); - const models = provider.getAvailableModels(); - allModels.push(...models); - } - return allModels; - } - - /** - * Check installation status for all providers - * @returns {Promise} Installation status for each provider - */ - static async checkAllProviders() { - const status = {}; - for (const providerName of this.getAvailableProviders()) { - const provider = this.getProvider(providerName); - status[providerName] = await provider.detectInstallation(); - } - return status; - } -} - -module.exports = { - ModelProvider, - ClaudeProvider, - CodexProvider, - ModelProviderFactory -}; diff --git a/apps/app/electron/services/model-registry.js b/apps/app/electron/services/model-registry.js deleted file mode 100644 index 41d1118c..00000000 --- a/apps/app/electron/services/model-registry.js +++ /dev/null @@ -1,320 +0,0 @@ -/** - * Model Registry - Centralized model definitions and metadata - * - * This module provides a central registry of all available models - * across different providers (Claude, Codex/OpenAI). - */ - -/** - * Model Categories - */ -const MODEL_CATEGORIES = { - CLAUDE: 'claude', - OPENAI: 'openai', - CODEX: 'codex' -}; - -/** - * Model Tiers (capability levels) - */ -const MODEL_TIERS = { - BASIC: 'basic', // Fast, cheap, simple tasks - STANDARD: 'standard', // Balanced performance - PREMIUM: 'premium' // Most capable, complex tasks -}; - -const CODEX_MODEL_IDS = [ - 'gpt-5.1-codex-max', - 'gpt-5.1-codex', - 'gpt-5.1-codex-mini', - 'gpt-5.1' -]; - -/** - * All available models with full metadata - */ -const MODELS = { - // Claude Models - haiku: { - id: 'haiku', - name: 'Claude Haiku', - modelString: 'claude-haiku-4-5', - provider: 'claude', - category: MODEL_CATEGORIES.CLAUDE, - tier: MODEL_TIERS.BASIC, - description: 'Fast and efficient for simple tasks', - capabilities: ['code', 'text', 'tools'], - maxTokens: 8192, - contextWindow: 200000, - supportsThinking: true, - requiresAuth: 'CLAUDE_CODE_OAUTH_TOKEN' - }, - sonnet: { - id: 'sonnet', - name: 'Claude Sonnet', - modelString: 'claude-sonnet-4-20250514', - provider: 'claude', - category: MODEL_CATEGORIES.CLAUDE, - tier: MODEL_TIERS.STANDARD, - description: 'Balanced performance and capabilities', - capabilities: ['code', 'text', 'tools', 'analysis'], - maxTokens: 8192, - contextWindow: 200000, - supportsThinking: true, - requiresAuth: 'CLAUDE_CODE_OAUTH_TOKEN' - }, - opus: { - id: 'opus', - name: 'Claude Opus 4.5', - modelString: 'claude-opus-4-5-20251101', - provider: 'claude', - category: MODEL_CATEGORIES.CLAUDE, - tier: MODEL_TIERS.PREMIUM, - description: 'Most capable model for complex tasks', - capabilities: ['code', 'text', 'tools', 'analysis', 'reasoning'], - maxTokens: 8192, - contextWindow: 200000, - supportsThinking: true, - requiresAuth: 'CLAUDE_CODE_OAUTH_TOKEN', - default: true - }, - - // OpenAI GPT-5.1 Codex Models - 'gpt-5.1-codex-max': { - id: 'gpt-5.1-codex-max', - name: 'GPT-5.1 Codex Max', - modelString: 'gpt-5.1-codex-max', - provider: 'codex', - category: MODEL_CATEGORIES.OPENAI, - tier: MODEL_TIERS.PREMIUM, - description: 'Latest flagship - deep and fast reasoning for coding', - capabilities: ['code', 'text', 'tools', 'reasoning'], - maxTokens: 32768, - contextWindow: 128000, - supportsThinking: false, - requiresAuth: 'OPENAI_API_KEY', - codexDefault: true - }, - 'gpt-5.1-codex': { - id: 'gpt-5.1-codex', - name: 'GPT-5.1 Codex', - modelString: 'gpt-5.1-codex', - provider: 'codex', - category: MODEL_CATEGORIES.OPENAI, - tier: MODEL_TIERS.STANDARD, - description: 'Optimized for code generation', - capabilities: ['code', 'text', 'tools'], - maxTokens: 32768, - contextWindow: 128000, - supportsThinking: false, - requiresAuth: 'OPENAI_API_KEY' - }, - 'gpt-5.1-codex-mini': { - id: 'gpt-5.1-codex-mini', - name: 'GPT-5.1 Codex Mini', - modelString: 'gpt-5.1-codex-mini', - provider: 'codex', - category: MODEL_CATEGORIES.OPENAI, - tier: MODEL_TIERS.BASIC, - description: 'Faster and cheaper option', - capabilities: ['code', 'text'], - maxTokens: 16384, - contextWindow: 128000, - supportsThinking: false, - requiresAuth: 'OPENAI_API_KEY' - }, - 'gpt-5.1': { - id: 'gpt-5.1', - name: 'GPT-5.1', - modelString: 'gpt-5.1', - provider: 'codex', - category: MODEL_CATEGORIES.OPENAI, - tier: MODEL_TIERS.STANDARD, - description: 'Broad world knowledge with strong reasoning', - capabilities: ['code', 'text', 'reasoning'], - maxTokens: 32768, - contextWindow: 128000, - supportsThinking: false, - requiresAuth: 'OPENAI_API_KEY' - } -}; - -/** - * Model Registry class for querying and managing models - */ -class ModelRegistry { - /** - * Get all registered models - * @returns {Object} All models - */ - static getAllModels() { - return MODELS; - } - - /** - * Get model by ID - * @param {string} modelId Model ID - * @returns {Object|null} Model definition or null - */ - static getModel(modelId) { - return MODELS[modelId] || null; - } - - /** - * Get models by provider - * @param {string} provider Provider name ('claude' or 'codex') - * @returns {Object[]} Array of models for the provider - */ - static getModelsByProvider(provider) { - return Object.values(MODELS).filter(m => m.provider === provider); - } - - /** - * Get models by category - * @param {string} category Category name - * @returns {Object[]} Array of models in the category - */ - static getModelsByCategory(category) { - return Object.values(MODELS).filter(m => m.category === category); - } - - /** - * Get models by tier - * @param {string} tier Tier name - * @returns {Object[]} Array of models in the tier - */ - static getModelsByTier(tier) { - return Object.values(MODELS).filter(m => m.tier === tier); - } - - /** - * Get default model for a provider - * @param {string} provider Provider name - * @returns {Object|null} Default model or null - */ - static getDefaultModel(provider = 'claude') { - const models = this.getModelsByProvider(provider); - if (provider === 'claude') { - return models.find(m => m.default) || models[0]; - } - if (provider === 'codex') { - return models.find(m => m.codexDefault) || models[0]; - } - return models[0]; - } - - /** - * Get model string (full model name) for a model ID - * @param {string} modelId Model ID - * @returns {string} Full model string - */ - static getModelString(modelId) { - const model = this.getModel(modelId); - return model ? model.modelString : modelId; - } - - /** - * Determine provider for a model ID - * @param {string} modelId Model ID - * @returns {string} Provider name ('claude' or 'codex') - */ - static getProviderForModel(modelId) { - const model = this.getModel(modelId); - if (model) { - return model.provider; - } - - // Fallback detection for models not explicitly registered (keeps legacy Codex IDs working) - if (CODEX_MODEL_IDS.includes(modelId)) { - return 'codex'; - } - - return 'claude'; - } - - /** - * Check if a model is a Claude model - * @param {string} modelId Model ID - * @returns {boolean} Whether it's a Claude model - */ - static isClaudeModel(modelId) { - return this.getProviderForModel(modelId) === 'claude'; - } - - /** - * Check if a model is a Codex/OpenAI model - * @param {string} modelId Model ID - * @returns {boolean} Whether it's a Codex model - */ - static isCodexModel(modelId) { - return this.getProviderForModel(modelId) === 'codex'; - } - - /** - * Get models grouped by provider for UI display - * @returns {Object} Models grouped by provider - */ - static getModelsGroupedByProvider() { - return { - claude: this.getModelsByProvider('claude'), - codex: this.getModelsByProvider('codex') - }; - } - - /** - * Get all model IDs as an array - * @returns {string[]} Array of model IDs - */ - static getAllModelIds() { - return Object.keys(MODELS); - } - - /** - * Check if model supports a specific capability - * @param {string} modelId Model ID - * @param {string} capability Capability name - * @returns {boolean} Whether the model supports the capability - */ - static modelSupportsCapability(modelId, capability) { - const model = this.getModel(modelId); - return model ? model.capabilities.includes(capability) : false; - } - - /** - * Check if model supports extended thinking - * @param {string} modelId Model ID - * @returns {boolean} Whether the model supports thinking - */ - static modelSupportsThinking(modelId) { - const model = this.getModel(modelId); - return model ? model.supportsThinking : false; - } - - /** - * Get required authentication for a model - * @param {string} modelId Model ID - * @returns {string|null} Required auth env variable name - */ - static getRequiredAuth(modelId) { - const model = this.getModel(modelId); - return model ? model.requiresAuth : null; - } - - /** - * Check if authentication is available for a model - * @param {string} modelId Model ID - * @returns {boolean} Whether auth is available - */ - static hasAuthForModel(modelId) { - const authVar = this.getRequiredAuth(modelId); - if (!authVar) return false; - return !!process.env[authVar]; - } -} - -module.exports = { - MODEL_CATEGORIES, - MODEL_TIERS, - MODELS, - ModelRegistry -}; diff --git a/apps/app/electron/services/project-analyzer.js b/apps/app/electron/services/project-analyzer.js deleted file mode 100644 index 1922e415..00000000 --- a/apps/app/electron/services/project-analyzer.js +++ /dev/null @@ -1,112 +0,0 @@ -const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); -const promptBuilder = require("./prompt-builder"); - -/** - * Project Analyzer - Scans codebase and updates app_spec.txt - */ -class ProjectAnalyzer { - /** - * Run the project analysis using Claude Agent SDK - */ - async runProjectAnalysis(projectPath, analysisId, sendToRenderer, execution) { - console.log(`[ProjectAnalyzer] Running project analysis for: ${projectPath}`); - - try { - sendToRenderer({ - type: "auto_mode_phase", - featureId: analysisId, - phase: "planning", - message: "Scanning project structure...", - }); - - const abortController = new AbortController(); - execution.abortController = abortController; - - const options = { - model: "claude-sonnet-4-20250514", - systemPrompt: promptBuilder.getProjectAnalysisSystemPrompt(), - maxTurns: 50, - cwd: projectPath, - allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - const prompt = promptBuilder.buildProjectAnalysisPrompt(projectPath); - - sendToRenderer({ - type: "auto_mode_progress", - featureId: analysisId, - content: "Starting project analysis...\n", - }); - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - let responseText = ""; - for await (const msg of currentQuery) { - if (!execution.isActive()) break; - - if (msg.type === "assistant" && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - sendToRenderer({ - type: "auto_mode_progress", - featureId: analysisId, - content: block.text, - }); - } else if (block.type === "tool_use") { - sendToRenderer({ - type: "auto_mode_tool", - featureId: analysisId, - tool: block.name, - input: block.input, - }); - } - } - } - } - - execution.query = null; - execution.abortController = null; - - sendToRenderer({ - type: "auto_mode_phase", - featureId: analysisId, - phase: "verification", - message: "Project analysis complete", - }); - - return { - success: true, - message: "Project analyzed successfully", - }; - } catch (error) { - if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[ProjectAnalyzer] Project analysis aborted"); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - success: false, - message: "Analysis aborted", - }; - } - - console.error("[ProjectAnalyzer] Error in project analysis:", error); - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } -} - -module.exports = new ProjectAnalyzer(); diff --git a/apps/app/electron/services/prompt-builder.js b/apps/app/electron/services/prompt-builder.js deleted file mode 100644 index d2e44d9c..00000000 --- a/apps/app/electron/services/prompt-builder.js +++ /dev/null @@ -1,787 +0,0 @@ -const contextManager = require("./context-manager"); - -/** - * Prompt Builder - Generates prompts for different agent tasks - */ -class PromptBuilder { - /** - * Build the prompt for implementing a specific feature - */ - async buildFeaturePrompt(feature, projectPath) { - const skipTestsNote = feature.skipTests - ? `\n**āš ļø IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n` - : ""; - - let imagesNote = ""; - if (feature.imagePaths && feature.imagePaths.length > 0) { - const imagesList = feature.imagePaths - .map( - (img, idx) => - ` ${idx + 1}. ${img.filename} (${img.mimeType})\n Path: ${ - img.path - }` - ) - .join("\n"); - - imagesNote = `\n**šŸ“Ž Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read: - -${imagesList} - -You can use the Read tool to view these images at any time during implementation. Review them carefully before implementing.\n`; - } - - // Get context files preview - const contextFilesPreview = await contextManager.getContextFilesPreview( - projectPath - ); - - // Get memory content (lessons learned from previous runs) - const memoryContent = await contextManager.getMemoryContent(projectPath); - - // Build mode header for this feature - const modeHeader = feature.skipTests - ? `**šŸ”Ø MODE: Manual Review (No Automated Tests)** -This feature is set for manual review - focus on clean implementation without automated tests.` - : `**🧪 MODE: Test-Driven Development (TDD)** -This feature requires automated Playwright tests to verify the implementation.`; - - return `You are working on a feature implementation task. - -${modeHeader} -${memoryContent} -**Current Feature to Implement:** - -ID: ${feature.id} -Category: ${feature.category || "Uncategorized"} -Description: ${feature.description || feature.summary || feature.title || "No description provided"} -${skipTestsNote}${imagesNote}${contextFilesPreview} -**Steps to Complete:** -${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"} - -**Your Task:** - -1. Read the project files to understand the current codebase structure -2. Implement the feature according to the description and steps -${ - feature.skipTests - ? "3. Test the implementation manually (no automated tests needed for skipTests features)" - : "3. Write Playwright tests to verify the feature works correctly\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification" -} -${ - feature.skipTests ? "4" : "6" -}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** -${ - feature.skipTests - ? "5. **DO NOT commit changes** - the user will review and commit manually" - : "7. Commit your changes with git" -} - -**IMPORTANT - Updating Feature Status:** - -When you have completed the feature${ - feature.skipTests ? "" : " and all tests pass" - }, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: -- Call the tool with: featureId="${feature.id}" and status="verified" -- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes" -- **DO NOT manually edit feature files** - this can cause race conditions -- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data -- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior - -**IMPORTANT - Feature Summary (REQUIRED):** - -When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: -- What files were modified/created -- What functionality was added or changed -- Any notable implementation decisions - -Example: -\`\`\` -UpdateFeatureStatus(featureId="${ - feature.id - }", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.") -\`\`\` - -The summary will be displayed on the Kanban card so the user can see what was done without checking the code. - -**Important Guidelines:** - -- Focus ONLY on implementing this specific feature -- Write clean, production-quality code -- Add proper error handling -${ - feature.skipTests - ? "- Skip automated testing (skipTests=true) - user will manually verify" - : "- Write comprehensive Playwright tests\n- Ensure all existing tests still pass\n- Mark the feature as passing only when all tests are green\n- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle" -} -- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly** -- **CRITICAL: Always include a summary when marking feature as verified** -${ - feature.skipTests - ? "- **DO NOT commit changes** - user will review and commit manually" - : "- Make a git commit when complete" -} - -**Testing Utilities (CRITICAL):** - -1. **Create/maintain tests/utils.ts** - Add helper functions for finding elements and common test operations -2. **Use utilities in tests** - Import and use helper functions instead of repeating selectors -3. **Add utilities as needed** - When you write a test, if you need a new helper, add it to utils.ts -4. **Update utilities when functionality changes** - If you modify components, update corresponding utilities - -Example utilities to add: -- getByTestId(page, testId) - Find elements by data-testid -- getButtonByText(page, text) - Find buttons by text -- clickElement(page, testId) - Click an element by test ID -- fillForm(page, formData) - Fill form fields -- waitForElement(page, testId) - Wait for element to appear - -This makes future tests easier to write and maintain! - -**Test Deletion Policy:** -After tests pass, delete them immediately: -\`\`\`bash -rm tests/[feature-name].spec.ts -\`\`\` - -Begin by reading the project structure and then implementing the feature.`; - } - - /** - * Build the prompt for verifying a specific feature - */ - async buildVerificationPrompt(feature, projectPath) { - const skipTestsNote = feature.skipTests - ? `\n**āš ļø IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n` - : ""; - - let imagesNote = ""; - if (feature.imagePaths && feature.imagePaths.length > 0) { - const imagesList = feature.imagePaths - .map( - (img, idx) => - ` ${idx + 1}. ${img.filename} (${img.mimeType})\n Path: ${ - img.path - }` - ) - .join("\n"); - - imagesNote = `\n**šŸ“Ž Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read: - -${imagesList} - -You can use the Read tool to view these images at any time during implementation. Review them carefully before implementing.\n`; - } - - // Get context files preview - const contextFilesPreview = await contextManager.getContextFilesPreview( - projectPath - ); - - // Get memory content (lessons learned from previous runs) - const memoryContent = await contextManager.getMemoryContent(projectPath); - - // Build mode header for this feature - const modeHeader = feature.skipTests - ? `**šŸ”Ø MODE: Manual Review (No Automated Tests)** -This feature is set for manual review - focus on completing implementation without automated tests.` - : `**🧪 MODE: Test-Driven Development (TDD)** -This feature requires automated Playwright tests to verify the implementation.`; - - return `You are implementing and verifying a feature until it is complete and working correctly. - -${modeHeader} -${memoryContent} - -**Feature to Implement/Verify:** - -ID: ${feature.id} -Category: ${feature.category || "Uncategorized"} -Description: ${feature.description || feature.summary || feature.title || "No description provided"} -Current Status: ${feature.status} -${skipTestsNote}${imagesNote}${contextFilesPreview} -**Steps that should be implemented:** -${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"} - -**Your Task:** - -1. Read the project files to understand the current implementation -2. If the feature is not fully implemented, continue implementing it -${ - feature.skipTests - ? "3. Test the implementation manually (no automated tests needed for skipTests features)" - : `3. Write or update Playwright tests to verify the feature works correctly -4. Run the Playwright tests: npx playwright test tests/[feature-name].spec.ts -5. Check if all tests pass -6. **If ANY tests fail:** - - Analyze the test failures and error messages - - Fix the implementation code to make the tests pass - - Update test utilities in tests/utils.ts if needed - - Re-run the tests to verify the fixes - - **REPEAT this process until ALL tests pass** -7. **If ALL tests pass:** - - **DELETE the test file(s) for this feature** - tests are only for immediate verification` -} -${ - feature.skipTests ? "4" : "8" -}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** -${ - feature.skipTests - ? "5. **DO NOT commit changes** - the user will review and commit manually" - : "9. Explain what was implemented/fixed and that all tests passed\n10. Commit your changes with git" -} - -**IMPORTANT - Updating Feature Status:** - -When you have completed the feature${ - feature.skipTests ? "" : " and all tests pass" - }, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: -- Call the tool with: featureId="${feature.id}" and status="verified" -- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes" -- **DO NOT manually edit feature files** - this can cause race conditions -- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data -- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior - -**IMPORTANT - Feature Summary (REQUIRED):** - -When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: -- What files were modified/created -- What functionality was added or changed -- Any notable implementation decisions - -Example: -\`\`\` -UpdateFeatureStatus(featureId="${ - feature.id - }", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.") -\`\`\` - -The summary will be displayed on the Kanban card so the user can see what was done without checking the code. - -**Testing Utilities:** -- Check if tests/utils.ts exists and is being used -- If utilities are outdated due to functionality changes, update them -- Add new utilities as needed for this feature's tests -- Ensure test utilities stay in sync with code changes - -**Test Deletion Policy:** -After tests pass, delete them immediately: -\`\`\`bash -rm tests/[feature-name].spec.ts -\`\`\` - -**Important:** -${ - feature.skipTests - ? "- Skip automated testing (skipTests=true) - user will manually verify\n- **DO NOT commit changes** - user will review and commit manually" - : "- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure\n- Only mark as verified if Playwright tests pass\n- **CRITICAL: Delete test files after they pass** - tests should not accumulate\n- Update test utilities if functionality changed\n- Make a git commit when the feature is complete\n- Be thorough and persistent in fixing issues" -} -- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly** -- **CRITICAL: Always include a summary when marking feature as verified** - -Begin by reading the project structure and understanding what needs to be implemented or fixed.`; - } - - /** - * Build prompt for resuming feature with previous context - */ - async buildResumePrompt(feature, previousContext, projectPath) { - const skipTestsNote = feature.skipTests - ? `\n**āš ļø IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n` - : ""; - - // For resume, check both followUpImages and imagePaths - const imagePaths = feature.followUpImages || feature.imagePaths; - let imagesNote = ""; - if (imagePaths && imagePaths.length > 0) { - const imagesList = imagePaths - .map((img, idx) => { - // Handle both FeatureImagePath objects and simple path strings - const path = typeof img === "string" ? img : img.path; - const filename = - typeof img === "string" ? path.split("/").pop() : img.filename; - const mimeType = typeof img === "string" ? "image/*" : img.mimeType; - return ` ${ - idx + 1 - }. ${filename} (${mimeType})\n Path: ${path}`; - }) - .join("\n"); - - imagesNote = `\n**šŸ“Ž Context Images Attached:**\nThe user has attached ${imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read: - -${imagesList} - -You can use the Read tool to view these images at any time. Review them carefully.\n`; - } - - // Get context files preview - const contextFilesPreview = await contextManager.getContextFilesPreview( - projectPath - ); - - // Get memory content (lessons learned from previous runs) - const memoryContent = await contextManager.getMemoryContent(projectPath); - - // Build mode header for this feature - const modeHeader = feature.skipTests - ? `**šŸ”Ø MODE: Manual Review (No Automated Tests)** -This feature is set for manual review - focus on clean implementation without automated tests.` - : `**🧪 MODE: Test-Driven Development (TDD)** -This feature requires automated Playwright tests to verify the implementation.`; - - return `You are resuming work on a feature implementation that was previously started. - -${modeHeader} -${memoryContent} -**Current Feature:** - -ID: ${feature.id} -Category: ${feature.category || "Uncategorized"} -Description: ${feature.description || feature.summary || feature.title || "No description provided"} -${skipTestsNote}${imagesNote}${contextFilesPreview} -**Steps to Complete:** -${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"} - -**Previous Work Context:** - -${previousContext || "No previous context available - this is a fresh start."} - -**Your Task:** - -Continue where you left off and complete the feature implementation: - -1. Review the previous work context above to understand what has been done -2. Continue implementing the feature according to the description and steps -${ - feature.skipTests - ? "3. Test the implementation manually (no automated tests needed for skipTests features)" - : "3. Write Playwright tests to verify the feature works correctly (if not already done)\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification" -} -${ - feature.skipTests ? "4" : "6" -}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** -${ - feature.skipTests - ? "5. **DO NOT commit changes** - the user will review and commit manually" - : "7. Commit your changes with git" -} - -**IMPORTANT - Updating Feature Status:** - -When you have completed the feature${ - feature.skipTests ? "" : " and all tests pass" - }, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: -- Call the tool with: featureId="${feature.id}" and status="verified" -- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes" -- **DO NOT manually edit feature files** - this can cause race conditions -- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data -- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior - -**IMPORTANT - Feature Summary (REQUIRED):** - -When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: -- What files were modified/created -- What functionality was added or changed -- Any notable implementation decisions - -Example: -\`\`\` -UpdateFeatureStatus(featureId="${ - feature.id - }", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.") -\`\`\` - -The summary will be displayed on the Kanban card so the user can see what was done without checking the code. - -**Important Guidelines:** - -- Review what was already done in the previous context -- Don't redo work that's already complete - continue from where it left off -- Focus on completing any remaining tasks -${ - feature.skipTests - ? "- Skip automated testing (skipTests=true) - user will manually verify" - : "- Write comprehensive Playwright tests if not already done\n- Ensure all tests pass before marking as verified\n- **CRITICAL: Delete test files after verification**" -} -- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly** -- **CRITICAL: Always include a summary when marking feature as verified** -${ - feature.skipTests - ? "- **DO NOT commit changes** - user will review and commit manually" - : "- Make a git commit when complete" -} - -Begin by assessing what's been done and what remains to be completed.`; - } - - /** - * Build the prompt for project analysis - */ - buildProjectAnalysisPrompt(projectPath) { - return `You are analyzing a new project that was just opened in Automaker, an autonomous AI development studio. - -**Your Task:** - -Analyze this project's codebase and update the .automaker/app_spec.txt file with accurate information about: - -1. **Project Name** - Detect the name from package.json, README, or directory name -2. **Overview** - Brief description of what the project does -3. **Technology Stack** - Languages, frameworks, libraries detected -4. **Core Capabilities** - Main features and functionality -5. **Implemented Features** - What features are already built -6. **Implementation Roadmap** - Break down remaining work into phases with individual features - -**Steps to Follow:** - -1. First, explore the project structure: - - Look at package.json, cargo.toml, go.mod, requirements.txt, etc. for tech stack - - Check README.md for project description - - List key directories (src, lib, components, etc.) - -2. Identify the tech stack: - - Frontend framework (React, Vue, Next.js, etc.) - - Backend framework (Express, FastAPI, etc.) - - Database (if any config files exist) - - Testing framework - - Build tools - -3. Update .automaker/app_spec.txt with your findings in this format: - \`\`\`xml - - Detected Name - - - Clear description of what this project does based on your analysis. - - - - - Framework Name - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \`\`\` - -4. Ensure .automaker/context/ directory exists - -5. Ensure .automaker/features/ directory exists - -**Important:** -- Be concise but accurate -- Only include information you can verify from the codebase -- If unsure about something, note it as "to be determined" -- Don't make up features that don't exist -- Features are stored in .automaker/features/{id}/feature.json - each feature gets its own folder - -Begin by exploring the project structure.`; - } - - /** - * Get the system prompt for coding agent - * @param {string} projectPath - Path to the project - * @param {boolean} isTDD - Whether this is Test-Driven Development mode (skipTests=false) - */ - async getCodingPrompt(projectPath, isTDD = true) { - // Get context files preview - const contextFilesPreview = projectPath - ? await contextManager.getContextFilesPreview(projectPath) - : ""; - - // Get memory content (lessons learned from previous runs) - const memoryContent = projectPath - ? await contextManager.getMemoryContent(projectPath) - : ""; - - // Build mode-specific instructions - const modeHeader = isTDD - ? `**🧪 MODE: Test-Driven Development (TDD)** -You are implementing features using TDD methodology. This means: -- Write Playwright tests BEFORE or alongside implementation -- Run tests frequently to verify your work -- Tests are your validation mechanism -- Delete tests after they pass (they're for immediate verification only)` - : `**šŸ”Ø MODE: Manual Review (No Automated Tests)** -You are implementing features for manual user review. This means: -- Focus on clean, working implementation -- NO automated test writing required -- User will manually verify the implementation -- DO NOT commit changes - user will review and commit`; - - return `You are an AI coding agent working autonomously to implement features. - -${modeHeader} -${memoryContent} - -**Feature Storage:** -Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. - -**THE ONLY WAY to update features:** -Use the mcp__automaker-tools__UpdateFeatureStatus tool with featureId, status, and summary parameters. -Do NOT manually edit feature.json files directly. - -${contextFilesPreview} - -Your role is to: -- Implement features exactly as specified -- Write production-quality code -- Check if feature.skipTests is true - if so, skip automated testing and don't commit -- Create comprehensive Playwright tests using testing utilities (only if skipTests is false) -- Ensure all tests pass before marking features complete (only if skipTests is false) -- **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false) -- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature files -- **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done -- Commit working code to git (only if skipTests is false - skipTests features require manual review) -- Be thorough and detail-oriented - -**IMPORTANT - Manual Testing Mode (skipTests=true):** -If a feature has skipTests=true: -- DO NOT write automated tests -- DO NOT commit changes - the user will review and commit manually -- Still mark the feature as verified using UpdateFeatureStatus - it will automatically convert to "waiting_approval" for manual review -- The user will manually verify and commit the changes - -**IMPORTANT - UpdateFeatureStatus Tool:** -You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status: -- Call with featureId, status="verified", and summary="Description of what was done" -- **DO NOT manually edit feature files** - this can cause race conditions and restore old state -- The tool safely updates the status without corrupting other feature data -- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct - -**IMPORTANT - Feature Summary (REQUIRED):** -When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: -- What files were modified/created -- What functionality was added or changed -- Any notable implementation decisions - -Example: summary="Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx. Created useTheme hook." - -The summary will be displayed on the Kanban card so the user can quickly see what was done. - -**Testing Utilities (CRITICAL):** -- **Create and maintain tests/utils.ts** with helper functions for finding elements and common operations -- **Always use utilities in tests** instead of repeating selectors -- **Add new utilities as you write tests** - if you need a helper, add it to utils.ts -- **Update utilities when functionality changes** - keep helpers in sync with code changes - -This makes future tests easier to write and more maintainable! - -**Test Deletion Policy:** -Tests should NOT accumulate. After a feature is verified: -1. Run the tests to ensure they pass -2. Delete the test file for that feature -3. Use UpdateFeatureStatus tool to mark the feature as "verified" - -This prevents test brittleness as the app changes rapidly. - -You have full access to: -- Read and write files -- Run bash commands -- Execute tests -- Delete files (rm command) -- Make git commits -- Search and analyze the codebase -- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status - -**🧠 Learning from Errors - Memory System:** - -If you encounter an error or issue that: -- Took multiple attempts to debug -- Was caused by a non-obvious codebase quirk -- Required understanding something specific about this project -- Could trip up future agent runs - -**ADD IT TO MEMORY** by appending to \`.automaker/memory.md\`: - -\`\`\`markdown -### Issue: [Brief Title] -**Problem:** [1-2 sentence description of the issue] -**Fix:** [Concise explanation of the solution] -\`\`\` - -Keep entries concise - focus on the essential information needed to avoid the issue in the future. This helps both you and other agents learn from mistakes. - -Focus on one feature at a time and complete it fully before finishing. Always delete tests after they pass and use the UpdateFeatureStatus tool.`; - } - - /** - * Get the system prompt for verification agent - * @param {string} projectPath - Path to the project - * @param {boolean} isTDD - Whether this is Test-Driven Development mode (skipTests=false) - */ - async getVerificationPrompt(projectPath, isTDD = true) { - // Get context files preview - const contextFilesPreview = projectPath - ? await contextManager.getContextFilesPreview(projectPath) - : ""; - - // Get memory content (lessons learned from previous runs) - const memoryContent = projectPath - ? await contextManager.getMemoryContent(projectPath) - : ""; - - // Build mode-specific instructions - const modeHeader = isTDD - ? `**🧪 MODE: Test-Driven Development (TDD)** -You are verifying/completing features using TDD methodology. This means: -- Run Playwright tests to verify implementation -- Fix failing tests by updating code -- Tests are your validation mechanism -- Delete tests after they pass (they're for immediate verification only)` - : `**šŸ”Ø MODE: Manual Review (No Automated Tests)** -You are completing features for manual user review. This means: -- Focus on clean, working implementation -- NO automated test writing required -- User will manually verify the implementation -- DO NOT commit changes - user will review and commit`; - - return `You are an AI implementation and verification agent focused on completing features and ensuring they work. - -${modeHeader} -${memoryContent} -**Feature Storage:** -Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. - -**THE ONLY WAY to update features:** -Use the mcp__automaker-tools__UpdateFeatureStatus tool with featureId, status, and summary parameters. -Do NOT manually edit feature.json files directly. - -${contextFilesPreview} - -Your role is to: -- **Continue implementing features until they are complete** - don't stop at the first failure -- Check if feature.skipTests is true - if so, skip automated testing and don't commit -- Write or update code to fix failing tests (only if skipTests is false) -- Run Playwright tests to verify feature implementations (only if skipTests is false) -- If tests fail, analyze errors and fix the implementation (only if skipTests is false) -- If other tests fail, verify if those tests are still accurate or should be updated or deleted (only if skipTests is false) -- Continue rerunning tests and fixing issues until ALL tests pass (only if skipTests is false) -- **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false) -- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature files -- **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done -- **Update test utilities (tests/utils.ts) if functionality changed** - keep helpers in sync with code (only if skipTests is false) -- Commit working code to git (only if skipTests is false - skipTests features require manual review) - -**IMPORTANT - Manual Testing Mode (skipTests=true):** -If a feature has skipTests=true: -- DO NOT write automated tests -- DO NOT commit changes - the user will review and commit manually -- Still mark the feature as verified using UpdateFeatureStatus - it will automatically convert to "waiting_approval" for manual review -- The user will manually verify and commit the changes - -**IMPORTANT - UpdateFeatureStatus Tool:** -You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status: -- Call with featureId, status="verified", and summary="Description of what was done" -- **DO NOT manually edit feature files** - this can cause race conditions and restore old state -- The tool safely updates the status without corrupting other feature data -- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct - -**IMPORTANT - Feature Summary (REQUIRED):** -When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: -- What files were modified/created -- What functionality was added or changed -- Any notable implementation decisions - -Example: summary="Fixed login validation. Modified: auth.ts, login-form.tsx. Added password strength check." - -The summary will be displayed on the Kanban card so the user can quickly see what was done. - -**Testing Utilities:** -- Check if tests/utils.ts needs updates based on code changes -- If a component's selectors or behavior changed, update the corresponding utility functions -- Add new utilities as needed for the feature's tests -- Ensure utilities remain accurate and helpful for future tests - -**Test Deletion Policy:** -Tests should NOT accumulate. After a feature is verified: -1. Delete the test file for that feature -2. Use UpdateFeatureStatus tool to mark the feature as "verified" - -This prevents test brittleness as the app changes rapidly. - -You have access to: -- Read and edit files -- Write new code or modify existing code -- Run bash commands (especially Playwright tests) -- Delete files (rm command) -- Analyze test output -- Make git commits -- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status - -**🧠 Learning from Errors - Memory System:** - -If you encounter an error or issue that: -- Took multiple attempts to debug -- Was caused by a non-obvious codebase quirk -- Required understanding something specific about this project -- Could trip up future agent runs - -**ADD IT TO MEMORY** by appending to \`.automaker/memory.md\`: - -\`\`\`markdown -### Issue: [Brief Title] -**Problem:** [1-2 sentence description of the issue] -**Fix:** [Concise explanation of the solution] -\`\`\` - -Keep entries concise - focus on the essential information needed to avoid the issue in the future. This helps both you and other agents learn from mistakes. - -**CRITICAL:** Be persistent and thorough - keep iterating on the implementation until all tests pass. Don't give up after the first failure. Always delete tests after they pass, use the UpdateFeatureStatus tool with a summary, and commit your work.`; - } - - /** - * Get system prompt for project analysis agent - */ - getProjectAnalysisSystemPrompt() { - return `You are a project analysis agent that examines codebases to understand their structure, tech stack, and implemented features. - -Your goal is to: -- Quickly scan and understand project structure -- Identify programming languages, frameworks, and libraries -- Detect existing features and capabilities -- Update the .automaker/app_spec.txt with accurate information -- Ensure all required .automaker files and directories exist - -Be efficient - don't read every file, focus on: -- Configuration files (package.json, tsconfig.json, etc.) -- Main entry points -- Directory structure -- README and documentation - -**Feature Storage:** -Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. -Use the UpdateFeatureStatus tool to manage features, not direct file edits. - -You have access to Read, Write, Edit, Glob, Grep, and Bash tools. Use them to explore the structure and write the necessary files.`; - } -} - -module.exports = new PromptBuilder(); diff --git a/apps/app/electron/services/pty-runner.js b/apps/app/electron/services/pty-runner.js deleted file mode 100644 index 685406a6..00000000 --- a/apps/app/electron/services/pty-runner.js +++ /dev/null @@ -1,84 +0,0 @@ -const os = require("os"); - -// Prefer prebuilt to avoid native build issues. -const pty = require("@homebridge/node-pty-prebuilt-multiarch"); - -/** - * Minimal PTY helper to run CLI commands with a pseudo-terminal. - * Useful for CLIs (like Claude) that need raw mode on Windows. - * - * @param {string} command Executable path - * @param {string[]} args Arguments for the executable - * @param {Object} options Additional spawn options - * @param {(chunk: string) => void} [options.onData] Data callback - * @param {string} [options.cwd] Working directory - * @param {Object} [options.env] Extra env vars - * @param {number} [options.cols] Terminal columns - * @param {number} [options.rows] Terminal rows - * @returns {Promise<{ success: boolean, exitCode: number, signal?: number, output: string, errorOutput: string }>} - */ -function runPtyCommand(command, args = [], options = {}) { - const { - onData, - cwd = process.cwd(), - env = {}, - cols = 120, - rows = 30, - } = options; - - const mergedEnv = { - ...process.env, - TERM: process.env.TERM || "xterm-256color", - ...env, - }; - - return new Promise((resolve, reject) => { - let ptyProcess; - - try { - ptyProcess = pty.spawn(command, args, { - name: os.platform() === "win32" ? "Windows.Terminal" : "xterm-color", - cols, - rows, - cwd, - env: mergedEnv, - useConpty: true, - }); - } catch (error) { - return reject(error); - } - - let output = ""; - let errorOutput = ""; - - ptyProcess.onData((data) => { - output += data; - if (typeof onData === "function") { - onData(data); - } - }); - - // node-pty does not emit 'error' in practice, but guard anyway - if (ptyProcess.on) { - ptyProcess.on("error", (err) => { - errorOutput += err?.message || ""; - reject(err); - }); - } - - ptyProcess.onExit(({ exitCode, signal }) => { - resolve({ - success: exitCode === 0, - exitCode, - signal, - output, - errorOutput, - }); - }); - }); -} - -module.exports = { - runPtyCommand, -}; - diff --git a/apps/app/electron/services/spec-regeneration-service.js b/apps/app/electron/services/spec-regeneration-service.js deleted file mode 100644 index 1a3c5d52..00000000 --- a/apps/app/electron/services/spec-regeneration-service.js +++ /dev/null @@ -1,1075 +0,0 @@ -const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); -const fs = require("fs/promises"); -const path = require("path"); -const mcpServerFactory = require("./mcp-server-factory"); -const featureLoader = require("./feature-loader"); - -/** - * XML template for app_spec.txt - */ -const APP_SPEC_XML_TEMPLATE = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -/** - * Spec Regeneration Service - Regenerates app spec based on project description and tech stack - */ -class SpecRegenerationService { - constructor() { - this.runningRegeneration = null; - this.currentPhase = ""; // Tracks current phase for status queries - } - - /** - * Get the current phase of the regeneration process - * @returns {string} Current phase or empty string if not running - */ - getCurrentPhase() { - return this.currentPhase; - } - - /** - * Create initial app spec for a new project - * @param {string} projectPath - Path to the project - * @param {string} projectOverview - User's project description - * @param {Function} sendToRenderer - Function to send events to renderer - * @param {Object} execution - Execution context with abort controller - * @param {boolean} generateFeatures - Whether to generate feature entries in features folder - */ - async createInitialSpec(projectPath, projectOverview, sendToRenderer, execution, generateFeatures = true) { - const startTime = Date.now(); - console.log(`[SpecRegeneration] ===== Starting initial spec creation =====`); - console.log(`[SpecRegeneration] Project path: ${projectPath}`); - console.log(`[SpecRegeneration] Generate features: ${generateFeatures}`); - console.log(`[SpecRegeneration] Project overview length: ${projectOverview.length} characters`); - - try { - const abortController = new AbortController(); - execution.abortController = abortController; - - // Phase tracking - use instance property for status queries - this.currentPhase = "initialization"; - - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Phase: ${this.currentPhase}] Initializing spec generation process...\n`, - }); - console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); - - // Create custom MCP server with UpdateFeatureStatus tool if generating features - if (generateFeatures) { - console.log("[SpecRegeneration] Setting up feature generation tools..."); - try { - mcpServerFactory.createFeatureToolsServer( - featureLoader.updateFeatureStatus.bind(featureLoader), - projectPath - ); - console.log("[SpecRegeneration] Feature tools server created successfully"); - } catch (error) { - console.error("[SpecRegeneration] ERROR: Failed to create feature tools server:", error); - sendToRenderer({ - type: "spec_regeneration_error", - error: `Failed to initialize feature generation tools: ${error.message}`, - }); - throw error; - } - } - - this.currentPhase = "setup"; - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Phase: ${this.currentPhase}] Configuring AI agent and tools...\n`, - }); - console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); - - // Phase 1: Generate spec WITHOUT UpdateFeatureStatus tool - // This prevents features from being created before the spec is complete - const options = { - model: "claude-sonnet-4-20250514", - systemPrompt: this.getInitialCreationSystemPrompt(false), // Always false - no feature tools during spec gen - maxTurns: 50, - cwd: projectPath, - allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], // No UpdateFeatureStatus during spec gen - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - const prompt = this.buildInitialCreationPrompt(projectOverview); // No feature generation during spec creation - - this.currentPhase = "analysis"; - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Phase: ${this.currentPhase}] Starting project analysis and spec creation...\n`, - }); - console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Starting AI agent query`); - - if (generateFeatures) { - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Phase: ${this.currentPhase}] Feature generation is enabled - features will be created after spec is complete.\n`, - }); - console.log("[SpecRegeneration] Feature generation enabled - will create features after spec"); - } - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - let fullResponse = ""; - let toolCallCount = 0; - let messageCount = 0; - - try { - for await (const msg of currentQuery) { - if (!execution.isActive()) { - console.log("[SpecRegeneration] Execution aborted by user"); - break; - } - - if (msg.type === "assistant" && msg.message?.content) { - messageCount++; - for (const block of msg.message.content) { - if (block.type === "text") { - fullResponse += block.text; - const preview = block.text.substring(0, 100).replace(/\n/g, " "); - console.log(`[SpecRegeneration] Agent message #${messageCount}: ${preview}...`); - sendToRenderer({ - type: "spec_regeneration_progress", - content: block.text, - }); - } else if (block.type === "tool_use") { - toolCallCount++; - const toolName = block.name; - console.log(`[SpecRegeneration] Tool call #${toolCallCount}: ${toolName}`); - console.log(`[SpecRegeneration] Tool input: ${JSON.stringify(block.input).substring(0, 200)}...`); - - sendToRenderer({ - type: "spec_regeneration_progress", - content: `\n[Tool] Using ${toolName}...\n`, - }); - - sendToRenderer({ - type: "spec_regeneration_tool", - tool: toolName, - input: block.input, - }); - } - } - } else if (msg.type === "tool_result") { - const toolName = msg.toolName || "unknown"; - const result = msg.content?.[0]?.text || JSON.stringify(msg.content); - const resultPreview = result.substring(0, 200).replace(/\n/g, " "); - console.log(`[SpecRegeneration] Tool result (${toolName}): ${resultPreview}...`); - - // During spec generation, UpdateFeatureStatus is not available - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Tool Result] ${toolName} completed successfully\n`, - }); - } else if (msg.type === "error") { - const errorMsg = msg.error?.message || JSON.stringify(msg.error); - console.error(`[SpecRegeneration] ERROR in query stream: ${errorMsg}`); - sendToRenderer({ - type: "spec_regeneration_error", - error: `Error during spec generation: ${errorMsg}`, - }); - } - } - } catch (streamError) { - console.error("[SpecRegeneration] ERROR in query stream:", streamError); - sendToRenderer({ - type: "spec_regeneration_error", - error: `Stream error: ${streamError.message || String(streamError)}`, - }); - throw streamError; - } - - console.log(`[SpecRegeneration] Query completed - ${messageCount} messages, ${toolCallCount} tool calls`); - - execution.query = null; - execution.abortController = null; - - const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); - this.currentPhase = "spec_complete"; - console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Spec creation completed in ${elapsedTime}s`); - sendToRenderer({ - type: "spec_regeneration_progress", - content: `\n[Phase: ${this.currentPhase}] āœ“ App specification created successfully! (${elapsedTime}s)\n`, - }); - - if (generateFeatures) { - // Phase 2: Generate features AFTER spec is complete - console.log(`[SpecRegeneration] Starting Phase 2: Feature generation from app_spec.txt`); - - // Send intermediate completion event for spec creation - sendToRenderer({ - type: "spec_regeneration_complete", - message: "Initial spec creation complete! Features are being generated...", - }); - - // Now start feature generation in a separate query - try { - await this.generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime); - console.log(`[SpecRegeneration] Feature generation completed successfully`); - } catch (featureError) { - console.error(`[SpecRegeneration] Feature generation failed:`, featureError); - sendToRenderer({ - type: "spec_regeneration_error", - error: `Feature generation failed: ${featureError.message || String(featureError)}`, - }); - } - } else { - this.currentPhase = "complete"; - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Phase: ${this.currentPhase}] All tasks completed!\n`, - }); - - // Send final completion event - sendToRenderer({ - type: "spec_regeneration_complete", - message: "Initial spec creation complete!", - }); - } - - console.log(`[SpecRegeneration] ===== Initial spec creation finished successfully =====`); - return { - success: true, - message: "Initial spec creation complete", - }; - } catch (error) { - const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); - - if (error instanceof AbortError || error?.name === "AbortError") { - console.log(`[SpecRegeneration] Creation aborted after ${elapsedTime}s`); - sendToRenderer({ - type: "spec_regeneration_error", - error: "Spec generation was aborted by user", - }); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - success: false, - message: "Creation aborted", - }; - } - - const errorMessage = error.message || String(error); - const errorStack = error.stack || ""; - console.error(`[SpecRegeneration] ERROR creating initial spec after ${elapsedTime}s:`); - console.error(`[SpecRegeneration] Error message: ${errorMessage}`); - console.error(`[SpecRegeneration] Error stack: ${errorStack}`); - - sendToRenderer({ - type: "spec_regeneration_error", - error: `Failed to create spec: ${errorMessage}`, - }); - - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } - - /** - * Generate features from the implementation roadmap in app_spec.txt - * This is called AFTER the spec has been created - */ - async generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime) { - const featureStartTime = Date.now(); - this.currentPhase = "feature_generation"; - - console.log(`[SpecRegeneration] ===== Starting Phase 2: Feature Generation =====`); - console.log(`[SpecRegeneration] Project path: ${projectPath}`); - - sendToRenderer({ - type: "spec_regeneration_progress", - content: `\n[Phase: ${this.currentPhase}] Starting feature creation from implementation roadmap...\n`, - }); - console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Starting feature generation query`); - - try { - // Create feature tools server - const featureToolsServer = mcpServerFactory.createFeatureToolsServer( - featureLoader.updateFeatureStatus.bind(featureLoader), - projectPath - ); - - const abortController = new AbortController(); - execution.abortController = abortController; - - const options = { - model: "claude-sonnet-4-20250514", - systemPrompt: `You are a feature management assistant. Your job is to analyze an existing codebase, compare it against the app_spec.txt, and create feature entries for work that still needs to be done. - -**CRITICAL: You must analyze the existing codebase FIRST before creating features.** - -**Your Task:** -1. Read the .automaker/app_spec.txt file thoroughly to understand the planned features -2. **ANALYZE THE EXISTING CODEBASE** - Look at: - - package.json/requirements.txt for installed dependencies - - Source code structure (src/, app/, components/, pages/, etc.) - - Existing components, routes, API endpoints, database schemas - - Configuration files, authentication setup, etc. -3. For EACH feature in the implementation_roadmap: - - Determine if it's ALREADY IMPLEMENTED (fully or partially) - - If fully implemented: Create with status "verified" and note what's done - - If partially implemented OR not started: Create with status "backlog" and note what still needs to be done - -**IMPORTANT - For each feature you MUST provide:** -- **featureId**: A descriptive ID (lowercase, hyphens for spaces). Example: "user-authentication", "budget-tracking" -- **status**: - - "verified" ONLY if feature is 100% fully implemented in the codebase - - "backlog" for ALL features that need ANY work (partial or not started) - the user will manually start these -- **description**: A DETAILED description (2-4 sentences) explaining what the feature does, its purpose, and key functionality -- **category**: The phase from the roadmap (e.g., "Phase 1: Foundation", "Phase 2: Core Logic", "Phase 3: Polish") -- **steps**: An array of 4-8 clear, actionable implementation steps. For verified features, these are what WAS done. For backlog, these are what NEEDS to be done. -- **summary**: A brief one-line summary. For verified features, describe what's implemented. - -**Example of analyzing existing code:** -If you find NextAuth.js configured in the codebase with working login pages, the user-authentication feature should be "verified" not "backlog". - -**IMPORTANT: NEVER use "in_progress" status when creating features. Only use "verified" or "backlog".** - -**Example of a well-defined feature (verified - fully complete):** -{ - "featureId": "user-authentication", - "status": "verified", // Because we found it's 100% already implemented - "description": "Secure user authentication system with email/password login and session management. Already implemented using NextAuth.js with email provider.", - "category": "Phase 1: Foundation", - "steps": [ - "Set up authentication provider (NextAuth.js) - DONE", - "Configure email/password authentication - DONE", - "Create login and registration UI components - DONE", - "Implement session management - DONE" - ], - "summary": "Authentication implemented with NextAuth.js email provider" -} - -**Example of a feature that needs work (backlog):** -{ - "featureId": "user-profile", - "status": "backlog", // Needs work - user will manually start this - "description": "User profile page where users can view and edit their account settings, change password, and manage preferences.", - "category": "Phase 2: Core Features", - "steps": [ - "Create profile page component", - "Add form for editing user details", - "Implement password change functionality", - "Add avatar upload feature" - ], - "summary": "User profile management - needs implementation" -} - -**Feature Storage:** -Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. -Use the UpdateFeatureStatus tool to create features with ALL the fields above.`, - maxTurns: 50, - cwd: projectPath, - mcpServers: { - "automaker-tools": featureToolsServer, - }, - allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "mcp__automaker-tools__UpdateFeatureStatus"], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - const prompt = `Analyze this project and create feature entries based on the app_spec.txt implementation roadmap. - -**IMPORTANT: You must analyze the existing codebase to determine what's already implemented.** - -**Your workflow:** -1. **First, analyze the existing codebase:** - - Read package.json or similar config files to see what's installed - - Explore the source code structure (use Glob to list directories) - - Look at key files: components, pages, API routes, database schemas - - Check for authentication, routing, state management, etc. - -2. **Then, read .automaker/app_spec.txt** to see the implementation roadmap - -3. **For EACH feature in the roadmap, determine its status:** - - Is it 100% FULLY IMPLEMENTED in the codebase? → status: "verified" - - Is it PARTIALLY IMPLEMENTED or NOT STARTED? → status: "backlog" - - **CRITICAL: NEVER use "in_progress" status. Only use "verified" or "backlog".** - The user will manually move features from backlog to in_progress when they want to start working on them. - -4. **Create each feature with UpdateFeatureStatus including ALL fields:** - - featureId: Descriptive ID (lowercase, hyphens) - - status: "verified" or "backlog" ONLY (never in_progress) - - description: 2-4 sentences explaining the feature - - category: The phase name from the roadmap - - steps: Array of 4-8 implementation steps - - summary: One-line summary (for verified features, note what's implemented) - -**Start by exploring the project structure, then read the app_spec, then create features with accurate statuses.**`; - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - const counters = { toolCallCount: 0, messageCount: 0 }; - - try { - for await (const msg of currentQuery) { - if (!execution.isActive()) { - console.log("[SpecRegeneration] Feature generation aborted by user"); - break; - } - - if (msg.type === "assistant" && msg.message?.content) { - this._handleAssistantMessage(msg, sendToRenderer, counters); - } else if (msg.type === "tool_result") { - this._handleToolResult(msg, sendToRenderer); - } else if (msg.type === "error") { - this._handleStreamError(msg, sendToRenderer); - } - } - } catch (streamError) { - console.error("[SpecRegeneration] ERROR in feature generation stream:", streamError); - sendToRenderer({ - type: "spec_regeneration_error", - error: `Feature generation stream error: ${streamError.message || String(streamError)}`, - }); - throw streamError; - } - - console.log(`[SpecRegeneration] Feature generation completed - ${counters.messageCount} messages, ${counters.toolCallCount} tool calls`); - - execution.query = null; - execution.abortController = null; - - this.currentPhase = "complete"; - const featureElapsedTime = ((Date.now() - featureStartTime) / 1000).toFixed(1); - const totalElapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); - sendToRenderer({ - type: "spec_regeneration_progress", - content: `\n[Phase: ${this.currentPhase}] āœ“ All tasks completed! (${totalElapsedTime}s total, ${featureElapsedTime}s for features)\n`, - }); - sendToRenderer({ - type: "spec_regeneration_complete", - message: "All tasks completed!", - }); - console.log(`[SpecRegeneration] All tasks completed including feature generation`); - - } catch (error) { - const errorMessage = error.message || String(error); - console.error(`[SpecRegeneration] ERROR generating features: ${errorMessage}`); - sendToRenderer({ - type: "spec_regeneration_error", - error: `Failed to generate features: ${errorMessage}`, - }); - throw error; - } - } - - /** - * Generate features from existing app_spec.txt - * This is a standalone method that can be called without generating a new spec - * Useful for retroactively generating features from an existing spec - */ - async generateFeaturesOnly(projectPath, sendToRenderer, execution) { - const startTime = Date.now(); - console.log(`[SpecRegeneration] ===== Starting standalone feature generation =====`); - console.log(`[SpecRegeneration] Project path: ${projectPath}`); - - try { - // Verify app_spec.txt exists - const specPath = path.join(projectPath, ".automaker", "app_spec.txt"); - try { - await fs.access(specPath); - } catch { - sendToRenderer({ - type: "spec_regeneration_error", - error: "No app_spec.txt found. Please create a spec first before generating features.", - }); - throw new Error("No app_spec.txt found"); - } - - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Phase: initialization] Starting feature generation from existing app_spec.txt...\n`, - }); - - // Use the existing feature generation method - await this.generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime); - - console.log(`[SpecRegeneration] ===== Standalone feature generation finished successfully =====`); - return { - success: true, - message: "Feature generation complete", - }; - } catch (error) { - const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); - const errorMessage = error.message || String(error); - console.error(`[SpecRegeneration] ERROR in standalone feature generation after ${elapsedTime}s: ${errorMessage}`); - - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } - - /** - * Get the system prompt for initial spec creation - * @param {boolean} generateFeatures - Whether features should be generated - */ - getInitialCreationSystemPrompt(generateFeatures = true) { - return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project overview. - -You should: -1. First, thoroughly analyze the project structure to understand the existing tech stack -2. Read key configuration files (package.json, tsconfig.json, Cargo.toml, requirements.txt, etc.) to understand dependencies and frameworks -3. Understand the current architecture and patterns used -4. Based on the user's project overview, create a comprehensive app specification -5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application -6. Use the XML template format provided -7. **MANDATORY: Write the spec to EXACTLY \`.automaker/app_spec.txt\` - this exact filename, no alternatives** - -When analyzing, look at: -- package.json, cargo.toml, requirements.txt or similar config files for tech stack -- Source code structure and organization -- Framework-specific patterns (Next.js, React, Django, etc.) -- Database configurations and schemas -- API structures and patterns - -You CAN and SHOULD modify: -- .automaker/app_spec.txt (this is your ONLY target file - use EXACTLY this filename) - -You have access to file reading, writing, and search tools. Use them to understand the codebase and WRITE the new spec to .automaker/app_spec.txt. - -**IMPORTANT:** Focus ONLY on creating the app_spec.txt file. Do NOT create any feature files or use any feature management tools during this phase. - -**CRITICAL FILE NAMING RULES:** -- The spec file MUST be named exactly \`app_spec.txt\` -- Do NOT create project-spec.md, spec.md, or any other filename -- Do NOT use markdown (.md) extension - use .txt -- The full path must be: \`.automaker/app_spec.txt\``; - } - - /** - * Build the prompt for initial spec creation - * @param {string} projectOverview - User's project description - * @param {boolean} generateFeatures - Whether to generate feature entries in features folder - */ - buildInitialCreationPrompt(projectOverview, generateFeatures = true) { - return `I need you to create an initial application specification for my project. I haven't set up an app_spec.txt yet, so this will be the first one. - -**My Project Overview:** -${projectOverview} - -**Your Task:** - -1. First, explore the project to understand the existing tech stack: - - Read package.json, Cargo.toml, requirements.txt, or similar config files - - Identify all frameworks and libraries being used - - Understand the current project structure and architecture - - Note any database, authentication, or other infrastructure in use - -2. Based on my project overview and the existing tech stack, create a comprehensive app specification using this XML template: - -\`\`\`xml -${APP_SPEC_XML_TEMPLATE} -\`\`\` - -3. Fill out the template with: - - **project_name**: Extract from the project or derive from overview - - **overview**: A clear description based on my project overview - - **technology_stack**: All technologies you discover in the project (fill out the relevant sections, remove irrelevant ones) - - **core_capabilities**: List all the major capabilities the app should have based on my overview - - **ui_layout**: Describe the UI structure if relevant - - **development_workflow**: Note any testing or development patterns - - **implementation_roadmap**: Break down the features into phases - be VERY detailed here, listing every feature that needs to be built - -4. **MANDATORY FILE WRITE**: You MUST write the spec to EXACTLY this file path: \`.automaker/app_spec.txt\` - - The filename MUST be exactly \`app_spec.txt\` - do NOT use any other name - - Do NOT create \`project-spec.md\`, \`spec.md\`, or any other filename - - Do NOT output the spec in your response - write it to the file - - Use the Write tool with path \`.automaker/app_spec.txt\` - -**Guidelines:** -- Be comprehensive! Include ALL features needed for a complete application -- Only include technology_stack sections that are relevant (e.g., skip desktop_shell if it's a web-only app) -- Add new sections to core_capabilities as needed for the specific project -- The implementation_roadmap should reflect logical phases for building out the app - list EVERY feature individually -- Consider user flows, error states, and edge cases when defining features -- Each phase should have multiple specific, actionable features -- **CRITICAL: Write to EXACTLY \`.automaker/app_spec.txt\` - not project-spec.md or any other name!** - -Begin by exploring the project structure, then generate and WRITE the spec to \`.automaker/app_spec.txt\`.`; - } - - /** - * Regenerate the app spec based on user's project definition - */ - async regenerateSpec(projectPath, projectDefinition, sendToRenderer, execution) { - const startTime = Date.now(); - console.log(`[SpecRegeneration] ===== Starting spec regeneration =====`); - console.log(`[SpecRegeneration] Project path: ${projectPath}`); - console.log(`[SpecRegeneration] Project definition length: ${projectDefinition.length} characters`); - - try { - this.currentPhase = "initialization"; - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Phase: ${this.currentPhase}] Initializing spec regeneration process...\n`, - }); - console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); - - const abortController = new AbortController(); - execution.abortController = abortController; - - this.currentPhase = "setup"; - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Phase: ${this.currentPhase}] Configuring AI agent and tools...\n`, - }); - console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); - - const options = { - model: "claude-sonnet-4-20250514", - systemPrompt: this.getSystemPrompt(), - maxTurns: 50, - cwd: projectPath, - allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], - permissionMode: "acceptEdits", - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - abortController: abortController, - }; - - const prompt = this.buildRegenerationPrompt(projectDefinition); - - this.currentPhase = "regeneration"; - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Phase: ${this.currentPhase}] Starting spec regeneration...\n`, - }); - console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Starting AI agent query`); - - const currentQuery = query({ prompt, options }); - execution.query = currentQuery; - - let fullResponse = ""; - let toolCallCount = 0; - let messageCount = 0; - - try { - for await (const msg of currentQuery) { - if (!execution.isActive()) { - console.log("[SpecRegeneration] Execution aborted by user"); - break; - } - - if (msg.type === "assistant" && msg.message?.content) { - messageCount++; - for (const block of msg.message.content) { - if (block.type === "text") { - fullResponse += block.text; - const preview = block.text.substring(0, 100).replace(/\n/g, " "); - console.log(`[SpecRegeneration] Agent message #${messageCount}: ${preview}...`); - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Agent] ${block.text}`, - }); - } else if (block.type === "tool_use") { - toolCallCount++; - const toolName = block.name; - const toolInput = block.input; - console.log(`[SpecRegeneration] Tool call #${toolCallCount}: ${toolName}`); - console.log(`[SpecRegeneration] Tool input: ${JSON.stringify(toolInput).substring(0, 200)}...`); - - sendToRenderer({ - type: "spec_regeneration_progress", - content: `\n[Tool] Using ${toolName}...\n`, - }); - - sendToRenderer({ - type: "spec_regeneration_tool", - tool: toolName, - input: toolInput, - }); - } - } - } else if (msg.type === "tool_result") { - // Log tool results for better visibility - const toolName = msg.toolName || "unknown"; - const result = msg.content?.[0]?.text || JSON.stringify(msg.content); - const resultPreview = result.substring(0, 200).replace(/\n/g, " "); - console.log(`[SpecRegeneration] Tool result (${toolName}): ${resultPreview}...`); - - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Tool Result] ${toolName} completed successfully\n`, - }); - } else if (msg.type === "error") { - const errorMsg = msg.error?.message || JSON.stringify(msg.error); - console.error(`[SpecRegeneration] ERROR in query stream: ${errorMsg}`); - sendToRenderer({ - type: "spec_regeneration_error", - error: `Error during spec regeneration: ${errorMsg}`, - }); - } - } - } catch (streamError) { - console.error("[SpecRegeneration] ERROR in query stream:", streamError); - sendToRenderer({ - type: "spec_regeneration_error", - error: `Stream error: ${streamError.message || String(streamError)}`, - }); - throw streamError; - } - - console.log(`[SpecRegeneration] Query completed - ${messageCount} messages, ${toolCallCount} tool calls`); - - execution.query = null; - execution.abortController = null; - - const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); - this.currentPhase = "complete"; - console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Spec regeneration completed in ${elapsedTime}s`); - sendToRenderer({ - type: "spec_regeneration_progress", - content: `\n[Phase: ${this.currentPhase}] āœ“ Spec regeneration complete! (${elapsedTime}s)\n`, - }); - - sendToRenderer({ - type: "spec_regeneration_complete", - message: "Spec regeneration complete!", - }); - - console.log(`[SpecRegeneration] ===== Spec regeneration finished successfully =====`); - return { - success: true, - message: "Spec regeneration complete", - }; - } catch (error) { - const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); - - if (error instanceof AbortError || error?.name === "AbortError") { - console.log(`[SpecRegeneration] Regeneration aborted after ${elapsedTime}s`); - sendToRenderer({ - type: "spec_regeneration_error", - error: "Spec regeneration was aborted by user", - }); - if (execution) { - execution.abortController = null; - execution.query = null; - } - return { - success: false, - message: "Regeneration aborted", - }; - } - - const errorMessage = error.message || String(error); - const errorStack = error.stack || ""; - console.error(`[SpecRegeneration] ERROR regenerating spec after ${elapsedTime}s:`); - console.error(`[SpecRegeneration] Error message: ${errorMessage}`); - console.error(`[SpecRegeneration] Error stack: ${errorStack}`); - - sendToRenderer({ - type: "spec_regeneration_error", - error: `Failed to regenerate spec: ${errorMessage}`, - }); - - if (execution) { - execution.abortController = null; - execution.query = null; - } - throw error; - } - } - - /** - * Get the system prompt for spec regeneration - */ - getSystemPrompt() { - return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project definition. - -You should: -1. First, thoroughly analyze the project structure to understand the existing tech stack -2. Read key configuration files (package.json, tsconfig.json, etc.) to understand dependencies and frameworks -3. Understand the current architecture and patterns used -4. Based on the user's project definition, create a comprehensive app specification that includes ALL features needed to realize their vision -5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application -6. **MANDATORY: Write the spec to EXACTLY \`.automaker/app_spec.txt\` - this exact filename, no alternatives** - -When analyzing, look at: -- package.json, cargo.toml, or similar config files for tech stack -- Source code structure and organization -- Framework-specific patterns (Next.js, React, etc.) -- Database configurations and schemas -- API structures and patterns - -**Note:** Feature files are stored separately in .automaker/features/{id}/feature.json. -Your task is ONLY to update the app_spec.txt file - feature files will be managed separately. - -You CAN and SHOULD modify: -- .automaker/app_spec.txt (this is your ONLY target file - use EXACTLY this filename) - -You have access to file reading, writing, and search tools. Use them to understand the codebase and WRITE the new spec to .automaker/app_spec.txt. - -**CRITICAL FILE NAMING RULES:** -- The spec file MUST be named exactly \`app_spec.txt\` -- Do NOT create project-spec.md, spec.md, or any other filename -- Do NOT use markdown (.md) extension - use .txt -- The full path must be: \`.automaker/app_spec.txt\``; - } - - /** - * Build the prompt for regenerating the spec - */ - buildRegenerationPrompt(projectDefinition) { - return `I need you to regenerate my application specification based on the following project definition. Be very comprehensive and liberal when defining features - I want a complete, polished application. - -**My Project Definition:** -${projectDefinition} - -**Your Task:** - -1. First, explore the project to understand the existing tech stack: - - Read package.json or similar config files - - Identify all frameworks and libraries being used - - Understand the current project structure and architecture - - Note any database, authentication, or other infrastructure in use - -2. Based on my project definition and the existing tech stack, create a comprehensive app specification that includes: - - Product Overview: A clear description of what the app does - - Tech Stack: All technologies currently in use - - Features: A COMPREHENSIVE list of all features needed to realize my vision - - Be liberal! Include all features that would make this a complete, production-ready application - - Include core features, supporting features, and nice-to-have features - - Think about user experience, error handling, edge cases, etc. - - Architecture Notes: Any important architectural decisions or patterns - -3. **MANDATORY FILE WRITE**: You MUST write the spec to EXACTLY this file path: \`.automaker/app_spec.txt\` - - The filename MUST be exactly \`app_spec.txt\` - do NOT use any other name - - Do NOT create \`project-spec.md\`, \`spec.md\`, or any other filename - - Do NOT output the spec in your response - write it to the file - - Use the Write tool with path \`.automaker/app_spec.txt\` - -**Format Guidelines for the Spec (use XML format in app_spec.txt):** - -Use this XML structure inside app_spec.txt: - -\`\`\`xml - - [App Name] - - - [Description of what the app does and its purpose] - - - - [frameworks, libraries] - [frameworks, APIs] - [if applicable] - - - - [List all the major capabilities] - - - - [Foundation features] - [Core features] - [Polish features] - - -\`\`\` - -**Remember:** -- Be comprehensive! Include ALL features needed for a complete application -- Consider user flows, error states, loading states, etc. -- Include authentication, authorization if relevant -- Think about what would make this a polished, production-ready app -- **CRITICAL: Write to EXACTLY \`.automaker/app_spec.txt\` - not project-spec.md or any other name!** - -Begin by exploring the project structure, then generate and WRITE the spec to \`.automaker/app_spec.txt\`.`; - } - - /** - * Handle assistant message in feature generation stream - * @private - */ - _handleAssistantMessage(msg, sendToRenderer, counters) { - counters.messageCount++; - for (const block of msg.message.content) { - if (block.type === "text") { - const preview = block.text.substring(0, 100).replace(/\n/g, " "); - console.log(`[SpecRegeneration] Feature gen message #${counters.messageCount}: ${preview}...`); - sendToRenderer({ - type: "spec_regeneration_progress", - content: block.text, - }); - } else if (block.type === "tool_use") { - this._handleToolUse(block, sendToRenderer, counters); - } - } - } - - /** - * Handle tool use block in feature generation stream - * @private - */ - _handleToolUse(block, sendToRenderer, counters) { - counters.toolCallCount++; - const toolName = block.name; - const toolInput = block.input; - console.log(`[SpecRegeneration] Feature gen tool call #${counters.toolCallCount}: ${toolName}`); - - if (toolName === "mcp__automaker-tools__UpdateFeatureStatus" || toolName === "UpdateFeatureStatus") { - const featureId = toolInput?.featureId || "unknown"; - const status = toolInput?.status || "unknown"; - const summary = toolInput?.summary || ""; - sendToRenderer({ - type: "spec_regeneration_progress", - content: `\n[Feature Creation] Creating feature "${featureId}" with status "${status}"${summary ? `\n Summary: ${summary}` : ""}\n`, - }); - } else { - sendToRenderer({ - type: "spec_regeneration_progress", - content: `\n[Tool] Using ${toolName}...\n`, - }); - } - - sendToRenderer({ - type: "spec_regeneration_tool", - tool: toolName, - input: toolInput, - }); - } - - /** - * Handle tool result in feature generation stream - * @private - */ - _handleToolResult(msg, sendToRenderer) { - const toolName = msg.toolName || "unknown"; - const result = msg.content?.[0]?.text || JSON.stringify(msg.content); - const resultPreview = result.substring(0, 200).replace(/\n/g, " "); - console.log(`[SpecRegeneration] Feature gen tool result (${toolName}): ${resultPreview}...`); - - if (toolName === "mcp__automaker-tools__UpdateFeatureStatus" || toolName === "UpdateFeatureStatus") { - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Feature Creation] ${result}\n`, - }); - } else { - sendToRenderer({ - type: "spec_regeneration_progress", - content: `[Tool Result] ${toolName} completed successfully\n`, - }); - } - } - - /** - * Handle error in feature generation stream - * @private - */ - _handleStreamError(msg, sendToRenderer) { - const errorMsg = msg.error?.message || JSON.stringify(msg.error); - console.error(`[SpecRegeneration] ERROR in feature generation stream: ${errorMsg}`); - sendToRenderer({ - type: "spec_regeneration_error", - error: `Error during feature generation: ${errorMsg}`, - }); - } - - /** - * Stop the current regeneration - */ - stop() { - if (this.runningRegeneration && this.runningRegeneration.abortController) { - this.runningRegeneration.abortController.abort(); - } - this.runningRegeneration = null; - this.currentPhase = ""; - } -} - -module.exports = new SpecRegenerationService(); diff --git a/apps/app/electron/services/worktree-manager.js b/apps/app/electron/services/worktree-manager.js deleted file mode 100644 index 0e1cfc45..00000000 --- a/apps/app/electron/services/worktree-manager.js +++ /dev/null @@ -1,569 +0,0 @@ -const path = require("path"); -const fs = require("fs/promises"); -const { exec, spawn } = require("child_process"); -const { promisify } = require("util"); - -const execAsync = promisify(exec); - -/** - * Worktree Manager - Handles git worktrees for feature isolation - * - * This service creates isolated git worktrees for each feature, allowing: - * - Features to be worked on in isolation without affecting the main branch - * - Easy rollback/revert by simply deleting the worktree - * - Checkpointing - user can see changes in the worktree before merging - */ -class WorktreeManager { - constructor() { - // Cache for worktree info - this.worktreeCache = new Map(); - } - - /** - * Get the base worktree directory path - */ - getWorktreeBasePath(projectPath) { - return path.join(projectPath, ".automaker", "worktrees"); - } - - /** - * Generate a safe branch name from feature description - */ - generateBranchName(feature) { - // Create a slug from the description - const slug = feature.description - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, "") // Remove special chars - .replace(/\s+/g, "-") // Replace spaces with hyphens - .substring(0, 40); // Limit length - - // Add feature ID for uniqueness - const shortId = feature.id.replace("feature-", "").substring(0, 12); - return `feature/${shortId}-${slug}`; - } - - /** - * Check if the project is a git repository - */ - async isGitRepo(projectPath) { - try { - await execAsync("git rev-parse --is-inside-work-tree", { cwd: projectPath }); - return true; - } catch { - return false; - } - } - - /** - * Get the current branch name - */ - async getCurrentBranch(projectPath) { - try { - const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: projectPath }); - return stdout.trim(); - } catch (error) { - console.error("[WorktreeManager] Failed to get current branch:", error); - return null; - } - } - - /** - * Check if a branch exists (local or remote) - */ - async branchExists(projectPath, branchName) { - try { - await execAsync(`git rev-parse --verify ${branchName}`, { cwd: projectPath }); - return true; - } catch { - return false; - } - } - - /** - * List all existing worktrees - */ - async listWorktrees(projectPath) { - try { - const { stdout } = await execAsync("git worktree list --porcelain", { cwd: projectPath }); - const worktrees = []; - const lines = stdout.split("\n"); - - let currentWorktree = null; - for (const line of lines) { - if (line.startsWith("worktree ")) { - if (currentWorktree) { - worktrees.push(currentWorktree); - } - currentWorktree = { path: line.replace("worktree ", "") }; - } else if (line.startsWith("branch ") && currentWorktree) { - currentWorktree.branch = line.replace("branch refs/heads/", ""); - } else if (line.startsWith("HEAD ") && currentWorktree) { - currentWorktree.head = line.replace("HEAD ", ""); - } - } - if (currentWorktree) { - worktrees.push(currentWorktree); - } - - return worktrees; - } catch (error) { - console.error("[WorktreeManager] Failed to list worktrees:", error); - return []; - } - } - - /** - * Create a worktree for a feature - * @param {string} projectPath - Path to the main project - * @param {object} feature - Feature object with id and description - * @returns {object} - { success, worktreePath, branchName, error } - */ - async createWorktree(projectPath, feature) { - console.log(`[WorktreeManager] Creating worktree for feature: ${feature.id}`); - - // Check if project is a git repo - if (!await this.isGitRepo(projectPath)) { - return { success: false, error: "Project is not a git repository" }; - } - - const branchName = this.generateBranchName(feature); - const worktreeBasePath = this.getWorktreeBasePath(projectPath); - const worktreePath = path.join(worktreeBasePath, branchName.replace("feature/", "")); - - try { - // Ensure worktree directory exists - await fs.mkdir(worktreeBasePath, { recursive: true }); - - // Check if worktree already exists - const worktrees = await this.listWorktrees(projectPath); - const existingWorktree = worktrees.find( - w => w.path === worktreePath || w.branch === branchName - ); - - if (existingWorktree) { - console.log(`[WorktreeManager] Worktree already exists for feature: ${feature.id}`); - return { - success: true, - worktreePath: existingWorktree.path, - branchName: existingWorktree.branch, - existed: true, - }; - } - - // Get current branch to base the new branch on - const baseBranch = await this.getCurrentBranch(projectPath); - if (!baseBranch) { - return { success: false, error: "Could not determine current branch" }; - } - - // Check if branch already exists - const branchExists = await this.branchExists(projectPath, branchName); - - if (branchExists) { - // Use existing branch - console.log(`[WorktreeManager] Using existing branch: ${branchName}`); - await execAsync(`git worktree add "${worktreePath}" ${branchName}`, { cwd: projectPath }); - } else { - // Create new worktree with new branch - console.log(`[WorktreeManager] Creating new branch: ${branchName} based on ${baseBranch}`); - await execAsync(`git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`, { cwd: projectPath }); - } - - // Copy .automaker directory to worktree (except worktrees directory itself to avoid recursion) - const automakerSrc = path.join(projectPath, ".automaker"); - const automakerDst = path.join(worktreePath, ".automaker"); - - try { - await fs.mkdir(automakerDst, { recursive: true }); - - // Note: Features are stored in .automaker/features/{id}/feature.json - // These are managed by the main project, not copied to worktrees - - // Copy app_spec.txt if it exists - const appSpecSrc = path.join(automakerSrc, "app_spec.txt"); - const appSpecDst = path.join(automakerDst, "app_spec.txt"); - try { - const content = await fs.readFile(appSpecSrc, "utf-8"); - await fs.writeFile(appSpecDst, content, "utf-8"); - } catch { - // App spec might not exist yet - } - - // Copy categories.json if it exists - const categoriesSrc = path.join(automakerSrc, "categories.json"); - const categoriesDst = path.join(automakerDst, "categories.json"); - try { - const content = await fs.readFile(categoriesSrc, "utf-8"); - await fs.writeFile(categoriesDst, content, "utf-8"); - } catch { - // Categories might not exist yet - } - } catch (error) { - console.warn("[WorktreeManager] Failed to copy .automaker directory:", error); - } - - // Store worktree info in cache - this.worktreeCache.set(feature.id, { - worktreePath, - branchName, - createdAt: new Date().toISOString(), - baseBranch, - }); - - console.log(`[WorktreeManager] Worktree created at: ${worktreePath}`); - return { - success: true, - worktreePath, - branchName, - baseBranch, - existed: false, - }; - } catch (error) { - console.error("[WorktreeManager] Failed to create worktree:", error); - return { success: false, error: error.message }; - } - } - - /** - * Get worktree info for a feature - */ - async getWorktreeInfo(projectPath, featureId) { - // Check cache first - if (this.worktreeCache.has(featureId)) { - return { success: true, ...this.worktreeCache.get(featureId) }; - } - - // Scan worktrees to find matching one - const worktrees = await this.listWorktrees(projectPath); - const worktreeBasePath = this.getWorktreeBasePath(projectPath); - - for (const worktree of worktrees) { - // Check if this worktree is in our worktree directory - if (worktree.path.startsWith(worktreeBasePath)) { - // Check if the feature ID is in the branch name - const shortId = featureId.replace("feature-", "").substring(0, 12); - if (worktree.branch && worktree.branch.includes(shortId)) { - const info = { - worktreePath: worktree.path, - branchName: worktree.branch, - head: worktree.head, - }; - this.worktreeCache.set(featureId, info); - return { success: true, ...info }; - } - } - } - - return { success: false, error: "Worktree not found" }; - } - - /** - * Remove a worktree for a feature - * This effectively reverts all changes made by the agent - */ - async removeWorktree(projectPath, featureId, deleteBranch = false) { - console.log(`[WorktreeManager] Removing worktree for feature: ${featureId}`); - - const worktreeInfo = await this.getWorktreeInfo(projectPath, featureId); - if (!worktreeInfo.success) { - console.log(`[WorktreeManager] No worktree found for feature: ${featureId}`); - return { success: true, message: "No worktree to remove" }; - } - - const { worktreePath, branchName } = worktreeInfo; - - try { - // Remove the worktree - await execAsync(`git worktree remove "${worktreePath}" --force`, { cwd: projectPath }); - console.log(`[WorktreeManager] Worktree removed: ${worktreePath}`); - - // Optionally delete the branch too - if (deleteBranch && branchName) { - try { - await execAsync(`git branch -D ${branchName}`, { cwd: projectPath }); - console.log(`[WorktreeManager] Branch deleted: ${branchName}`); - } catch (error) { - console.warn(`[WorktreeManager] Could not delete branch ${branchName}:`, error.message); - } - } - - // Remove from cache - this.worktreeCache.delete(featureId); - - return { success: true, removedPath: worktreePath, removedBranch: deleteBranch ? branchName : null }; - } catch (error) { - console.error("[WorktreeManager] Failed to remove worktree:", error); - return { success: false, error: error.message }; - } - } - - /** - * Get status of changes in a worktree - */ - async getWorktreeStatus(worktreePath) { - try { - const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath }); - const { stdout: diffStat } = await execAsync("git diff --stat", { cwd: worktreePath }); - const { stdout: commitLog } = await execAsync("git log --oneline -10", { cwd: worktreePath }); - - const files = statusOutput.trim().split("\n").filter(Boolean); - const commits = commitLog.trim().split("\n").filter(Boolean); - - return { - success: true, - modifiedFiles: files.length, - files: files.slice(0, 20), // Limit to 20 files - diffStat: diffStat.trim(), - recentCommits: commits.slice(0, 5), // Last 5 commits - }; - } catch (error) { - console.error("[WorktreeManager] Failed to get worktree status:", error); - return { success: false, error: error.message }; - } - } - - /** - * Get detailed file diff content for a worktree - * Returns unified diff format for all changes - */ - async getFileDiffs(worktreePath) { - try { - // Get both staged and unstaged diffs - const { stdout: unstagedDiff } = await execAsync("git diff --no-color", { - cwd: worktreePath, - maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large diffs - }); - const { stdout: stagedDiff } = await execAsync("git diff --cached --no-color", { - cwd: worktreePath, - maxBuffer: 10 * 1024 * 1024 - }); - - // Get list of files with their status - const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath }); - const files = statusOutput.trim().split("\n").filter(Boolean); - - // Parse file statuses - const fileStatuses = files.map(line => { - const status = line.substring(0, 2); - const filePath = line.substring(3); - return { - status: status.trim() || 'M', - path: filePath, - statusText: this.getStatusText(status) - }; - }); - - // Combine diffs - const combinedDiff = [stagedDiff, unstagedDiff].filter(Boolean).join("\n"); - - return { - success: true, - diff: combinedDiff, - files: fileStatuses, - hasChanges: files.length > 0 - }; - } catch (error) { - console.error("[WorktreeManager] Failed to get file diffs:", error); - return { success: false, error: error.message }; - } - } - - /** - * Get human-readable status text from git status code - */ - getStatusText(status) { - const statusMap = { - 'M': 'Modified', - 'A': 'Added', - 'D': 'Deleted', - 'R': 'Renamed', - 'C': 'Copied', - 'U': 'Updated', - '?': 'Untracked', - '!': 'Ignored' - }; - const firstChar = status.charAt(0); - const secondChar = status.charAt(1); - return statusMap[firstChar] || statusMap[secondChar] || 'Changed'; - } - - /** - * Get diff for a specific file in a worktree - */ - async getFileDiff(worktreePath, filePath) { - try { - // Try to get unstaged diff first, then staged if no unstaged changes - let diff = ''; - try { - const { stdout } = await execAsync(`git diff --no-color -- "${filePath}"`, { - cwd: worktreePath, - maxBuffer: 5 * 1024 * 1024 - }); - diff = stdout; - } catch { - // File might be staged - } - - if (!diff) { - try { - const { stdout } = await execAsync(`git diff --cached --no-color -- "${filePath}"`, { - cwd: worktreePath, - maxBuffer: 5 * 1024 * 1024 - }); - diff = stdout; - } catch { - // File might be untracked, show the content - } - } - - // If still no diff, might be an untracked file - show the content - if (!diff) { - try { - const fullPath = path.join(worktreePath, filePath); - const content = await fs.readFile(fullPath, 'utf-8'); - diff = `+++ ${filePath} (new file)\n${content.split('\n').map(l => '+' + l).join('\n')}`; - } catch { - diff = '(Unable to read file content)'; - } - } - - return { - success: true, - diff, - filePath - }; - } catch (error) { - console.error(`[WorktreeManager] Failed to get diff for ${filePath}:`, error); - return { success: false, error: error.message }; - } - } - - /** - * Merge worktree changes back to the main branch - */ - async mergeWorktree(projectPath, featureId, options = {}) { - console.log(`[WorktreeManager] Merging worktree for feature: ${featureId}`); - - const worktreeInfo = await this.getWorktreeInfo(projectPath, featureId); - if (!worktreeInfo.success) { - return { success: false, error: "Worktree not found" }; - } - - const { branchName, worktreePath } = worktreeInfo; - const baseBranch = await this.getCurrentBranch(projectPath); - - try { - // First commit any uncommitted changes in the worktree - const { stdout: status } = await execAsync("git status --porcelain", { cwd: worktreePath }); - if (status.trim()) { - // There are uncommitted changes - commit them - await execAsync("git add -A", { cwd: worktreePath }); - const commitMsg = options.commitMessage || `feat: complete ${featureId}`; - await execAsync(`git commit -m "${commitMsg}"`, { cwd: worktreePath }); - } - - // Merge the feature branch into the current branch in the main repo - if (options.squash) { - await execAsync(`git merge --squash ${branchName}`, { cwd: projectPath }); - const squashMsg = options.squashMessage || `feat: ${featureId} - squashed merge`; - await execAsync(`git commit -m "${squashMsg}"`, { cwd: projectPath }); - } else { - await execAsync(`git merge ${branchName} --no-ff -m "Merge ${branchName}"`, { cwd: projectPath }); - } - - console.log(`[WorktreeManager] Successfully merged ${branchName} into ${baseBranch}`); - - // Optionally cleanup worktree after merge - if (options.cleanup) { - await this.removeWorktree(projectPath, featureId, true); - } - - return { - success: true, - mergedBranch: branchName, - intoBranch: baseBranch, - }; - } catch (error) { - console.error("[WorktreeManager] Failed to merge worktree:", error); - return { success: false, error: error.message }; - } - } - - /** - * Sync changes from main branch to worktree (rebase or merge) - */ - async syncWorktree(projectPath, featureId, method = "rebase") { - console.log(`[WorktreeManager] Syncing worktree for feature: ${featureId}`); - - const worktreeInfo = await this.getWorktreeInfo(projectPath, featureId); - if (!worktreeInfo.success) { - return { success: false, error: "Worktree not found" }; - } - - const { worktreePath, baseBranch } = worktreeInfo; - - try { - if (method === "rebase") { - await execAsync(`git rebase ${baseBranch}`, { cwd: worktreePath }); - } else { - await execAsync(`git merge ${baseBranch}`, { cwd: worktreePath }); - } - - return { success: true, method }; - } catch (error) { - console.error("[WorktreeManager] Failed to sync worktree:", error); - return { success: false, error: error.message }; - } - } - - /** - * Get list of all feature worktrees - */ - async getAllFeatureWorktrees(projectPath) { - const worktrees = await this.listWorktrees(projectPath); - const worktreeBasePath = this.getWorktreeBasePath(projectPath); - - return worktrees.filter(w => - w.path.startsWith(worktreeBasePath) && - w.branch && - w.branch.startsWith("feature/") - ); - } - - /** - * Cleanup orphaned worktrees (worktrees without matching features) - */ - async cleanupOrphanedWorktrees(projectPath, activeFeatureIds) { - console.log("[WorktreeManager] Cleaning up orphaned worktrees..."); - - const worktrees = await this.getAllFeatureWorktrees(projectPath); - const cleaned = []; - - for (const worktree of worktrees) { - // Extract feature ID from branch name - const branchParts = worktree.branch.replace("feature/", "").split("-"); - const shortId = branchParts[0]; - - // Check if any active feature has this short ID - const hasMatchingFeature = activeFeatureIds.some(id => { - const featureShortId = id.replace("feature-", "").substring(0, 12); - return featureShortId === shortId; - }); - - if (!hasMatchingFeature) { - console.log(`[WorktreeManager] Removing orphaned worktree: ${worktree.path}`); - try { - await execAsync(`git worktree remove "${worktree.path}" --force`, { cwd: projectPath }); - await execAsync(`git branch -D ${worktree.branch}`, { cwd: projectPath }); - cleaned.push(worktree.path); - } catch (error) { - console.warn(`[WorktreeManager] Failed to cleanup worktree ${worktree.path}:`, error.message); - } - } - } - - return { success: true, cleaned }; - } -} - -module.exports = new WorktreeManager(); diff --git a/apps/app/package.json b/apps/app/package.json index 11656e0d..9c973c0e 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -29,11 +29,9 @@ "dev:electron:wsl:gpu": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA electron . --no-sandbox --disable-gpu-sandbox\"" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.1.61", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -97,8 +95,7 @@ "electron/**/*", ".next/**/*", "public/**/*", - "!node_modules/**/*", - "node_modules/@anthropic-ai/**/*" + "!node_modules/**/*" ], "extraResources": [ { diff --git a/plan.md b/plan.md index 1e719824..56a355a3 100644 --- a/plan.md +++ b/plan.md @@ -224,10 +224,11 @@ NEXT_PUBLIC_SERVER_URL=http://localhost:3008 - `apps/server/src/routes/running-agents.ts` - active agent tracking - [x] **Phase 7**: Simplify Electron - - `apps/app/electron/main-simplified.js` - spawns server, minimal IPC - - `apps/app/electron/preload-simplified.js` - only native features exposed + - `apps/app/electron/main.js` - spawns server, minimal IPC (10 handlers for native features only) + - `apps/app/electron/preload.js` - only native features exposed - Updated `electron.ts` to detect simplified mode - Updated `http-api-client.ts` to use native dialogs when available + - Removed ~13,000 lines of dead code (obsolete services, agent-service.js, auto-mode-service.js) - [x] **Phase 8**: Production ready - `apps/server/src/lib/auth.ts` - API key authentication middleware