/** * HTTP API Client for web mode * * This client provides the same API as the Electron IPC bridge, * but communicates with the backend server via HTTP/WebSocket. */ import type { ElectronAPI, FileResult, WriteResult, ReaddirResult, StatResult, DialogResult, SaveImageResult, AutoModeAPI, FeaturesAPI, SuggestionsAPI, SpecRegenerationAPI, AutoModeEvent, SuggestionsEvent, SpecRegenerationEvent, SuggestionType, GitHubAPI, GitHubIssue, GitHubPR, IssueValidationInput, IssueValidationEvent, } from './electron'; import type { Message, SessionListItem } from '@/types/electron'; import type { Feature, ClaudeUsageResponse } from '@/store/app-store'; import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron'; import { getGlobalFileBrowser } from '@/contexts/file-browser-context'; // Server URL - configurable via environment variable const getServerUrl = (): string => { if (typeof window !== 'undefined') { const envUrl = import.meta.env.VITE_SERVER_URL; if (envUrl) return envUrl; } return 'http://localhost:3008'; }; // Get API key from environment variable const getApiKey = (): string | null => { if (typeof window !== 'undefined') { return import.meta.env.VITE_AUTOMAKER_API_KEY || null; } return null; }; type EventType = | 'agent:stream' | 'auto-mode:event' | 'suggestions:event' | 'spec-regeneration:event' | 'issue-validation:event'; type EventCallback = (payload: unknown) => void; interface EnhancePromptResult { success: boolean; enhancedText?: string; error?: string; } /** * HTTP API Client that implements ElectronAPI interface */ export class HttpApiClient implements ElectronAPI { private serverUrl: string; private ws: WebSocket | null = null; private eventCallbacks: Map> = new Map(); private reconnectTimer: NodeJS.Timeout | null = null; private isConnecting = false; constructor() { this.serverUrl = getServerUrl(); this.connectWebSocket(); } private connectWebSocket(): void { if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) { return; } this.isConnecting = true; try { const wsUrl = this.serverUrl.replace(/^http/, 'ws') + '/api/events'; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('[HttpApiClient] WebSocket connected'); this.isConnecting = false; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } }; this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); const callbacks = this.eventCallbacks.get(data.type); if (callbacks) { callbacks.forEach((cb) => cb(data.payload)); } } catch (error) { console.error('[HttpApiClient] Failed to parse WebSocket message:', error); } }; this.ws.onclose = () => { console.log('[HttpApiClient] WebSocket disconnected'); this.isConnecting = false; this.ws = null; // Attempt to reconnect after 5 seconds if (!this.reconnectTimer) { this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.connectWebSocket(); }, 5000); } }; this.ws.onerror = (error) => { console.error('[HttpApiClient] WebSocket error:', error); this.isConnecting = false; }; } catch (error) { console.error('[HttpApiClient] Failed to create WebSocket:', error); this.isConnecting = false; } } private subscribeToEvent(type: EventType, callback: EventCallback): () => void { if (!this.eventCallbacks.has(type)) { this.eventCallbacks.set(type, new Set()); } this.eventCallbacks.get(type)!.add(callback); // Ensure WebSocket is connected this.connectWebSocket(); return () => { const callbacks = this.eventCallbacks.get(type); if (callbacks) { callbacks.delete(callback); } }; } private getHeaders(): Record { const headers: Record = { 'Content-Type': 'application/json', }; const apiKey = getApiKey(); if (apiKey) { headers['X-API-Key'] = apiKey; } return headers; } private async post(endpoint: string, body?: unknown): Promise { const response = await fetch(`${this.serverUrl}${endpoint}`, { method: 'POST', headers: this.getHeaders(), body: body ? JSON.stringify(body) : undefined, }); return response.json(); } private async get(endpoint: string): Promise { const headers = this.getHeaders(); const response = await fetch(`${this.serverUrl}${endpoint}`, { headers }); return response.json(); } private async put(endpoint: string, body?: unknown): Promise { const response = await fetch(`${this.serverUrl}${endpoint}`, { method: 'PUT', headers: this.getHeaders(), body: body ? JSON.stringify(body) : undefined, }); return response.json(); } private async httpDelete(endpoint: string): Promise { const response = await fetch(`${this.serverUrl}${endpoint}`, { method: 'DELETE', headers: this.getHeaders(), }); return response.json(); } // Basic operations async ping(): Promise { const result = await this.get<{ status: string }>('/api/health'); return result.status === 'ok' ? 'pong' : 'error'; } async openExternalLink(url: string): Promise<{ success: boolean; error?: string }> { // Open in new tab window.open(url, '_blank', 'noopener,noreferrer'); return { success: true }; } async openInEditor( filePath: string, line?: number, column?: number ): Promise<{ success: boolean; error?: string }> { // Build VS Code URL scheme: vscode://file/path:line:column // This works on systems where VS Code's URL handler is registered // URL encode the path to handle special characters (spaces, brackets, etc.) // Handle both Unix (/) and Windows (\) path separators const normalizedPath = filePath.replace(/\\/g, '/'); const encodedPath = normalizedPath.startsWith('/') ? '/' + normalizedPath.slice(1).split('/').map(encodeURIComponent).join('/') : normalizedPath.split('/').map(encodeURIComponent).join('/'); let url = `vscode://file${encodedPath}`; if (line !== undefined && line > 0) { url += `:${line}`; if (column !== undefined && column > 0) { url += `:${column}`; } } try { // Use anchor click approach which is most reliable for custom URL schemes // This triggers the browser's URL handler without navigation issues const anchor = document.createElement('a'); anchor.href = url; anchor.style.display = 'none'; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to open in editor', }; } } // File picker - uses server-side file browser dialog async openDirectory(): Promise { const fileBrowser = getGlobalFileBrowser(); if (!fileBrowser) { console.error('File browser not initialized'); return { canceled: true, filePaths: [] }; } const path = await fileBrowser(); if (!path) { return { canceled: true, filePaths: [] }; } // Validate with server const result = await this.post<{ success: boolean; path?: string; error?: string; }>('/api/fs/validate-path', { filePath: path }); if (result.success && result.path) { return { canceled: false, filePaths: [result.path] }; } console.error('Invalid directory:', result.error); return { canceled: true, filePaths: [] }; } async openFile(_options?: object): Promise { const fileBrowser = getGlobalFileBrowser(); if (!fileBrowser) { console.error('File browser not initialized'); return { canceled: true, filePaths: [] }; } // For now, use the same directory browser (could be enhanced for file selection) const path = await fileBrowser(); if (!path) { return { canceled: true, filePaths: [] }; } const result = await this.post<{ success: boolean; exists: boolean }>('/api/fs/exists', { filePath: path, }); if (result.success && result.exists) { return { canceled: false, filePaths: [path] }; } console.error('File not found'); return { canceled: true, filePaths: [] }; } // File system operations async readFile(filePath: string): Promise { return this.post('/api/fs/read', { filePath }); } async writeFile(filePath: string, content: string): Promise { return this.post('/api/fs/write', { filePath, content }); } async mkdir(dirPath: string): Promise { return this.post('/api/fs/mkdir', { dirPath }); } async readdir(dirPath: string): Promise { return this.post('/api/fs/readdir', { dirPath }); } async exists(filePath: string): Promise { const result = await this.post<{ success: boolean; exists: boolean }>('/api/fs/exists', { filePath, }); return result.exists; } async stat(filePath: string): Promise { return this.post('/api/fs/stat', { filePath }); } async deleteFile(filePath: string): Promise { return this.post('/api/fs/delete', { filePath }); } async trashItem(filePath: string): Promise { // In web mode, trash is just delete return this.deleteFile(filePath); } async getPath(name: string): Promise { // Server provides data directory if (name === 'userData') { const result = await this.get<{ dataDir: string }>('/api/health/detailed'); return result.dataDir || '/data'; } return `/data/${name}`; } async saveImageToTemp( data: string, filename: string, mimeType: string, projectPath?: string ): Promise { return this.post('/api/fs/save-image', { data, filename, mimeType, projectPath, }); } async saveBoardBackground( data: string, filename: string, mimeType: string, projectPath: string ): Promise<{ success: boolean; path?: string; error?: string }> { return this.post('/api/fs/save-board-background', { data, filename, mimeType, projectPath, }); } async deleteBoardBackground(projectPath: string): Promise<{ success: boolean; error?: string }> { return this.post('/api/fs/delete-board-background', { projectPath }); } // CLI checks - server-side async checkClaudeCli(): Promise<{ success: boolean; status?: string; method?: string; version?: string; path?: string; recommendation?: string; installCommands?: { macos?: string; windows?: string; linux?: string; npm?: string; }; error?: string; }> { return this.get('/api/setup/claude-status'); } // Model API model = { getAvailable: async (): Promise<{ success: boolean; models?: ModelDefinition[]; error?: string; }> => { return this.get('/api/models/available'); }, checkProviders: async (): Promise<{ success: boolean; providers?: Record; error?: string; }> => { return this.get('/api/models/providers'); }, }; // Setup API setup = { getClaudeStatus: (): Promise<{ success: boolean; status?: string; installed?: boolean; method?: string; version?: string; path?: string; auth?: { authenticated: boolean; method: string; hasCredentialsFile?: boolean; hasToken?: boolean; hasStoredOAuthToken?: boolean; hasStoredApiKey?: boolean; hasEnvApiKey?: boolean; hasEnvOAuthToken?: boolean; hasCliAuth?: boolean; hasRecentActivity?: boolean; }; error?: string; }> => this.get('/api/setup/claude-status'), installClaude: (): Promise<{ success: boolean; message?: string; error?: string; }> => this.post('/api/setup/install-claude'), authClaude: (): Promise<{ success: boolean; token?: string; requiresManualAuth?: boolean; terminalOpened?: boolean; command?: string; error?: string; message?: string; output?: string; }> => this.post('/api/setup/auth-claude'), storeApiKey: ( provider: string, apiKey: string ): Promise<{ success: boolean; error?: string; }> => this.post('/api/setup/store-api-key', { provider, apiKey }), deleteApiKey: ( provider: string ): Promise<{ success: boolean; error?: string; message?: string; }> => this.post('/api/setup/delete-api-key', { provider }), getApiKeys: (): Promise<{ success: boolean; hasAnthropicKey: boolean; hasGoogleKey: boolean; }> => this.get('/api/setup/api-keys'), getPlatform: (): Promise<{ success: boolean; platform: string; arch: string; homeDir: string; isWindows: boolean; isMac: boolean; isLinux: boolean; }> => this.get('/api/setup/platform'), verifyClaudeAuth: ( authMethod?: 'cli' | 'api_key' ): Promise<{ success: boolean; authenticated: boolean; error?: string; }> => this.post('/api/setup/verify-claude-auth', { authMethod }), getGhStatus: (): Promise<{ success: boolean; installed: boolean; authenticated: boolean; version: string | null; path: string | null; user: string | null; error?: string; }> => this.get('/api/setup/gh-status'), onInstallProgress: (callback: (progress: unknown) => void) => { return this.subscribeToEvent('agent:stream', callback); }, onAuthProgress: (callback: (progress: unknown) => void) => { return this.subscribeToEvent('agent:stream', callback); }, }; // Features API features: FeaturesAPI = { getAll: (projectPath: string) => this.post('/api/features/list', { projectPath }), get: (projectPath: string, featureId: string) => this.post('/api/features/get', { projectPath, featureId }), create: (projectPath: string, feature: Feature) => this.post('/api/features/create', { projectPath, feature }), update: (projectPath: string, featureId: string, updates: Partial) => this.post('/api/features/update', { projectPath, featureId, updates }), delete: (projectPath: string, featureId: string) => this.post('/api/features/delete', { projectPath, featureId }), getAgentOutput: (projectPath: string, featureId: string) => this.post('/api/features/agent-output', { projectPath, featureId }), generateTitle: (description: string) => this.post('/api/features/generate-title', { description }), }; // Auto Mode API autoMode: AutoModeAPI = { start: (projectPath: string, maxConcurrency?: number) => this.post('/api/auto-mode/start', { projectPath, maxConcurrency }), stop: (projectPath: string) => this.post('/api/auto-mode/stop', { projectPath }), stopFeature: (featureId: string) => this.post('/api/auto-mode/stop-feature', { featureId }), status: (projectPath?: string) => this.post('/api/auto-mode/status', { projectPath }), runFeature: ( projectPath: string, featureId: string, useWorktrees?: boolean, worktreePath?: string ) => this.post('/api/auto-mode/run-feature', { projectPath, featureId, useWorktrees, worktreePath, }), verifyFeature: (projectPath: string, featureId: string) => this.post('/api/auto-mode/verify-feature', { projectPath, featureId }), resumeFeature: (projectPath: string, featureId: string, useWorktrees?: boolean) => this.post('/api/auto-mode/resume-feature', { projectPath, featureId, useWorktrees, }), contextExists: (projectPath: string, featureId: string) => this.post('/api/auto-mode/context-exists', { projectPath, featureId }), analyzeProject: (projectPath: string) => this.post('/api/auto-mode/analyze-project', { projectPath }), followUpFeature: ( projectPath: string, featureId: string, prompt: string, imagePaths?: string[], worktreePath?: string ) => this.post('/api/auto-mode/follow-up-feature', { projectPath, featureId, prompt, imagePaths, worktreePath, }), commitFeature: (projectPath: string, featureId: string, worktreePath?: string) => this.post('/api/auto-mode/commit-feature', { projectPath, featureId, worktreePath, }), approvePlan: ( projectPath: string, featureId: string, approved: boolean, editedPlan?: string, feedback?: string ) => this.post('/api/auto-mode/approve-plan', { projectPath, featureId, approved, editedPlan, feedback, }), onEvent: (callback: (event: AutoModeEvent) => void) => { return this.subscribeToEvent('auto-mode:event', callback as EventCallback); }, }; // Enhance Prompt API enhancePrompt = { enhance: ( originalText: string, enhancementMode: string, model?: string ): Promise => this.post('/api/enhance-prompt', { originalText, enhancementMode, model, }), }; // Worktree API worktree: WorktreeAPI = { mergeFeature: (projectPath: string, featureId: string, options?: object) => this.post('/api/worktree/merge', { projectPath, featureId, options }), getInfo: (projectPath: string, featureId: string) => this.post('/api/worktree/info', { projectPath, featureId }), getStatus: (projectPath: string, featureId: string) => this.post('/api/worktree/status', { projectPath, featureId }), list: (projectPath: string) => this.post('/api/worktree/list', { projectPath }), listAll: (projectPath: string, includeDetails?: boolean) => this.post('/api/worktree/list', { projectPath, includeDetails }), create: (projectPath: string, branchName: string, baseBranch?: string) => this.post('/api/worktree/create', { projectPath, branchName, baseBranch, }), delete: (projectPath: string, worktreePath: string, deleteBranch?: boolean) => this.post('/api/worktree/delete', { projectPath, worktreePath, deleteBranch, }), commit: (worktreePath: string, message: string) => this.post('/api/worktree/commit', { worktreePath, message }), push: (worktreePath: string, force?: boolean) => this.post('/api/worktree/push', { worktreePath, force }), createPR: (worktreePath: string, options?: any) => this.post('/api/worktree/create-pr', { worktreePath, ...options }), getDiffs: (projectPath: string, featureId: string) => this.post('/api/worktree/diffs', { projectPath, featureId }), getFileDiff: (projectPath: string, featureId: string, filePath: string) => this.post('/api/worktree/file-diff', { projectPath, featureId, filePath, }), pull: (worktreePath: string) => this.post('/api/worktree/pull', { worktreePath }), checkoutBranch: (worktreePath: string, branchName: string) => this.post('/api/worktree/checkout-branch', { worktreePath, branchName }), listBranches: (worktreePath: string) => this.post('/api/worktree/list-branches', { worktreePath }), switchBranch: (worktreePath: string, branchName: string) => this.post('/api/worktree/switch-branch', { worktreePath, branchName }), openInEditor: (worktreePath: string) => this.post('/api/worktree/open-in-editor', { worktreePath }), getDefaultEditor: () => this.get('/api/worktree/default-editor'), initGit: (projectPath: string) => this.post('/api/worktree/init-git', { projectPath }), startDevServer: (projectPath: string, worktreePath: string) => this.post('/api/worktree/start-dev', { projectPath, worktreePath }), stopDevServer: (worktreePath: string) => this.post('/api/worktree/stop-dev', { worktreePath }), listDevServers: () => this.post('/api/worktree/list-dev-servers', {}), getPRInfo: (worktreePath: string, branchName: string) => this.post('/api/worktree/pr-info', { worktreePath, branchName }), }; // Git API git: GitAPI = { getDiffs: (projectPath: string) => this.post('/api/git/diffs', { projectPath }), getFileDiff: (projectPath: string, filePath: string) => this.post('/api/git/file-diff', { projectPath, filePath }), }; // Suggestions API suggestions: SuggestionsAPI = { generate: (projectPath: string, suggestionType?: SuggestionType) => this.post('/api/suggestions/generate', { projectPath, suggestionType }), stop: () => this.post('/api/suggestions/stop'), status: () => this.get('/api/suggestions/status'), onEvent: (callback: (event: SuggestionsEvent) => void) => { return this.subscribeToEvent('suggestions:event', callback as EventCallback); }, }; // Spec Regeneration API specRegeneration: SpecRegenerationAPI = { create: ( projectPath: string, projectOverview: string, generateFeatures?: boolean, analyzeProject?: boolean, maxFeatures?: number ) => this.post('/api/spec-regeneration/create', { projectPath, projectOverview, generateFeatures, analyzeProject, maxFeatures, }), generate: ( projectPath: string, projectDefinition: string, generateFeatures?: boolean, analyzeProject?: boolean, maxFeatures?: number ) => this.post('/api/spec-regeneration/generate', { projectPath, projectDefinition, generateFeatures, analyzeProject, maxFeatures, }), generateFeatures: (projectPath: string, maxFeatures?: number) => this.post('/api/spec-regeneration/generate-features', { projectPath, maxFeatures, }), stop: () => this.post('/api/spec-regeneration/stop'), status: () => this.get('/api/spec-regeneration/status'), onEvent: (callback: (event: SpecRegenerationEvent) => void) => { return this.subscribeToEvent('spec-regeneration:event', callback as EventCallback); }, }; // Running Agents API runningAgents = { getAll: (): Promise<{ success: boolean; runningAgents?: Array<{ featureId: string; projectPath: string; projectName: string; isAutoMode: boolean; }>; totalCount?: number; error?: string; }> => this.get('/api/running-agents'), }; // GitHub API github: GitHubAPI = { checkRemote: (projectPath: string) => this.post('/api/github/check-remote', { projectPath }), listIssues: (projectPath: string) => this.post('/api/github/issues', { projectPath }), listPRs: (projectPath: string) => this.post('/api/github/prs', { projectPath }), validateIssue: (projectPath: string, issue: IssueValidationInput, model?: string) => this.post('/api/github/validate-issue', { projectPath, ...issue, model }), getValidationStatus: (projectPath: string, issueNumber?: number) => this.post('/api/github/validation-status', { projectPath, issueNumber }), stopValidation: (projectPath: string, issueNumber: number) => this.post('/api/github/validation-stop', { projectPath, issueNumber }), getValidations: (projectPath: string, issueNumber?: number) => this.post('/api/github/validations', { projectPath, issueNumber }), markValidationViewed: (projectPath: string, issueNumber: number) => this.post('/api/github/validation-mark-viewed', { projectPath, issueNumber }), onValidationEvent: (callback: (event: IssueValidationEvent) => void) => this.subscribeToEvent('issue-validation:event', callback as EventCallback), }; // Workspace API workspace = { getConfig: (): Promise<{ success: boolean; configured: boolean; workspaceDir?: string; defaultDir?: string | null; error?: string; }> => this.get('/api/workspace/config'), getDirectories: (): Promise<{ success: boolean; directories?: Array<{ name: string; path: string }>; error?: string; }> => this.get('/api/workspace/directories'), }; // Agent API agent = { start: ( sessionId: string, workingDirectory?: string ): Promise<{ success: boolean; messages?: Message[]; error?: string; }> => this.post('/api/agent/start', { sessionId, workingDirectory }), send: ( sessionId: string, message: string, workingDirectory?: string, imagePaths?: string[], model?: string ): Promise<{ success: boolean; error?: string }> => this.post('/api/agent/send', { sessionId, message, workingDirectory, imagePaths, model, }), getHistory: ( sessionId: string ): Promise<{ success: boolean; messages?: Message[]; isRunning?: boolean; error?: string; }> => this.post('/api/agent/history', { sessionId }), stop: (sessionId: string): Promise<{ success: boolean; error?: string }> => this.post('/api/agent/stop', { sessionId }), clear: (sessionId: string): Promise<{ success: boolean; error?: string }> => this.post('/api/agent/clear', { sessionId }), onStream: (callback: (data: unknown) => void): (() => void) => { return this.subscribeToEvent('agent:stream', callback as EventCallback); }, // Queue management queueAdd: ( sessionId: string, message: string, imagePaths?: string[], model?: string ): Promise<{ success: boolean; queuedPrompt?: { id: string; message: string; imagePaths?: string[]; model?: string; addedAt: string; }; error?: string; }> => this.post('/api/agent/queue/add', { sessionId, message, imagePaths, model }), queueList: ( sessionId: string ): Promise<{ success: boolean; queue?: Array<{ id: string; message: string; imagePaths?: string[]; model?: string; addedAt: string; }>; error?: string; }> => this.post('/api/agent/queue/list', { sessionId }), queueRemove: ( sessionId: string, promptId: string ): Promise<{ success: boolean; error?: string }> => this.post('/api/agent/queue/remove', { sessionId, promptId }), queueClear: (sessionId: string): Promise<{ success: boolean; error?: string }> => this.post('/api/agent/queue/clear', { sessionId }), }; // Templates API templates = { clone: ( repoUrl: string, projectName: string, parentDir: string ): Promise<{ success: boolean; projectPath?: string; projectName?: string; error?: string; }> => this.post('/api/templates/clone', { repoUrl, projectName, parentDir }), }; // Settings API - persistent file-based settings settings = { // Get settings status (check if migration needed) getStatus: (): Promise<{ success: boolean; hasGlobalSettings: boolean; hasCredentials: boolean; dataDir: string; needsMigration: boolean; }> => this.get('/api/settings/status'), // Global settings getGlobal: (): Promise<{ success: boolean; settings?: { version: number; theme: string; sidebarOpen: boolean; chatHistoryOpen: boolean; kanbanCardDetailLevel: string; maxConcurrency: number; defaultSkipTests: boolean; enableDependencyBlocking: boolean; useWorktrees: boolean; showProfilesOnly: boolean; defaultPlanningMode: string; defaultRequirePlanApproval: boolean; defaultAIProfileId: string | null; muteDoneSound: boolean; enhancementModel: string; keyboardShortcuts: Record; aiProfiles: unknown[]; projects: unknown[]; trashedProjects: unknown[]; projectHistory: string[]; projectHistoryIndex: number; lastProjectDir?: string; recentFolders: string[]; worktreePanelCollapsed: boolean; lastSelectedSessionByProject: Record; }; error?: string; }> => this.get('/api/settings/global'), updateGlobal: ( updates: Record ): Promise<{ success: boolean; settings?: Record; error?: string; }> => this.put('/api/settings/global', updates), // Credentials (masked for security) getCredentials: (): Promise<{ success: boolean; credentials?: { anthropic: { configured: boolean; masked: string }; google: { configured: boolean; masked: string }; openai: { configured: boolean; masked: string }; }; error?: string; }> => this.get('/api/settings/credentials'), updateCredentials: (updates: { apiKeys?: { anthropic?: string; google?: string; openai?: string }; }): Promise<{ success: boolean; credentials?: { anthropic: { configured: boolean; masked: string }; google: { configured: boolean; masked: string }; openai: { configured: boolean; masked: string }; }; error?: string; }> => this.put('/api/settings/credentials', updates), // Project settings getProject: ( projectPath: string ): Promise<{ success: boolean; settings?: { version: number; theme?: string; useWorktrees?: boolean; currentWorktree?: { path: string | null; branch: string }; worktrees?: Array<{ path: string; branch: string; isMain: boolean; hasChanges?: boolean; changedFilesCount?: number; }>; boardBackground?: { imagePath: string | null; imageVersion?: number; cardOpacity: number; columnOpacity: number; columnBorderEnabled: boolean; cardGlassmorphism: boolean; cardBorderEnabled: boolean; cardBorderOpacity: number; hideScrollbar: boolean; }; lastSelectedSessionId?: string; }; error?: string; }> => this.post('/api/settings/project', { projectPath }), updateProject: ( projectPath: string, updates: Record ): Promise<{ success: boolean; settings?: Record; error?: string; }> => this.put('/api/settings/project', { projectPath, updates }), // Migration from localStorage migrate: (data: { 'automaker-storage'?: string; 'automaker-setup'?: string; 'worktree-panel-collapsed'?: string; 'file-browser-recent-folders'?: string; 'automaker:lastProjectDir'?: string; }): Promise<{ success: boolean; migratedGlobalSettings: boolean; migratedCredentials: boolean; migratedProjectCount: number; errors: string[]; }> => this.post('/api/settings/migrate', { data }), }; // Sessions API sessions = { list: ( includeArchived?: boolean ): Promise<{ success: boolean; sessions?: SessionListItem[]; error?: string; }> => this.get(`/api/sessions?includeArchived=${includeArchived || false}`), create: ( name: string, projectPath: string, workingDirectory?: string ): Promise<{ success: boolean; session?: { id: string; name: string; projectPath: string; workingDirectory?: string; createdAt: string; updatedAt: string; }; error?: string; }> => this.post('/api/sessions', { name, projectPath, workingDirectory }), update: ( sessionId: string, name?: string, tags?: string[] ): Promise<{ success: boolean; error?: string }> => this.put(`/api/sessions/${sessionId}`, { name, tags }), archive: (sessionId: string): Promise<{ success: boolean; error?: string }> => this.post(`/api/sessions/${sessionId}/archive`, {}), unarchive: (sessionId: string): Promise<{ success: boolean; error?: string }> => this.post(`/api/sessions/${sessionId}/unarchive`, {}), delete: (sessionId: string): Promise<{ success: boolean; error?: string }> => this.httpDelete(`/api/sessions/${sessionId}`), }; // Claude API claude = { getUsage: (): Promise => this.get('/api/claude/usage'), }; // Context API context = { describeImage: ( imagePath: string ): Promise<{ success: boolean; description?: string; error?: string; }> => this.post('/api/context/describe-image', { imagePath }), describeFile: ( filePath: string ): Promise<{ success: boolean; description?: string; error?: string; }> => this.post('/api/context/describe-file', { filePath }), }; // Backlog Plan API backlogPlan = { generate: ( projectPath: string, prompt: string, model?: string ): Promise<{ success: boolean; error?: string }> => this.post('/api/backlog-plan/generate', { projectPath, prompt, model }), stop: (): Promise<{ success: boolean; error?: string }> => this.post('/api/backlog-plan/stop', {}), status: (): Promise<{ success: boolean; isRunning?: boolean; error?: string }> => this.get('/api/backlog-plan/status'), apply: ( projectPath: string, plan: { changes: Array<{ type: 'add' | 'update' | 'delete'; featureId?: string; feature?: Record; reason: string; }>; summary: string; dependencyUpdates: Array<{ featureId: string; removedDependencies: string[]; addedDependencies: string[]; }>; } ): Promise<{ success: boolean; appliedChanges?: string[]; error?: string }> => this.post('/api/backlog-plan/apply', { projectPath, plan }), onEvent: (callback: (data: unknown) => void): (() => void) => { return this.subscribeToEvent('backlog-plan:event', callback as EventCallback); }, }; } // Singleton instance let httpApiClientInstance: HttpApiClient | null = null; export function getHttpApiClient(): HttpApiClient { if (!httpApiClientInstance) { httpApiClientInstance = new HttpApiClient(); } return httpApiClientInstance; }