| |
| |
| |
| |
| |
| |
|
|
| import { createLogger } from '@automaker/utils/logger'; |
| import type { |
| ElectronAPI, |
| FileResult, |
| WriteResult, |
| ReaddirResult, |
| StatResult, |
| DialogResult, |
| SaveImageResult, |
| AutoModeAPI, |
| FeaturesAPI, |
| SpecRegenerationAPI, |
| AutoModeEvent, |
| SpecRegenerationEvent, |
| GitHubAPI, |
| IssueValidationInput, |
| IssueValidationEvent, |
| IdeationAPI, |
| IdeaCategory, |
| AnalysisSuggestion, |
| StartSessionOptions, |
| CreateIdeaInput, |
| UpdateIdeaInput, |
| ConvertToFeatureOptions, |
| NotificationsAPI, |
| EventHistoryAPI, |
| CreatePROptions, |
| } from './electron'; |
| import type { |
| IdeationContextSources, |
| EventHistoryFilter, |
| IdeationStreamEvent, |
| IdeationAnalysisEvent, |
| Notification, |
| } from '@automaker/types'; |
| import type { Message, SessionListItem } from '@/types/electron'; |
| import type { |
| ClaudeUsageResponse, |
| CodexUsageResponse, |
| GeminiUsage, |
| ZaiUsageResponse, |
| } from '@/store/app-store'; |
| import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron'; |
| import type { ModelId, ThinkingLevel, ReasoningEffort, Feature } from '@automaker/types'; |
| import { getGlobalFileBrowser } from '@/contexts/file-browser-context'; |
|
|
| const logger = createLogger('HttpClient'); |
| const NO_STORE_CACHE_MODE: RequestCache = 'no-store'; |
|
|
| |
| let cachedServerUrl: string | null = null; |
|
|
| |
| |
| |
| |
| const notifyLoggedOut = (): void => { |
| if (typeof window === 'undefined') return; |
| try { |
| window.dispatchEvent(new CustomEvent('automaker:logged-out')); |
| } catch { |
| |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| const handleUnauthorized = (): void => { |
| clearSessionToken(); |
| |
| fetch(`${getServerUrl()}/api/auth/logout`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| credentials: 'include', |
| body: '{}', |
| cache: NO_STORE_CACHE_MODE, |
| }).catch(() => {}); |
| notifyLoggedOut(); |
| }; |
|
|
| |
| |
| |
| |
| const notifyServerOffline = (): void => { |
| if (typeof window === 'undefined') return; |
| try { |
| window.dispatchEvent(new CustomEvent('automaker:server-offline')); |
| } catch { |
| |
| } |
| }; |
|
|
| |
| |
| |
| |
| export const isConnectionError = (error: unknown): boolean => { |
| if (error instanceof TypeError) { |
| const message = error.message.toLowerCase(); |
| return ( |
| message.includes('failed to fetch') || |
| message.includes('network') || |
| message.includes('econnrefused') || |
| message.includes('connection refused') |
| ); |
| } |
| |
| if (error && typeof error === 'object' && 'message' in error) { |
| const message = String((error as { message: unknown }).message).toLowerCase(); |
| return ( |
| message.includes('failed to fetch') || |
| message.includes('network') || |
| message.includes('econnrefused') || |
| message.includes('connection refused') |
| ); |
| } |
| return false; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| let serverOfflineCheckPending = false; |
|
|
| export const handleServerOffline = (): void => { |
| |
| if (serverOfflineCheckPending) return; |
| serverOfflineCheckPending = true; |
|
|
| |
| setTimeout(() => { |
| (async () => { |
| try { |
| const response = await fetch(`${getServerUrl()}/api/health`, { |
| method: 'GET', |
| cache: NO_STORE_CACHE_MODE, |
| signal: AbortSignal.timeout(5000), |
| }); |
| if (response.ok) { |
| logger.info('Server health check passed, ignoring transient connection error'); |
| return; |
| } |
| } catch { |
| |
| } |
|
|
| logger.error('Server appears to be offline, redirecting to login...'); |
| notifyServerOffline(); |
| })().finally(() => { |
| serverOfflineCheckPending = false; |
| }); |
| }, 2000); |
| }; |
|
|
| |
| |
| |
| |
| export const initServerUrl = async (): Promise<void> => { |
| const electron = typeof window !== 'undefined' ? window.electronAPI : null; |
| if (electron?.getServerUrl) { |
| try { |
| cachedServerUrl = await electron.getServerUrl(); |
| logger.info('Server URL from Electron:', cachedServerUrl); |
| } catch (error) { |
| logger.warn('Failed to get server URL from Electron:', error); |
| } |
| } |
| }; |
|
|
| |
| const getServerUrl = (): string => { |
| |
| if (cachedServerUrl) { |
| return cachedServerUrl; |
| } |
|
|
| if (typeof window !== 'undefined') { |
| const envUrl = import.meta.env.VITE_SERVER_URL; |
| if (envUrl) return envUrl; |
|
|
| |
| |
| if (!window.isElectron) { |
| return ''; |
| } |
| } |
| |
| const hostname = import.meta.env.VITE_HOSTNAME || 'localhost'; |
| return `http://${hostname}:3008`; |
| }; |
|
|
| |
| |
| |
| export const getServerUrlSync = (): string => getServerUrl(); |
|
|
| |
| let cachedApiKey: string | null = null; |
| let apiKeyInitialized = false; |
| let apiKeyInitPromise: Promise<void> | null = null; |
|
|
| |
| |
| let cachedSessionToken: string | null = null; |
| const SESSION_TOKEN_KEY = 'automaker:sessionToken'; |
|
|
| |
| |
| const initSessionToken = (): void => { |
| if (typeof window === 'undefined') return; |
| try { |
| cachedSessionToken = window.localStorage.getItem(SESSION_TOKEN_KEY); |
| } catch { |
| |
| cachedSessionToken = null; |
| } |
| }; |
|
|
| |
| initSessionToken(); |
|
|
| |
| |
| export const getApiKey = (): string | null => cachedApiKey; |
|
|
| |
| |
| |
| |
| export const waitForApiKeyInit = (): Promise<void> => { |
| if (apiKeyInitialized) return Promise.resolve(); |
| if (apiKeyInitPromise) return apiKeyInitPromise; |
| |
| return initApiKey(); |
| }; |
|
|
| |
| export const getSessionToken = (): string | null => cachedSessionToken; |
|
|
| |
| export const setSessionToken = (token: string | null): void => { |
| cachedSessionToken = token; |
| if (typeof window === 'undefined') return; |
| try { |
| if (token) { |
| window.localStorage.setItem(SESSION_TOKEN_KEY, token); |
| } else { |
| window.localStorage.removeItem(SESSION_TOKEN_KEY); |
| } |
| } catch { |
| |
| } |
| }; |
|
|
| |
| export const clearSessionToken = (): void => { |
| cachedSessionToken = null; |
| if (typeof window === 'undefined') return; |
| try { |
| window.localStorage.removeItem(SESSION_TOKEN_KEY); |
| } catch { |
| |
| } |
| }; |
|
|
| |
| |
| |
| export const isElectronMode = (): boolean => { |
| if (typeof window === 'undefined') return false; |
|
|
| |
| |
| |
| const api = window.electronAPI; |
| return api?.isElectron === true || !!api?.getApiKey; |
| }; |
|
|
| |
| let cachedExternalServerMode: boolean | null = null; |
|
|
| |
| |
| |
| |
| export const checkExternalServerMode = async (): Promise<boolean> => { |
| if (cachedExternalServerMode !== null) { |
| return cachedExternalServerMode; |
| } |
|
|
| if (typeof window !== 'undefined') { |
| const api = window.electronAPI; |
| if (api?.isExternalServerMode) { |
| try { |
| cachedExternalServerMode = Boolean(await api.isExternalServerMode()); |
| return cachedExternalServerMode; |
| } catch (error) { |
| logger.warn('Failed to check external server mode:', error); |
| } |
| } |
| } |
|
|
| cachedExternalServerMode = false; |
| return false; |
| }; |
|
|
| |
| |
| |
| export const isExternalServerMode = (): boolean | null => cachedExternalServerMode; |
|
|
| |
| |
| |
| |
| |
| |
| export const initApiKey = async (): Promise<void> => { |
| |
| if (apiKeyInitPromise) return apiKeyInitPromise; |
|
|
| |
| if (apiKeyInitialized) return; |
|
|
| |
| apiKeyInitPromise = (async () => { |
| try { |
| |
| await initServerUrl(); |
|
|
| |
| if (typeof window !== 'undefined' && window.electronAPI?.getApiKey) { |
| try { |
| cachedApiKey = await window.electronAPI.getApiKey(); |
| if (cachedApiKey) { |
| logger.info('Using API key from Electron'); |
| return; |
| } |
| } catch (error) { |
| logger.warn('Failed to get API key from Electron:', error); |
| } |
| } |
|
|
| |
| logger.info('Web mode - using cookie-based authentication'); |
| } finally { |
| |
| apiKeyInitialized = true; |
| } |
| })(); |
|
|
| return apiKeyInitPromise; |
| }; |
|
|
| |
| |
| |
| export const checkAuthStatus = async (): Promise<{ |
| authenticated: boolean; |
| required: boolean; |
| }> => { |
| try { |
| const response = await fetch(`${getServerUrl()}/api/auth/status`, { |
| credentials: 'include', |
| headers: getApiKey() ? { 'X-API-Key': getApiKey()! } : undefined, |
| cache: NO_STORE_CACHE_MODE, |
| }); |
| const data = await response.json(); |
| return { |
| authenticated: data.authenticated ?? false, |
| required: data.required ?? true, |
| }; |
| } catch (error) { |
| logger.error('Failed to check auth status:', error); |
| return { authenticated: false, required: true }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| export const login = async ( |
| apiKey: string |
| ): Promise<{ success: boolean; error?: string; token?: string }> => { |
| try { |
| const response = await fetch(`${getServerUrl()}/api/auth/login`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| credentials: 'include', |
| body: JSON.stringify({ apiKey }), |
| cache: NO_STORE_CACHE_MODE, |
| }); |
| const data = await response.json(); |
|
|
| |
| if (data.success && data.token) { |
| setSessionToken(data.token); |
| logger.info('Session token stored after login'); |
|
|
| |
| const verified = await verifySession(); |
| if (!verified) { |
| logger.error('Login appeared successful but session verification failed'); |
| return { |
| success: false, |
| error: 'Session verification failed. Please try again.', |
| }; |
| } |
| logger.info('Login verified successfully'); |
| } |
|
|
| return data; |
| } catch (error) { |
| logger.error('Login failed:', error); |
| return { success: false, error: 'Network error' }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| export const fetchSessionToken = async (): Promise<boolean> => { |
| |
| |
| |
| try { |
| const response = await fetch(`${getServerUrl()}/api/auth/status`, { |
| credentials: 'include', |
| cache: NO_STORE_CACHE_MODE, |
| }); |
|
|
| if (!response.ok) { |
| logger.info('Failed to check auth status'); |
| return false; |
| } |
|
|
| const data = await response.json(); |
| if (data.success && data.authenticated) { |
| logger.info('Session cookie is valid'); |
| return true; |
| } |
|
|
| logger.info('Session cookie is not authenticated'); |
| return false; |
| } catch (error) { |
| logger.error('Failed to check session:', error); |
| return false; |
| } |
| }; |
|
|
| |
| |
| |
| export const logout = async (): Promise<{ success: boolean }> => { |
| try { |
| const response = await fetch(`${getServerUrl()}/api/auth/logout`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| credentials: 'include', |
| cache: NO_STORE_CACHE_MODE, |
| }); |
|
|
| |
| clearSessionToken(); |
| logger.info('Session token cleared on logout'); |
|
|
| return await response.json(); |
| } catch (error) { |
| logger.error('Logout failed:', error); |
| return { success: false }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const verifySession = async (): Promise<boolean> => { |
| const headers: Record<string, string> = { |
| 'Content-Type': 'application/json', |
| }; |
|
|
| |
| const apiKey = getApiKey(); |
| if (apiKey) { |
| headers['X-API-Key'] = apiKey; |
| } |
|
|
| |
| const sessionToken = getSessionToken(); |
| if (sessionToken) { |
| headers['X-Session-Token'] = sessionToken; |
| } |
|
|
| |
| |
| |
| const response = await fetch(`${getServerUrl()}/api/settings/status`, { |
| headers, |
| credentials: 'include', |
| cache: NO_STORE_CACHE_MODE, |
| |
| signal: AbortSignal.timeout(2500), |
| }); |
|
|
| |
| if (response.status === 401 || response.status === 403) { |
| logger.warn('Session verification failed - session expired or invalid'); |
| |
| |
| |
| clearSessionToken(); |
| return false; |
| } |
|
|
| |
| if (!response.ok) { |
| const error = new Error(`Session verification failed with status: ${response.status}`); |
| logger.warn('Session verification failed with status:', response.status); |
| throw error; |
| } |
|
|
| logger.info('Session verified successfully'); |
| return true; |
| }; |
|
|
| |
| |
| |
| |
| export const checkSandboxEnvironment = async (): Promise<{ |
| isContainerized: boolean; |
| skipSandboxWarning?: boolean; |
| error?: string; |
| }> => { |
| try { |
| const response = await fetch(`${getServerUrl()}/api/health/environment`, { |
| method: 'GET', |
| cache: NO_STORE_CACHE_MODE, |
| signal: AbortSignal.timeout(5000), |
| }); |
|
|
| if (!response.ok) { |
| logger.warn('Failed to check sandbox environment'); |
| return { isContainerized: false, error: 'Failed to check environment' }; |
| } |
|
|
| const data = await response.json(); |
| return { |
| isContainerized: data.isContainerized ?? false, |
| skipSandboxWarning: data.skipSandboxWarning ?? false, |
| }; |
| } catch (error) { |
| logger.error('Sandbox environment check failed:', error); |
| return { isContainerized: false, error: 'Network error' }; |
| } |
| }; |
|
|
| type EventType = |
| | 'agent:stream' |
| | 'auto-mode:event' |
| | 'spec-regeneration:event' |
| | 'issue-validation:event' |
| | 'backlog-plan:event' |
| | 'ideation:stream' |
| | 'ideation:analysis' |
| | 'worktree:init-started' |
| | 'worktree:init-output' |
| | 'worktree:init-completed' |
| | 'dev-server:starting' |
| | 'dev-server:started' |
| | 'dev-server:output' |
| | 'dev-server:stopped' |
| | 'dev-server:url-detected' |
| | 'test-runner:started' |
| | 'test-runner:output' |
| | 'test-runner:completed' |
| | 'notification:created'; |
|
|
| |
| |
| |
|
|
| |
| interface DevServerUrlEvent { |
| worktreePath: string; |
| url: string; |
| port: number; |
| timestamp: string; |
| } |
|
|
| export interface DevServerStartingEvent { |
| worktreePath: string; |
| timestamp: string; |
| } |
|
|
| export type DevServerStartedEvent = DevServerUrlEvent; |
|
|
| export interface DevServerOutputEvent { |
| worktreePath: string; |
| content: string; |
| timestamp: string; |
| } |
|
|
| export interface DevServerStoppedEvent { |
| worktreePath: string; |
| port: number; |
| exitCode: number | null; |
| error?: string; |
| timestamp: string; |
| } |
|
|
| export type DevServerUrlDetectedEvent = DevServerUrlEvent; |
|
|
| export type DevServerLogEvent = |
| | { type: 'dev-server:starting'; payload: DevServerStartingEvent } |
| | { type: 'dev-server:started'; payload: DevServerStartedEvent } |
| | { type: 'dev-server:output'; payload: DevServerOutputEvent } |
| | { type: 'dev-server:stopped'; payload: DevServerStoppedEvent } |
| | { type: 'dev-server:url-detected'; payload: DevServerUrlDetectedEvent }; |
|
|
| |
| |
| |
| export type TestRunStatus = 'pending' | 'running' | 'passed' | 'failed' | 'cancelled' | 'error'; |
|
|
| export interface TestRunnerStartedEvent { |
| sessionId: string; |
| worktreePath: string; |
| |
| command: string; |
| testFile?: string; |
| timestamp: string; |
| } |
|
|
| export interface TestRunnerOutputEvent { |
| sessionId: string; |
| worktreePath: string; |
| content: string; |
| timestamp: string; |
| } |
|
|
| export interface TestRunnerCompletedEvent { |
| sessionId: string; |
| worktreePath: string; |
| |
| command: string; |
| status: TestRunStatus; |
| testFile?: string; |
| exitCode: number | null; |
| duration: number; |
| timestamp: string; |
| } |
|
|
| export type TestRunnerEvent = |
| | { type: 'test-runner:started'; payload: TestRunnerStartedEvent } |
| | { type: 'test-runner:output'; payload: TestRunnerOutputEvent } |
| | { type: 'test-runner:completed'; payload: TestRunnerCompletedEvent }; |
|
|
| |
| |
| |
| export interface DevServerLogsResponse { |
| success: boolean; |
| result?: { |
| worktreePath: string; |
| port: number; |
| url: string; |
| logs: string; |
| startedAt: string; |
| }; |
| error?: string; |
| } |
|
|
| |
| |
| |
| export interface TestLogsResponse { |
| success: boolean; |
| result?: { |
| sessionId: string; |
| worktreePath: string; |
| |
| command: string; |
| status: TestRunStatus; |
| testFile?: string; |
| logs: string; |
| startedAt: string; |
| finishedAt: string | null; |
| exitCode: number | null; |
| }; |
| error?: string; |
| } |
|
|
| type EventCallback = (payload: unknown) => void; |
|
|
| interface EnhancePromptResult { |
| success: boolean; |
| enhancedText?: string; |
| error?: string; |
| } |
|
|
| |
| |
| |
| export class HttpApiClient implements ElectronAPI { |
| private serverUrl: string; |
| private ws: WebSocket | null = null; |
| private eventCallbacks: Map<EventType, Set<EventCallback>> = new Map(); |
| private reconnectTimer: NodeJS.Timeout | null = null; |
| private isConnecting = false; |
| |
| private reconnectAttempts = 0; |
| |
| private visibilityHandler: (() => void) | null = null; |
|
|
| constructor() { |
| this.serverUrl = getServerUrl(); |
| |
| |
| |
| if (isElectronMode()) { |
| waitForApiKeyInit() |
| .then(() => { |
| this.connectWebSocket(); |
| }) |
| .catch((error) => { |
| logger.error('API key initialization failed:', error); |
| |
| this.connectWebSocket(); |
| }); |
| } |
|
|
| |
| |
| this.visibilityHandler = () => { |
| if (document.visibilityState === 'visible') { |
| |
| if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { |
| logger.info('Tab became visible - attempting immediate WebSocket reconnect'); |
| |
| if (this.reconnectTimer) { |
| clearTimeout(this.reconnectTimer); |
| this.reconnectTimer = null; |
| } |
| this.reconnectAttempts = 0; |
| |
| |
| |
| this.connectWebSocket({ silent: true }); |
| } |
| } |
| }; |
| if (typeof document !== 'undefined') { |
| document.addEventListener('visibilitychange', this.visibilityHandler); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private async fetchWsToken(options?: { silent?: boolean }): Promise<string | null> { |
| try { |
| const headers: Record<string, string> = { |
| 'Content-Type': 'application/json', |
| }; |
|
|
| |
| const sessionToken = getSessionToken(); |
| if (sessionToken) { |
| headers['X-Session-Token'] = sessionToken; |
| } |
|
|
| const response = await fetch(`${this.serverUrl}/api/auth/token`, { |
| headers, |
| credentials: 'include', |
| cache: NO_STORE_CACHE_MODE, |
| }); |
|
|
| if (response.status === 401 || response.status === 403) { |
| if (options?.silent) { |
| logger.debug('fetchWsToken: 401/403 during silent reconnect — skipping logout'); |
| } else { |
| handleUnauthorized(); |
| } |
| return null; |
| } |
|
|
| if (!response.ok) { |
| logger.warn('Failed to fetch wsToken:', response.status); |
| return null; |
| } |
|
|
| const data = await response.json(); |
| if (data.success && data.token) { |
| return data.token; |
| } |
|
|
| return null; |
| } catch (error) { |
| logger.error('Error fetching wsToken:', error); |
| return null; |
| } |
| } |
|
|
| private connectWebSocket(options?: { silent?: boolean }): void { |
| if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) { |
| return; |
| } |
|
|
| this.isConnecting = true; |
|
|
| |
| |
| waitForApiKeyInit() |
| .then(() => this.doConnectWebSocketInternal(options)) |
| .catch((error) => { |
| logger.error('Failed to initialize for WebSocket connection:', error); |
| this.isConnecting = false; |
| }); |
| } |
|
|
| private doConnectWebSocketInternal(options?: { silent?: boolean }): void { |
| |
| |
| |
| |
| if (isElectronMode()) { |
| const apiKey = getApiKey(); |
| if (!apiKey) { |
| logger.warn('Electron mode: API key missing, attempting wsToken/cookie auth for WebSocket'); |
| this.fetchWsToken(options) |
| .then((wsToken) => { |
| const wsUrl = this.serverUrl.replace(/^http/, 'ws') + '/api/events'; |
| if (wsToken) { |
| this.establishWebSocket(`${wsUrl}?wsToken=${encodeURIComponent(wsToken)}`); |
| } else { |
| |
| logger.warn('No wsToken available, attempting WebSocket connection anyway'); |
| this.establishWebSocket(wsUrl); |
| } |
| }) |
| .catch((error) => { |
| logger.error('Failed to prepare WebSocket connection (electron fallback):', error); |
| this.isConnecting = false; |
| }); |
| return; |
| } |
|
|
| const wsUrl = this.serverUrl.replace(/^http/, 'ws') + '/api/events'; |
| this.establishWebSocket(`${wsUrl}?apiKey=${encodeURIComponent(apiKey)}`); |
| return; |
| } |
|
|
| |
| this.fetchWsToken(options) |
| .then((wsToken) => { |
| const wsUrl = this.serverUrl.replace(/^http/, 'ws') + '/api/events'; |
| if (wsToken) { |
| this.establishWebSocket(`${wsUrl}?wsToken=${encodeURIComponent(wsToken)}`); |
| } else { |
| |
| logger.warn('No wsToken available, attempting connection anyway'); |
| this.establishWebSocket(wsUrl); |
| } |
| }) |
| .catch((error) => { |
| logger.error('Failed to prepare WebSocket connection:', error); |
| this.isConnecting = false; |
| }); |
| } |
|
|
| |
| |
| |
| private establishWebSocket(wsUrl: string): void { |
| try { |
| this.ws = new WebSocket(wsUrl); |
|
|
| this.ws.onopen = () => { |
| logger.info('WebSocket connected'); |
| this.isConnecting = false; |
| this.reconnectAttempts = 0; |
| if (this.reconnectTimer) { |
| clearTimeout(this.reconnectTimer); |
| this.reconnectTimer = null; |
| } |
| }; |
|
|
| this.ws.onmessage = (event) => { |
| try { |
| const data = JSON.parse(event.data); |
| |
| |
| |
| |
| const isHighFrequency = |
| data.type === 'dev-server:output' || |
| data.type === 'test-runner:output' || |
| data.type === 'feature:progress' || |
| (data.type === 'auto-mode:event' && data.payload?.type === 'auto_mode_progress'); |
| if (!isHighFrequency) { |
| logger.info('WebSocket message:', data.type); |
| } |
| const callbacks = this.eventCallbacks.get(data.type); |
| if (callbacks) { |
| callbacks.forEach((cb) => cb(data.payload)); |
| } |
| } catch (error) { |
| logger.error('Failed to parse WebSocket message:', error); |
| } |
| }; |
|
|
| this.ws.onclose = () => { |
| logger.info('WebSocket disconnected'); |
| this.isConnecting = false; |
| this.ws = null; |
|
|
| |
| |
| if (!this.reconnectTimer) { |
| const backoffDelays = [0, 500, 1000, 2000, 5000]; |
| const delayMs = |
| backoffDelays[Math.min(this.reconnectAttempts, backoffDelays.length - 1)] ?? 5000; |
| this.reconnectAttempts++; |
|
|
| if (delayMs === 0) { |
| |
| this.connectWebSocket(); |
| } else { |
| logger.info( |
| `WebSocket reconnecting in ${delayMs}ms (attempt ${this.reconnectAttempts})` |
| ); |
| this.reconnectTimer = setTimeout(() => { |
| this.reconnectTimer = null; |
| this.connectWebSocket(); |
| }, delayMs); |
| } |
| } |
| }; |
|
|
| this.ws.onerror = (error) => { |
| logger.error('WebSocket error:', error); |
| this.isConnecting = false; |
| }; |
| } catch (error) { |
| logger.error('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); |
|
|
| |
| this.connectWebSocket(); |
|
|
| return () => { |
| const callbacks = this.eventCallbacks.get(type); |
| if (callbacks) { |
| callbacks.delete(callback); |
| } |
| }; |
| } |
|
|
| private getHeaders(): Record<string, string> { |
| const headers: Record<string, string> = { |
| 'Content-Type': 'application/json', |
| }; |
|
|
| |
| const apiKey = getApiKey(); |
| if (apiKey) { |
| headers['X-API-Key'] = apiKey; |
| return headers; |
| } |
|
|
| |
| const sessionToken = getSessionToken(); |
| if (sessionToken) { |
| headers['X-Session-Token'] = sessionToken; |
| } |
|
|
| return headers; |
| } |
|
|
| private async post<T>(endpoint: string, body?: unknown, signal?: AbortSignal): Promise<T> { |
| |
| await waitForApiKeyInit(); |
| const response = await fetch(`${this.serverUrl}${endpoint}`, { |
| method: 'POST', |
| headers: this.getHeaders(), |
| credentials: 'include', |
| body: body ? JSON.stringify(body) : undefined, |
| signal, |
| }); |
|
|
| if (response.status === 401 || response.status === 403) { |
| handleUnauthorized(); |
| throw new Error('Unauthorized'); |
| } |
|
|
| if (!response.ok) { |
| let errorMessage = `HTTP ${response.status}: ${response.statusText}`; |
| try { |
| const errorData = await response.json(); |
| if (errorData.error) { |
| errorMessage = errorData.error; |
| } |
| } catch { |
| |
| } |
| throw new Error(errorMessage); |
| } |
|
|
| return response.json(); |
| } |
|
|
| async get<T>(endpoint: string): Promise<T> { |
| |
| await waitForApiKeyInit(); |
| const response = await fetch(`${this.serverUrl}${endpoint}`, { |
| headers: this.getHeaders(), |
| credentials: 'include', |
| cache: NO_STORE_CACHE_MODE, |
| }); |
|
|
| if (response.status === 401 || response.status === 403) { |
| handleUnauthorized(); |
| throw new Error('Unauthorized'); |
| } |
|
|
| if (!response.ok) { |
| let errorMessage = `HTTP ${response.status}: ${response.statusText}`; |
| try { |
| const errorData = await response.json(); |
| if (errorData.error) { |
| errorMessage = errorData.error; |
| } |
| } catch { |
| |
| } |
| throw new Error(errorMessage); |
| } |
|
|
| return response.json(); |
| } |
|
|
| async put<T>(endpoint: string, body?: unknown): Promise<T> { |
| |
| await waitForApiKeyInit(); |
| const response = await fetch(`${this.serverUrl}${endpoint}`, { |
| method: 'PUT', |
| headers: this.getHeaders(), |
| credentials: 'include', |
| body: body ? JSON.stringify(body) : undefined, |
| }); |
|
|
| if (response.status === 401 || response.status === 403) { |
| handleUnauthorized(); |
| throw new Error('Unauthorized'); |
| } |
|
|
| if (!response.ok) { |
| let errorMessage = `HTTP ${response.status}: ${response.statusText}`; |
| try { |
| const errorData = await response.json(); |
| if (errorData.error) { |
| errorMessage = errorData.error; |
| } |
| } catch { |
| |
| } |
| throw new Error(errorMessage); |
| } |
|
|
| return response.json(); |
| } |
|
|
| private async httpDelete<T>(endpoint: string, body?: unknown): Promise<T> { |
| |
| await waitForApiKeyInit(); |
| const response = await fetch(`${this.serverUrl}${endpoint}`, { |
| method: 'DELETE', |
| headers: this.getHeaders(), |
| credentials: 'include', |
| body: body ? JSON.stringify(body) : undefined, |
| }); |
|
|
| if (response.status === 401 || response.status === 403) { |
| handleUnauthorized(); |
| throw new Error('Unauthorized'); |
| } |
|
|
| if (!response.ok) { |
| let errorMessage = `HTTP ${response.status}: ${response.statusText}`; |
| try { |
| const errorData = await response.json(); |
| if (errorData.error) { |
| errorMessage = errorData.error; |
| } |
| } catch { |
| |
| } |
| throw new Error(errorMessage); |
| } |
|
|
| return response.json(); |
| } |
|
|
| |
| async ping(): Promise<string> { |
| const result = await this.get<{ status: string }>('/api/health'); |
| return result.status === 'ok' ? 'pong' : 'error'; |
| } |
|
|
| async openExternalLink(url: string): Promise<{ success: boolean; error?: string }> { |
| |
| window.open(url, '_blank', 'noopener,noreferrer'); |
| return { success: true }; |
| } |
|
|
| async openInEditor( |
| filePath: string, |
| line?: number, |
| column?: number |
| ): Promise<{ success: boolean; error?: string }> { |
| |
| |
| |
| |
| 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 { |
| |
| |
| 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', |
| }; |
| } |
| } |
|
|
| |
| async openDirectory(): Promise<DialogResult> { |
| const fileBrowser = getGlobalFileBrowser(); |
|
|
| if (!fileBrowser) { |
| logger.error('File browser not initialized'); |
| return { canceled: true, filePaths: [] }; |
| } |
|
|
| const path = await fileBrowser(); |
|
|
| if (!path) { |
| return { canceled: true, filePaths: [] }; |
| } |
|
|
| |
| const result = await this.post<{ |
| success: boolean; |
| path?: string; |
| isAllowed?: boolean; |
| error?: string; |
| }>('/api/fs/validate-path', { filePath: path }); |
|
|
| if (result.success && result.path && result.isAllowed !== false) { |
| return { canceled: false, filePaths: [result.path] }; |
| } |
|
|
| logger.error('Invalid directory:', result.error || 'Path not allowed'); |
| return { canceled: true, filePaths: [] }; |
| } |
|
|
| async openFile(_options?: object): Promise<DialogResult> { |
| const fileBrowser = getGlobalFileBrowser(); |
|
|
| if (!fileBrowser) { |
| logger.error('File browser not initialized'); |
| return { canceled: true, filePaths: [] }; |
| } |
|
|
| |
| 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] }; |
| } |
|
|
| logger.error('File not found'); |
| return { canceled: true, filePaths: [] }; |
| } |
|
|
| |
| async readFile(filePath: string): Promise<FileResult> { |
| return this.post('/api/fs/read', { filePath }); |
| } |
|
|
| async writeFile(filePath: string, content: string): Promise<WriteResult> { |
| return this.post('/api/fs/write', { filePath, content }); |
| } |
|
|
| async mkdir(dirPath: string): Promise<WriteResult> { |
| return this.post('/api/fs/mkdir', { dirPath }); |
| } |
|
|
| async readdir(dirPath: string): Promise<ReaddirResult> { |
| return this.post('/api/fs/readdir', { dirPath }); |
| } |
|
|
| async exists(filePath: string): Promise<boolean> { |
| const result = await this.post<{ success: boolean; exists: boolean }>('/api/fs/exists', { |
| filePath, |
| }); |
| return result.exists; |
| } |
|
|
| async stat(filePath: string): Promise<StatResult> { |
| return this.post('/api/fs/stat', { filePath }); |
| } |
|
|
| async deleteFile(filePath: string): Promise<WriteResult> { |
| return this.post('/api/fs/delete', { filePath }); |
| } |
|
|
| async trashItem(filePath: string): Promise<WriteResult> { |
| |
| return this.deleteFile(filePath); |
| } |
|
|
| async copyItem( |
| sourcePath: string, |
| destinationPath: string, |
| overwrite?: boolean |
| ): Promise<WriteResult & { exists?: boolean }> { |
| return this.post('/api/fs/copy', { sourcePath, destinationPath, overwrite }); |
| } |
|
|
| async moveItem( |
| sourcePath: string, |
| destinationPath: string, |
| overwrite?: boolean |
| ): Promise<WriteResult & { exists?: boolean }> { |
| return this.post('/api/fs/move', { sourcePath, destinationPath, overwrite }); |
| } |
|
|
| async downloadItem(filePath: string): Promise<void> { |
| const serverUrl = getServerUrl(); |
| const headers: Record<string, string> = { |
| 'Content-Type': 'application/json', |
| }; |
| const apiKey = getApiKey(); |
| if (apiKey) { |
| headers['X-API-Key'] = apiKey; |
| } |
| const token = getSessionToken(); |
| if (token) { |
| headers['X-Session-Token'] = token; |
| } |
|
|
| const response = await fetch(`${serverUrl}/api/fs/download`, { |
| method: 'POST', |
| headers, |
| credentials: 'include', |
| body: JSON.stringify({ filePath }), |
| }); |
|
|
| if (response.status === 401 || response.status === 403) { |
| handleUnauthorized(); |
| throw new Error('Unauthorized'); |
| } |
|
|
| if (!response.ok) { |
| const error = await response.json().catch(() => ({ error: 'Download failed' })); |
| throw new Error(error.error || `Download failed with status ${response.status}`); |
| } |
|
|
| |
| const blob = await response.blob(); |
| const contentDisposition = response.headers.get('Content-Disposition'); |
| const fileNameMatch = contentDisposition?.match(/filename="(.+)"/); |
| const fileName = fileNameMatch ? fileNameMatch[1] : filePath.split('/').pop() || 'download'; |
|
|
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = fileName; |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| URL.revokeObjectURL(url); |
| } |
|
|
| async getPath(name: string): Promise<string> { |
| |
| 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<SaveImageResult> { |
| 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 }); |
| } |
|
|
| |
| 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 = { |
| getAvailable: async (): Promise<{ |
| success: boolean; |
| models?: ModelDefinition[]; |
| error?: string; |
| }> => { |
| return this.get('/api/models/available'); |
| }, |
| checkProviders: async (): Promise<{ |
| success: boolean; |
| providers?: Record<string, ProviderStatus>; |
| error?: string; |
| }> => { |
| return this.get('/api/models/providers'); |
| }, |
| }; |
|
|
| |
| 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'), |
|
|
| deauthClaude: (): Promise<{ |
| success: boolean; |
| requiresManualDeauth?: boolean; |
| command?: string; |
| message?: string; |
| error?: string; |
| }> => this.post('/api/setup/deauth-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; |
| hasOpenaiKey: 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', |
| apiKey?: string |
| ): Promise<{ |
| success: boolean; |
| authenticated: boolean; |
| authType?: 'oauth' | 'api_key' | 'cli'; |
| error?: string; |
| }> => this.post('/api/setup/verify-claude-auth', { authMethod, apiKey }), |
|
|
| 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'), |
|
|
| |
| getCursorStatus: (): Promise<{ |
| success: boolean; |
| installed?: boolean; |
| version?: string | null; |
| path?: string | null; |
| auth?: { |
| authenticated: boolean; |
| method: string; |
| }; |
| installCommand?: string; |
| loginCommand?: string; |
| error?: string; |
| }> => this.get('/api/setup/cursor-status'), |
|
|
| authCursor: (): Promise<{ |
| success: boolean; |
| token?: string; |
| requiresManualAuth?: boolean; |
| terminalOpened?: boolean; |
| command?: string; |
| message?: string; |
| output?: string; |
| }> => this.post('/api/setup/auth-cursor'), |
|
|
| deauthCursor: (): Promise<{ |
| success: boolean; |
| requiresManualDeauth?: boolean; |
| command?: string; |
| message?: string; |
| error?: string; |
| }> => this.post('/api/setup/deauth-cursor'), |
|
|
| authOpencode: (): Promise<{ |
| success: boolean; |
| token?: string; |
| requiresManualAuth?: boolean; |
| terminalOpened?: boolean; |
| command?: string; |
| message?: string; |
| output?: string; |
| }> => this.post('/api/setup/auth-opencode'), |
|
|
| deauthOpencode: (): Promise<{ |
| success: boolean; |
| requiresManualDeauth?: boolean; |
| command?: string; |
| message?: string; |
| error?: string; |
| }> => this.post('/api/setup/deauth-opencode'), |
|
|
| getCursorConfig: ( |
| projectPath: string |
| ): Promise<{ |
| success: boolean; |
| config?: { |
| defaultModel?: string; |
| models?: string[]; |
| mcpServers?: string[]; |
| rules?: string[]; |
| }; |
| availableModels?: Array<{ |
| id: string; |
| label: string; |
| description: string; |
| hasThinking: boolean; |
| tier: 'free' | 'pro'; |
| }>; |
| error?: string; |
| }> => this.get(`/api/setup/cursor-config?projectPath=${encodeURIComponent(projectPath)}`), |
|
|
| setCursorDefaultModel: ( |
| projectPath: string, |
| model: string |
| ): Promise<{ |
| success: boolean; |
| model?: string; |
| error?: string; |
| }> => this.post('/api/setup/cursor-config/default-model', { projectPath, model }), |
|
|
| setCursorModels: ( |
| projectPath: string, |
| models: string[] |
| ): Promise<{ |
| success: boolean; |
| models?: string[]; |
| error?: string; |
| }> => this.post('/api/setup/cursor-config/models', { projectPath, models }), |
|
|
| |
| getCursorPermissions: ( |
| projectPath?: string |
| ): Promise<{ |
| success: boolean; |
| globalPermissions?: { allow: string[]; deny: string[] } | null; |
| projectPermissions?: { allow: string[]; deny: string[] } | null; |
| effectivePermissions?: { allow: string[]; deny: string[] } | null; |
| activeProfile?: 'strict' | 'development' | 'custom' | null; |
| hasProjectConfig?: boolean; |
| availableProfiles?: Array<{ |
| id: string; |
| name: string; |
| description: string; |
| permissions: { allow: string[]; deny: string[] }; |
| }>; |
| error?: string; |
| }> => |
| this.get( |
| `/api/setup/cursor-permissions${projectPath ? `?projectPath=${encodeURIComponent(projectPath)}` : ''}` |
| ), |
|
|
| applyCursorPermissionProfile: ( |
| profileId: 'strict' | 'development', |
| scope: 'global' | 'project', |
| projectPath?: string |
| ): Promise<{ |
| success: boolean; |
| message?: string; |
| scope?: string; |
| profileId?: string; |
| error?: string; |
| }> => this.post('/api/setup/cursor-permissions/profile', { profileId, scope, projectPath }), |
|
|
| setCursorCustomPermissions: ( |
| projectPath: string, |
| permissions: { allow: string[]; deny: string[] } |
| ): Promise<{ |
| success: boolean; |
| message?: string; |
| permissions?: { allow: string[]; deny: string[] }; |
| error?: string; |
| }> => this.post('/api/setup/cursor-permissions/custom', { projectPath, permissions }), |
|
|
| deleteCursorProjectPermissions: ( |
| projectPath: string |
| ): Promise<{ |
| success: boolean; |
| message?: string; |
| error?: string; |
| }> => |
| this.httpDelete( |
| `/api/setup/cursor-permissions?projectPath=${encodeURIComponent(projectPath)}` |
| ), |
|
|
| getCursorExampleConfig: ( |
| profileId?: 'strict' | 'development' |
| ): Promise<{ |
| success: boolean; |
| profileId?: string; |
| config?: string; |
| error?: string; |
| }> => |
| this.get( |
| `/api/setup/cursor-permissions/example${profileId ? `?profileId=${profileId}` : ''}` |
| ), |
|
|
| |
| getCodexStatus: (): Promise<{ |
| success: boolean; |
| status?: string; |
| installed?: boolean; |
| method?: string; |
| version?: string; |
| path?: string; |
| auth?: { |
| authenticated: boolean; |
| method: string; |
| hasAuthFile?: boolean; |
| hasOAuthToken?: boolean; |
| hasApiKey?: boolean; |
| hasStoredApiKey?: boolean; |
| hasEnvApiKey?: boolean; |
| }; |
| error?: string; |
| }> => this.get('/api/setup/codex-status'), |
|
|
| installCodex: (): Promise<{ |
| success: boolean; |
| message?: string; |
| error?: string; |
| }> => this.post('/api/setup/install-codex'), |
|
|
| authCodex: (): Promise<{ |
| success: boolean; |
| token?: string; |
| requiresManualAuth?: boolean; |
| terminalOpened?: boolean; |
| command?: string; |
| error?: string; |
| message?: string; |
| output?: string; |
| }> => this.post('/api/setup/auth-codex'), |
|
|
| deauthCodex: (): Promise<{ |
| success: boolean; |
| requiresManualDeauth?: boolean; |
| command?: string; |
| message?: string; |
| error?: string; |
| }> => this.post('/api/setup/deauth-codex'), |
|
|
| verifyCodexAuth: ( |
| authMethod: 'cli' | 'api_key', |
| apiKey?: string |
| ): Promise<{ |
| success: boolean; |
| authenticated: boolean; |
| error?: string; |
| }> => this.post('/api/setup/verify-codex-auth', { authMethod, apiKey }), |
|
|
| |
| getOpencodeStatus: (): Promise<{ |
| success: boolean; |
| status?: string; |
| installed?: boolean; |
| method?: string; |
| version?: string; |
| path?: string; |
| recommendation?: string; |
| installCommands?: { |
| macos?: string; |
| linux?: string; |
| npm?: string; |
| }; |
| auth?: { |
| authenticated: boolean; |
| method: string; |
| hasAuthFile?: boolean; |
| hasOAuthToken?: boolean; |
| hasApiKey?: boolean; |
| hasStoredApiKey?: boolean; |
| hasEnvApiKey?: boolean; |
| }; |
| error?: string; |
| }> => this.get('/api/setup/opencode-status'), |
|
|
| |
| getOpencodeModels: ( |
| refresh?: boolean |
| ): Promise<{ |
| success: boolean; |
| models?: Array<{ |
| id: string; |
| name: string; |
| modelString: string; |
| provider: string; |
| description: string; |
| supportsTools: boolean; |
| supportsVision: boolean; |
| tier: string; |
| default?: boolean; |
| }>; |
| count?: number; |
| cached?: boolean; |
| error?: string; |
| }> => this.get(`/api/setup/opencode/models${refresh ? '?refresh=true' : ''}`), |
|
|
| refreshOpencodeModels: (): Promise<{ |
| success: boolean; |
| models?: Array<{ |
| id: string; |
| name: string; |
| modelString: string; |
| provider: string; |
| description: string; |
| supportsTools: boolean; |
| supportsVision: boolean; |
| tier: string; |
| default?: boolean; |
| }>; |
| count?: number; |
| error?: string; |
| }> => this.post('/api/setup/opencode/models/refresh'), |
|
|
| getOpencodeProviders: (): Promise<{ |
| success: boolean; |
| providers?: Array<{ |
| id: string; |
| name: string; |
| authenticated: boolean; |
| authMethod?: 'oauth' | 'api_key'; |
| }>; |
| authenticated?: Array<{ |
| id: string; |
| name: string; |
| authenticated: boolean; |
| authMethod?: 'oauth' | 'api_key'; |
| }>; |
| error?: string; |
| }> => this.get('/api/setup/opencode/providers'), |
|
|
| clearOpencodeCache: (): Promise<{ |
| success: boolean; |
| message?: string; |
| error?: string; |
| }> => this.post('/api/setup/opencode/cache/clear'), |
|
|
| |
| getGeminiStatus: (): Promise<{ |
| success: boolean; |
| status?: string; |
| installed?: boolean; |
| method?: string; |
| version?: string; |
| path?: string; |
| recommendation?: string; |
| installCommands?: { |
| macos?: string; |
| linux?: string; |
| npm?: string; |
| }; |
| auth?: { |
| authenticated: boolean; |
| method: string; |
| hasApiKey?: boolean; |
| hasEnvApiKey?: boolean; |
| error?: string; |
| }; |
| loginCommand?: string; |
| installCommand?: string; |
| error?: string; |
| }> => this.get('/api/setup/gemini-status'), |
|
|
| authGemini: (): Promise<{ |
| success: boolean; |
| requiresManualAuth?: boolean; |
| command?: string; |
| message?: string; |
| error?: string; |
| }> => this.post('/api/setup/auth-gemini'), |
|
|
| deauthGemini: (): Promise<{ |
| success: boolean; |
| requiresManualDeauth?: boolean; |
| command?: string; |
| message?: string; |
| error?: string; |
| }> => this.post('/api/setup/deauth-gemini'), |
|
|
| |
| getCopilotStatus: (): Promise<{ |
| success: boolean; |
| status?: string; |
| installed?: boolean; |
| method?: string; |
| version?: string; |
| path?: string; |
| recommendation?: string; |
| auth?: { |
| authenticated: boolean; |
| method: string; |
| login?: string; |
| host?: string; |
| error?: string; |
| }; |
| loginCommand?: string; |
| installCommand?: string; |
| error?: string; |
| }> => this.get('/api/setup/copilot-status'), |
|
|
| onInstallProgress: ( |
| callback: (progress: { cli?: string; data?: string; type?: string }) => void |
| ) => { |
| return this.subscribeToEvent('agent:stream', callback as EventCallback); |
| }, |
|
|
| onAuthProgress: ( |
| callback: (progress: { cli?: string; data?: string; type?: string }) => void |
| ) => { |
| return this.subscribeToEvent('agent:stream', callback as EventCallback); |
| }, |
| }; |
|
|
| |
| zai = { |
| getStatus: (): Promise<{ |
| success: boolean; |
| available: boolean; |
| message?: string; |
| hasApiKey?: boolean; |
| hasEnvApiKey?: boolean; |
| error?: string; |
| }> => this.get('/api/zai/status'), |
|
|
| getUsage: (): Promise<ZaiUsageResponse> => this.get('/api/zai/usage'), |
|
|
| configure: ( |
| apiToken?: string, |
| apiHost?: string |
| ): Promise<{ |
| success: boolean; |
| message?: string; |
| isAvailable?: boolean; |
| error?: string; |
| }> => this.post('/api/zai/configure', { apiToken, apiHost }), |
|
|
| verify: ( |
| apiKey: string |
| ): Promise<{ |
| success: boolean; |
| authenticated: boolean; |
| message?: string; |
| error?: string; |
| }> => this.post('/api/zai/verify', { apiKey }), |
| }; |
|
|
| |
| features: FeaturesAPI & { |
| bulkUpdate: ( |
| projectPath: string, |
| featureIds: string[], |
| updates: Partial<Feature> |
| ) => Promise<{ |
| success: boolean; |
| updatedCount?: number; |
| failedCount?: number; |
| results?: Array<{ featureId: string; success: boolean; error?: string }>; |
| features?: Feature[]; |
| error?: string; |
| }>; |
| bulkDelete: ( |
| projectPath: string, |
| featureIds: string[] |
| ) => Promise<{ |
| success: boolean; |
| deletedCount?: number; |
| failedCount?: number; |
| results?: Array<{ featureId: string; success: boolean; error?: string }>; |
| error?: string; |
| }>; |
| export: ( |
| projectPath: string, |
| options?: { |
| featureIds?: string[]; |
| format?: 'json' | 'yaml'; |
| includeHistory?: boolean; |
| includePlanSpec?: boolean; |
| category?: string; |
| status?: string; |
| prettyPrint?: boolean; |
| metadata?: Record<string, unknown>; |
| } |
| ) => Promise<{ |
| success: boolean; |
| data?: string; |
| format?: 'json' | 'yaml'; |
| contentType?: string; |
| filename?: string; |
| error?: string; |
| }>; |
| import: ( |
| projectPath: string, |
| data: string, |
| options?: { |
| overwrite?: boolean; |
| preserveBranchInfo?: boolean; |
| targetCategory?: string; |
| } |
| ) => Promise<{ |
| success: boolean; |
| importedCount?: number; |
| failedCount?: number; |
| results?: Array<{ |
| success: boolean; |
| featureId?: string; |
| importedAt: string; |
| warnings?: string[]; |
| errors?: string[]; |
| wasOverwritten?: boolean; |
| }>; |
| error?: string; |
| }>; |
| checkConflicts: ( |
| projectPath: string, |
| data: string |
| ) => Promise<{ |
| success: boolean; |
| hasConflicts?: boolean; |
| conflicts?: Array<{ |
| featureId: string; |
| title?: string; |
| existingTitle?: string; |
| hasConflict: boolean; |
| }>; |
| totalFeatures?: number; |
| conflictCount?: number; |
| error?: string; |
| }>; |
| } = { |
| getAll: (projectPath: string) => |
| this.get(`/api/features/list?projectPath=${encodeURIComponent(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<Feature>, |
| descriptionHistorySource?: 'enhance' | 'edit', |
| enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer', |
| preEnhancementDescription?: string |
| ) => |
| this.post('/api/features/update', { |
| projectPath, |
| featureId, |
| updates, |
| descriptionHistorySource, |
| enhancementMode, |
| preEnhancementDescription, |
| }), |
| 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, projectPath?: string) => |
| this.post('/api/features/generate-title', { description, projectPath }), |
| bulkUpdate: (projectPath: string, featureIds: string[], updates: Partial<Feature>) => |
| this.post('/api/features/bulk-update', { projectPath, featureIds, updates }), |
| bulkDelete: (projectPath: string, featureIds: string[]) => |
| this.post('/api/features/bulk-delete', { projectPath, featureIds }), |
| export: ( |
| projectPath: string, |
| options?: { |
| featureIds?: string[]; |
| format?: 'json' | 'yaml'; |
| includeHistory?: boolean; |
| includePlanSpec?: boolean; |
| category?: string; |
| status?: string; |
| prettyPrint?: boolean; |
| metadata?: Record<string, unknown>; |
| } |
| ): Promise<{ |
| success: boolean; |
| data?: string; |
| format?: 'json' | 'yaml'; |
| contentType?: string; |
| filename?: string; |
| error?: string; |
| }> => this.post('/api/features/export', { projectPath, ...options }), |
| import: ( |
| projectPath: string, |
| data: string, |
| options?: { |
| overwrite?: boolean; |
| preserveBranchInfo?: boolean; |
| targetCategory?: string; |
| } |
| ): Promise<{ |
| success: boolean; |
| importedCount?: number; |
| failedCount?: number; |
| results?: Array<{ |
| success: boolean; |
| featureId?: string; |
| importedAt: string; |
| warnings?: string[]; |
| errors?: string[]; |
| wasOverwritten?: boolean; |
| }>; |
| error?: string; |
| }> => this.post('/api/features/import', { projectPath, data, ...options }), |
| checkConflicts: ( |
| projectPath: string, |
| data: string |
| ): Promise<{ |
| success: boolean; |
| hasConflicts?: boolean; |
| conflicts?: Array<{ |
| featureId: string; |
| title?: string; |
| existingTitle?: string; |
| hasConflict: boolean; |
| }>; |
| totalFeatures?: number; |
| conflictCount?: number; |
| error?: string; |
| }> => this.post('/api/features/check-conflicts', { projectPath, data }), |
| getOrphaned: ( |
| projectPath: string |
| ): Promise<{ |
| success: boolean; |
| orphanedFeatures?: Array<{ feature: Feature; missingBranch: string }>; |
| error?: string; |
| }> => this.post('/api/features/orphaned', { projectPath }), |
| resolveOrphaned: ( |
| projectPath: string, |
| featureId: string, |
| action: 'delete' | 'create-worktree' | 'move-to-branch', |
| targetBranch?: string | null |
| ): Promise<{ |
| success: boolean; |
| action?: string; |
| worktreePath?: string; |
| branchName?: string; |
| error?: string; |
| }> => |
| this.post('/api/features/orphaned/resolve', { projectPath, featureId, action, targetBranch }), |
| bulkResolveOrphaned: ( |
| projectPath: string, |
| featureIds: string[], |
| action: 'delete' | 'create-worktree' | 'move-to-branch', |
| targetBranch?: string | null |
| ): Promise<{ |
| success: boolean; |
| resolvedCount?: number; |
| failedCount?: number; |
| results?: Array<{ featureId: string; success: boolean; action?: string; error?: string }>; |
| error?: string; |
| }> => |
| this.post('/api/features/orphaned/bulk-resolve', { |
| projectPath, |
| featureIds, |
| action, |
| targetBranch, |
| }), |
| }; |
|
|
| |
| autoMode: AutoModeAPI = { |
| start: (projectPath: string, branchName?: string | null, maxConcurrency?: number) => |
| this.post('/api/auto-mode/start', { projectPath, branchName, maxConcurrency }), |
| stop: (projectPath: string, branchName?: string | null) => |
| this.post('/api/auto-mode/stop', { projectPath, branchName }), |
| stopFeature: (featureId: string) => this.post('/api/auto-mode/stop-feature', { featureId }), |
| status: (projectPath?: string, branchName?: string | null) => |
| this.post('/api/auto-mode/status', { projectPath, branchName }), |
| 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[], |
| useWorktrees?: boolean |
| ) => |
| this.post('/api/auto-mode/follow-up-feature', { |
| projectPath, |
| featureId, |
| prompt, |
| imagePaths, |
| useWorktrees, |
| }), |
| 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, |
| }), |
| resumeInterrupted: (projectPath: string) => |
| this.post('/api/auto-mode/resume-interrupted', { projectPath }), |
| onEvent: (callback: (event: AutoModeEvent) => void) => { |
| return this.subscribeToEvent('auto-mode:event', callback as EventCallback); |
| }, |
| }; |
|
|
| |
| enhancePrompt = { |
| enhance: ( |
| originalText: string, |
| enhancementMode: string, |
| model?: string, |
| thinkingLevel?: string, |
| projectPath?: string |
| ): Promise<EnhancePromptResult> => |
| this.post('/api/enhance-prompt', { |
| originalText, |
| enhancementMode, |
| model, |
| thinkingLevel, |
| projectPath, |
| }), |
| }; |
|
|
| |
| worktree: WorktreeAPI = { |
| mergeFeature: ( |
| projectPath: string, |
| branchName: string, |
| worktreePath: string, |
| targetBranch?: string, |
| options?: object |
| ) => |
| this.post('/api/worktree/merge', { |
| projectPath, |
| branchName, |
| worktreePath, |
| targetBranch, |
| 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, forceRefreshGitHub?: boolean) => |
| this.post('/api/worktree/list', { projectPath, includeDetails, forceRefreshGitHub }), |
| 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, files?: string[]) => |
| this.post('/api/worktree/commit', { worktreePath, message, files }), |
| generateCommitMessage: ( |
| worktreePath: string, |
| model?: string, |
| thinkingLevel?: string, |
| providerId?: string |
| ) => |
| this.post('/api/worktree/generate-commit-message', { |
| worktreePath, |
| model, |
| thinkingLevel, |
| providerId, |
| }), |
| generatePRDescription: ( |
| worktreePath: string, |
| baseBranch?: string, |
| model?: string, |
| thinkingLevel?: string, |
| providerId?: string |
| ) => |
| this.post('/api/worktree/generate-pr-description', { |
| worktreePath, |
| baseBranch, |
| model, |
| thinkingLevel, |
| providerId, |
| }), |
| push: (worktreePath: string, force?: boolean, remote?: string, autoResolve?: boolean) => |
| this.post('/api/worktree/push', { worktreePath, force, remote, autoResolve }), |
| sync: (worktreePath: string, remote?: string) => |
| this.post('/api/worktree/sync', { worktreePath, remote }), |
| setTracking: (worktreePath: string, remote: string, branch?: string) => |
| this.post('/api/worktree/set-tracking', { worktreePath, remote, branch }), |
| createPR: (worktreePath: string, options?: CreatePROptions) => |
| this.post('/api/worktree/create-pr', { worktreePath, ...options }), |
| updatePRNumber: (worktreePath: string, prNumber: number, projectPath?: string) => |
| this.post('/api/worktree/update-pr-number', { worktreePath, prNumber, projectPath }), |
| 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, |
| }), |
| stageFiles: (worktreePath: string, files: string[], operation: 'stage' | 'unstage') => |
| this.post('/api/worktree/stage-files', { worktreePath, files, operation }), |
| pull: (worktreePath: string, remote?: string, stashIfNeeded?: boolean, remoteBranch?: string) => |
| this.post('/api/worktree/pull', { worktreePath, remote, remoteBranch, stashIfNeeded }), |
| checkoutBranch: ( |
| worktreePath: string, |
| branchName: string, |
| baseBranch?: string, |
| stashChanges?: boolean, |
| includeUntracked?: boolean |
| ) => |
| this.post('/api/worktree/checkout-branch', { |
| worktreePath, |
| branchName, |
| baseBranch, |
| stashChanges, |
| includeUntracked, |
| }), |
| checkChanges: (worktreePath: string) => |
| this.post('/api/worktree/check-changes', { worktreePath }), |
| listBranches: (worktreePath: string, includeRemote?: boolean, signal?: AbortSignal) => |
| this.post('/api/worktree/list-branches', { worktreePath, includeRemote }, signal), |
| switchBranch: (worktreePath: string, branchName: string) => |
| this.post('/api/worktree/switch-branch', { worktreePath, branchName }), |
| listRemotes: (worktreePath: string) => |
| this.post('/api/worktree/list-remotes', { worktreePath }), |
| addRemote: (worktreePath: string, remoteName: string, remoteUrl: string) => |
| this.post('/api/worktree/add-remote', { worktreePath, remoteName, remoteUrl }), |
| openInEditor: (worktreePath: string, editorCommand?: string) => |
| this.post('/api/worktree/open-in-editor', { worktreePath, editorCommand }), |
| getDefaultEditor: () => this.get('/api/worktree/default-editor'), |
| getAvailableEditors: () => this.get('/api/worktree/available-editors'), |
| refreshEditors: () => this.post('/api/worktree/refresh-editors', {}), |
| getAvailableTerminals: () => this.get('/api/worktree/available-terminals'), |
| getDefaultTerminal: () => this.get('/api/worktree/default-terminal'), |
| refreshTerminals: () => this.post('/api/worktree/refresh-terminals', {}), |
| openInExternalTerminal: (worktreePath: string, terminalId?: string) => |
| this.post('/api/worktree/open-in-external-terminal', { worktreePath, terminalId }), |
| 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', {}), |
| getDevServerLogs: (worktreePath: string): Promise<DevServerLogsResponse> => |
| this.get(`/api/worktree/dev-server-logs?worktreePath=${encodeURIComponent(worktreePath)}`), |
| onDevServerLogEvent: (callback: (event: DevServerLogEvent) => void) => { |
| const unsub0 = this.subscribeToEvent('dev-server:starting', (payload) => |
| callback({ type: 'dev-server:starting', payload: payload as DevServerStartingEvent }) |
| ); |
| const unsub1 = this.subscribeToEvent('dev-server:started', (payload) => |
| callback({ type: 'dev-server:started', payload: payload as DevServerStartedEvent }) |
| ); |
| const unsub2 = this.subscribeToEvent('dev-server:output', (payload) => |
| callback({ type: 'dev-server:output', payload: payload as DevServerOutputEvent }) |
| ); |
| const unsub3 = this.subscribeToEvent('dev-server:stopped', (payload) => |
| callback({ type: 'dev-server:stopped', payload: payload as DevServerStoppedEvent }) |
| ); |
| const unsub4 = this.subscribeToEvent('dev-server:url-detected', (payload) => |
| callback({ type: 'dev-server:url-detected', payload: payload as DevServerUrlDetectedEvent }) |
| ); |
| return () => { |
| unsub0(); |
| unsub1(); |
| unsub2(); |
| unsub3(); |
| unsub4(); |
| }; |
| }, |
| getPRInfo: (worktreePath: string, branchName: string) => |
| this.post('/api/worktree/pr-info', { worktreePath, branchName }), |
| |
| getInitScript: (projectPath: string) => |
| this.get(`/api/worktree/init-script?projectPath=${encodeURIComponent(projectPath)}`), |
| setInitScript: (projectPath: string, content: string) => |
| this.put('/api/worktree/init-script', { projectPath, content }), |
| deleteInitScript: (projectPath: string) => |
| this.httpDelete('/api/worktree/init-script', { projectPath }), |
| runInitScript: (projectPath: string, worktreePath: string, branch: string) => |
| this.post('/api/worktree/run-init-script', { projectPath, worktreePath, branch }), |
| discardChanges: (worktreePath: string, files?: string[]) => |
| this.post('/api/worktree/discard-changes', { worktreePath, files }), |
| onInitScriptEvent: ( |
| callback: (event: { |
| type: 'worktree:init-started' | 'worktree:init-output' | 'worktree:init-completed'; |
| payload: unknown; |
| }) => void |
| ) => { |
| |
| const unsub1 = this.subscribeToEvent('worktree:init-started', (payload) => |
| callback({ type: 'worktree:init-started', payload }) |
| ); |
| const unsub2 = this.subscribeToEvent('worktree:init-output', (payload) => |
| callback({ type: 'worktree:init-output', payload }) |
| ); |
| const unsub3 = this.subscribeToEvent('worktree:init-completed', (payload) => |
| callback({ type: 'worktree:init-completed', payload }) |
| ); |
| return () => { |
| unsub1(); |
| unsub2(); |
| unsub3(); |
| }; |
| }, |
| |
| startTests: (worktreePath: string, options?: { projectPath?: string; testFile?: string }) => |
| this.post('/api/worktree/start-tests', { worktreePath, ...options }), |
| stopTests: (sessionId: string) => this.post('/api/worktree/stop-tests', { sessionId }), |
| getCommitLog: (worktreePath: string, limit?: number) => |
| this.post('/api/worktree/commit-log', { worktreePath, limit }), |
| stashPush: (worktreePath: string, message?: string, files?: string[]) => |
| this.post('/api/worktree/stash-push', { worktreePath, message, files }), |
| stashList: (worktreePath: string) => this.post('/api/worktree/stash-list', { worktreePath }), |
| stashApply: (worktreePath: string, stashIndex: number, pop?: boolean) => |
| this.post('/api/worktree/stash-apply', { worktreePath, stashIndex, pop }), |
| stashDrop: (worktreePath: string, stashIndex: number) => |
| this.post('/api/worktree/stash-drop', { worktreePath, stashIndex }), |
| cherryPick: (worktreePath: string, commitHashes: string[], options?: { noCommit?: boolean }) => |
| this.post('/api/worktree/cherry-pick', { worktreePath, commitHashes, options }), |
| rebase: (worktreePath: string, ontoBranch: string, remote?: string) => |
| this.post('/api/worktree/rebase', { worktreePath, ontoBranch, remote }), |
| abortOperation: (worktreePath: string) => |
| this.post('/api/worktree/abort-operation', { worktreePath }), |
| continueOperation: (worktreePath: string) => |
| this.post('/api/worktree/continue-operation', { worktreePath }), |
| getBranchCommitLog: (worktreePath: string, branchName?: string, limit?: number) => |
| this.post('/api/worktree/branch-commit-log', { worktreePath, branchName, limit }), |
| getTestLogs: (worktreePath?: string, sessionId?: string): Promise<TestLogsResponse> => { |
| const params = new URLSearchParams(); |
| if (worktreePath) params.append('worktreePath', worktreePath); |
| if (sessionId) params.append('sessionId', sessionId); |
| return this.get(`/api/worktree/test-logs?${params.toString()}`); |
| }, |
| onTestRunnerEvent: (callback: (event: TestRunnerEvent) => void) => { |
| const unsub1 = this.subscribeToEvent('test-runner:started', (payload) => |
| callback({ type: 'test-runner:started', payload: payload as TestRunnerStartedEvent }) |
| ); |
| const unsub2 = this.subscribeToEvent('test-runner:output', (payload) => |
| callback({ type: 'test-runner:output', payload: payload as TestRunnerOutputEvent }) |
| ); |
| const unsub3 = this.subscribeToEvent('test-runner:completed', (payload) => |
| callback({ type: 'test-runner:completed', payload: payload as TestRunnerCompletedEvent }) |
| ); |
| return () => { |
| unsub1(); |
| unsub2(); |
| unsub3(); |
| }; |
| }, |
| }; |
|
|
| |
| git: GitAPI = { |
| getDiffs: (projectPath: string) => this.post('/api/git/diffs', { projectPath }), |
| getFileDiff: (projectPath: string, filePath: string) => |
| this.post('/api/git/file-diff', { projectPath, filePath }), |
| stageFiles: (projectPath: string, files: string[], operation: 'stage' | 'unstage') => |
| this.post('/api/git/stage-files', { projectPath, files, operation }), |
| getDetails: (projectPath: string, filePath?: string) => |
| this.post('/api/git/details', { projectPath, filePath }), |
| getEnhancedStatus: (projectPath: string) => |
| this.post('/api/git/enhanced-status', { projectPath }), |
| }; |
|
|
| |
| 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, |
| }), |
| sync: (projectPath: string) => this.post('/api/spec-regeneration/sync', { projectPath }), |
| stop: (projectPath?: string) => this.post('/api/spec-regeneration/stop', { projectPath }), |
| status: (projectPath?: string) => |
| this.get( |
| projectPath |
| ? `/api/spec-regeneration/status?projectPath=${encodeURIComponent(projectPath)}` |
| : '/api/spec-regeneration/status' |
| ), |
| onEvent: (callback: (event: SpecRegenerationEvent) => void) => { |
| return this.subscribeToEvent('spec-regeneration:event', callback as EventCallback); |
| }, |
| }; |
|
|
| |
| 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: 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?: ModelId, |
| thinkingLevel?: ThinkingLevel, |
| reasoningEffort?: ReasoningEffort, |
| providerId?: string |
| ) => |
| this.post('/api/github/validate-issue', { |
| projectPath, |
| ...issue, |
| model, |
| thinkingLevel, |
| reasoningEffort, |
| providerId, |
| }), |
| 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), |
| getIssueComments: (projectPath: string, issueNumber: number, cursor?: string) => |
| this.post('/api/github/issue-comments', { projectPath, issueNumber, cursor }), |
| getPRReviewComments: (projectPath: string, prNumber: number) => |
| this.post('/api/github/pr-review-comments', { projectPath, prNumber }), |
| resolveReviewThread: (projectPath: string, threadId: string, resolve: boolean) => |
| this.post('/api/github/resolve-pr-comment', { projectPath, threadId, resolve }), |
| }; |
|
|
| |
| 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 = { |
| 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, |
| thinkingLevel?: string |
| ): Promise<{ success: boolean; error?: string }> => |
| this.post('/api/agent/send', { |
| sessionId, |
| message, |
| workingDirectory, |
| imagePaths, |
| model, |
| thinkingLevel, |
| }), |
|
|
| 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); |
| }, |
|
|
| |
| queueAdd: ( |
| sessionId: string, |
| message: string, |
| imagePaths?: string[], |
| model?: string, |
| thinkingLevel?: string |
| ): Promise<{ |
| success: boolean; |
| queuedPrompt?: { |
| id: string; |
| message: string; |
| imagePaths?: string[]; |
| model?: string; |
| thinkingLevel?: string; |
| addedAt: string; |
| }; |
| error?: string; |
| }> => |
| this.post('/api/agent/queue/add', { sessionId, message, imagePaths, model, thinkingLevel }), |
|
|
| queueList: ( |
| sessionId: string |
| ): Promise<{ |
| success: boolean; |
| queue?: Array<{ |
| id: string; |
| message: string; |
| imagePaths?: string[]; |
| model?: string; |
| thinkingLevel?: 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 = { |
| 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 = { |
| |
| getStatus: (): Promise<{ |
| success: boolean; |
| hasGlobalSettings: boolean; |
| hasCredentials: boolean; |
| dataDir: string; |
| needsMigration: boolean; |
| }> => this.get('/api/settings/status'), |
|
|
| |
| getGlobal: (): Promise<{ |
| success: boolean; |
| settings?: { |
| version: number; |
| theme: string; |
| sidebarOpen: boolean; |
| chatHistoryOpen: boolean; |
| maxConcurrency: number; |
| defaultSkipTests: boolean; |
| enableDependencyBlocking: boolean; |
| useWorktrees: boolean; |
| defaultPlanningMode: string; |
| defaultRequirePlanApproval: boolean; |
| muteDoneSound: boolean; |
| enhancementModel: string; |
| keyboardShortcuts: Record<string, string>; |
| projects: unknown[]; |
| trashedProjects: unknown[]; |
| projectHistory: string[]; |
| projectHistoryIndex: number; |
| lastProjectDir?: string; |
| recentFolders: string[]; |
| worktreePanelCollapsed: boolean; |
| lastSelectedSessionByProject: Record<string, string>; |
| mcpServers?: Array<{ |
| id: string; |
| name: string; |
| description?: string; |
| type?: 'stdio' | 'sse' | 'http'; |
| command?: string; |
| args?: string[]; |
| env?: Record<string, string>; |
| url?: string; |
| headers?: Record<string, string>; |
| enabled?: boolean; |
| }>; |
| eventHooks?: Array<{ |
| id: string; |
| trigger: string; |
| enabled: boolean; |
| action: Record<string, unknown>; |
| name?: string; |
| }>; |
| ntfyEndpoints?: Array<{ |
| id: string; |
| name: string; |
| serverUrl: string; |
| topic: string; |
| authType: string; |
| enabled: boolean; |
| }>; |
| }; |
| error?: string; |
| }> => this.get('/api/settings/global'), |
|
|
| updateGlobal: ( |
| updates: Record<string, unknown> |
| ): Promise<{ |
| success: boolean; |
| settings?: Record<string, unknown>; |
| error?: string; |
| }> => this.put('/api/settings/global', updates), |
|
|
| |
| 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), |
|
|
| |
| 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; |
| }; |
| worktreePanelVisible?: boolean; |
| showInitScriptIndicator?: boolean; |
| defaultDeleteBranchWithWorktree?: boolean; |
| autoDismissInitScriptIndicator?: boolean; |
| worktreeCopyFiles?: string[]; |
| pinnedWorktreesCount?: number; |
| worktreeDropdownThreshold?: number; |
| alwaysUseWorktreeDropdown?: boolean; |
| lastSelectedSessionId?: string; |
| testCommand?: string; |
| }; |
| error?: string; |
| }> => this.post('/api/settings/project', { projectPath }), |
|
|
| updateProject: ( |
| projectPath: string, |
| updates: Record<string, unknown> |
| ): Promise<{ |
| success: boolean; |
| settings?: Record<string, unknown>; |
| error?: string; |
| }> => this.put('/api/settings/project', { projectPath, updates }), |
|
|
| |
| 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 }), |
|
|
| |
| discoverAgents: ( |
| projectPath?: string, |
| sources?: Array<'user' | 'project'> |
| ): Promise<{ |
| success: boolean; |
| agents?: Array<{ |
| name: string; |
| definition: { |
| description: string; |
| prompt: string; |
| tools?: string[]; |
| model?: 'sonnet' | 'opus' | 'haiku' | 'inherit'; |
| }; |
| source: 'user' | 'project'; |
| filePath: string; |
| }>; |
| error?: string; |
| }> => this.post('/api/settings/agents/discover', { projectPath, sources }), |
| }; |
|
|
| |
| 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 = { |
| getUsage: (): Promise<ClaudeUsageResponse> => this.get('/api/claude/usage'), |
| }; |
|
|
| |
| codex = { |
| getUsage: (): Promise<CodexUsageResponse> => this.get('/api/codex/usage'), |
| getModels: ( |
| refresh = false |
| ): Promise<{ |
| success: boolean; |
| models?: Array<{ |
| id: string; |
| label: string; |
| description: string; |
| hasThinking: boolean; |
| supportsVision: boolean; |
| tier: 'premium' | 'standard' | 'basic'; |
| isDefault: boolean; |
| }>; |
| cachedAt?: number; |
| error?: string; |
| }> => { |
| const url = `/api/codex/models${refresh ? '?refresh=true' : ''}`; |
| return this.get(url); |
| }, |
| }; |
|
|
| |
| gemini = { |
| getUsage: (): Promise<GeminiUsage> => this.get('/api/gemini/usage'), |
| }; |
|
|
| |
| 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 }), |
| }; |
|
|
| |
| backlogPlan = { |
| generate: ( |
| projectPath: string, |
| prompt: string, |
| model?: string, |
| branchName?: string |
| ): Promise<{ success: boolean; error?: string }> => |
| this.post('/api/backlog-plan/generate', { projectPath, prompt, model, branchName }), |
|
|
| stop: (): Promise<{ success: boolean; error?: string }> => |
| this.post('/api/backlog-plan/stop', {}), |
|
|
| status: ( |
| projectPath: string |
| ): Promise<{ |
| success: boolean; |
| isRunning?: boolean; |
| savedPlan?: { |
| savedAt: string; |
| prompt: string; |
| model?: string; |
| result: { |
| changes: Array<{ |
| type: 'add' | 'update' | 'delete'; |
| featureId?: string; |
| feature?: Record<string, unknown>; |
| reason: string; |
| }>; |
| summary: string; |
| dependencyUpdates: Array<{ |
| featureId: string; |
| removedDependencies: string[]; |
| addedDependencies: string[]; |
| }>; |
| }; |
| } | null; |
| error?: string; |
| }> => this.get(`/api/backlog-plan/status?projectPath=${encodeURIComponent(projectPath)}`), |
|
|
| apply: ( |
| projectPath: string, |
| plan: { |
| changes: Array<{ |
| type: 'add' | 'update' | 'delete'; |
| featureId?: string; |
| feature?: Record<string, unknown>; |
| reason: string; |
| }>; |
| summary: string; |
| dependencyUpdates: Array<{ |
| featureId: string; |
| removedDependencies: string[]; |
| addedDependencies: string[]; |
| }>; |
| }, |
| branchName?: string |
| ): Promise<{ success: boolean; appliedChanges?: string[]; error?: string }> => |
| this.post('/api/backlog-plan/apply', { projectPath, plan, branchName }), |
|
|
| clear: (projectPath: string): Promise<{ success: boolean; error?: string }> => |
| this.post('/api/backlog-plan/clear', { projectPath }), |
|
|
| onEvent: (callback: (data: unknown) => void): (() => void) => { |
| return this.subscribeToEvent('backlog-plan:event', callback as EventCallback); |
| }, |
| }; |
|
|
| |
| ideation: IdeationAPI = { |
| startSession: (projectPath: string, options?: StartSessionOptions) => |
| this.post('/api/ideation/session/start', { projectPath, options }), |
|
|
| getSession: (projectPath: string, sessionId: string) => |
| this.post('/api/ideation/session/get', { projectPath, sessionId }), |
|
|
| sendMessage: ( |
| sessionId: string, |
| message: string, |
| options?: { imagePaths?: string[]; model?: string } |
| ) => this.post('/api/ideation/session/message', { sessionId, message, options }), |
|
|
| stopSession: (sessionId: string) => this.post('/api/ideation/session/stop', { sessionId }), |
|
|
| listIdeas: (projectPath: string) => this.post('/api/ideation/ideas/list', { projectPath }), |
|
|
| createIdea: (projectPath: string, idea: CreateIdeaInput) => |
| this.post('/api/ideation/ideas/create', { projectPath, idea }), |
|
|
| getIdea: (projectPath: string, ideaId: string) => |
| this.post('/api/ideation/ideas/get', { projectPath, ideaId }), |
|
|
| updateIdea: (projectPath: string, ideaId: string, updates: UpdateIdeaInput) => |
| this.post('/api/ideation/ideas/update', { projectPath, ideaId, updates }), |
|
|
| deleteIdea: (projectPath: string, ideaId: string) => |
| this.post('/api/ideation/ideas/delete', { projectPath, ideaId }), |
|
|
| analyzeProject: (projectPath: string) => this.post('/api/ideation/analyze', { projectPath }), |
|
|
| generateSuggestions: ( |
| projectPath: string, |
| promptId: string, |
| category: IdeaCategory, |
| count?: number, |
| contextSources?: IdeationContextSources |
| ) => |
| this.post('/api/ideation/suggestions/generate', { |
| projectPath, |
| promptId, |
| category, |
| count, |
| contextSources, |
| }), |
|
|
| convertToFeature: (projectPath: string, ideaId: string, options?: ConvertToFeatureOptions) => |
| this.post('/api/ideation/convert', { projectPath, ideaId, ...options }), |
|
|
| addSuggestionToBoard: ( |
| projectPath: string, |
| suggestion: AnalysisSuggestion |
| ): Promise<{ success: boolean; featureId?: string; error?: string }> => |
| this.post('/api/ideation/add-suggestion', { projectPath, suggestion }), |
|
|
| getPrompts: () => this.get('/api/ideation/prompts'), |
|
|
| onStream: (callback: (event: IdeationStreamEvent) => void): (() => void) => { |
| return this.subscribeToEvent('ideation:stream', callback as EventCallback); |
| }, |
|
|
| onAnalysisEvent: (callback: (event: IdeationAnalysisEvent) => void): (() => void) => { |
| return this.subscribeToEvent('ideation:analysis', callback as EventCallback); |
| }, |
| }; |
|
|
| |
| notifications: NotificationsAPI & { |
| onNotificationCreated: (callback: (notification: Notification) => void) => () => void; |
| } = { |
| list: (projectPath: string) => this.post('/api/notifications/list', { projectPath }), |
|
|
| getUnreadCount: (projectPath: string) => |
| this.post('/api/notifications/unread-count', { projectPath }), |
|
|
| markAsRead: (projectPath: string, notificationId?: string) => |
| this.post('/api/notifications/mark-read', { projectPath, notificationId }), |
|
|
| dismiss: (projectPath: string, notificationId?: string) => |
| this.post('/api/notifications/dismiss', { projectPath, notificationId }), |
|
|
| onNotificationCreated: (callback: (notification: Notification) => void): (() => void) => { |
| return this.subscribeToEvent('notification:created', callback as EventCallback); |
| }, |
| }; |
|
|
| |
| eventHistory: EventHistoryAPI = { |
| list: (projectPath: string, filter?: EventHistoryFilter) => |
| this.post('/api/event-history/list', { projectPath, filter }), |
|
|
| get: (projectPath: string, eventId: string) => |
| this.post('/api/event-history/get', { projectPath, eventId }), |
|
|
| delete: (projectPath: string, eventId: string) => |
| this.post('/api/event-history/delete', { projectPath, eventId }), |
|
|
| clear: (projectPath: string) => this.post('/api/event-history/clear', { projectPath }), |
|
|
| replay: (projectPath: string, eventId: string, hookIds?: string[]) => |
| this.post('/api/event-history/replay', { projectPath, eventId, hookIds }), |
| }; |
|
|
| |
| |
| |
| mcp = { |
| testServer: ( |
| serverId: string |
| ): Promise<{ |
| success: boolean; |
| tools?: Array<{ |
| name: string; |
| description?: string; |
| inputSchema?: Record<string, unknown>; |
| enabled: boolean; |
| }>; |
| error?: string; |
| connectionTime?: number; |
| serverInfo?: { |
| name?: string; |
| version?: string; |
| }; |
| }> => this.post('/api/mcp/test', { serverId }), |
|
|
| listTools: ( |
| serverId: string |
| ): Promise<{ |
| success: boolean; |
| tools?: Array<{ |
| name: string; |
| description?: string; |
| inputSchema?: Record<string, unknown>; |
| enabled: boolean; |
| }>; |
| error?: string; |
| }> => this.post('/api/mcp/tools', { serverId }), |
| }; |
|
|
| |
| pipeline = { |
| getConfig: ( |
| projectPath: string |
| ): Promise<{ |
| success: boolean; |
| config?: { |
| version: 1; |
| steps: Array<{ |
| id: string; |
| name: string; |
| order: number; |
| instructions: string; |
| colorClass: string; |
| createdAt: string; |
| updatedAt: string; |
| }>; |
| }; |
| error?: string; |
| }> => this.post('/api/pipeline/config', { projectPath }), |
|
|
| saveConfig: ( |
| projectPath: string, |
| config: { |
| version: 1; |
| steps: Array<{ |
| id: string; |
| name: string; |
| order: number; |
| instructions: string; |
| colorClass: string; |
| createdAt: string; |
| updatedAt: string; |
| }>; |
| } |
| ): Promise<{ success: boolean; error?: string }> => |
| this.post('/api/pipeline/config/save', { projectPath, config }), |
|
|
| addStep: ( |
| projectPath: string, |
| step: { |
| name: string; |
| order: number; |
| instructions: string; |
| colorClass: string; |
| } |
| ): Promise<{ |
| success: boolean; |
| step?: { |
| id: string; |
| name: string; |
| order: number; |
| instructions: string; |
| colorClass: string; |
| createdAt: string; |
| updatedAt: string; |
| }; |
| error?: string; |
| }> => this.post('/api/pipeline/steps/add', { projectPath, step }), |
|
|
| updateStep: ( |
| projectPath: string, |
| stepId: string, |
| updates: Partial<{ |
| name: string; |
| order: number; |
| instructions: string; |
| colorClass: string; |
| }> |
| ): Promise<{ |
| success: boolean; |
| step?: { |
| id: string; |
| name: string; |
| order: number; |
| instructions: string; |
| colorClass: string; |
| createdAt: string; |
| updatedAt: string; |
| }; |
| error?: string; |
| }> => this.post('/api/pipeline/steps/update', { projectPath, stepId, updates }), |
|
|
| deleteStep: ( |
| projectPath: string, |
| stepId: string |
| ): Promise<{ success: boolean; error?: string }> => |
| this.post('/api/pipeline/steps/delete', { projectPath, stepId }), |
|
|
| reorderSteps: ( |
| projectPath: string, |
| stepIds: string[] |
| ): Promise<{ success: boolean; error?: string }> => |
| this.post('/api/pipeline/steps/reorder', { projectPath, stepIds }), |
| }; |
| } |
|
|
| |
| let httpApiClientInstance: HttpApiClient | null = null; |
|
|
| export function getHttpApiClient(): HttpApiClient { |
| if (!httpApiClientInstance) { |
| httpApiClientInstance = new HttpApiClient(); |
| } |
| return httpApiClientInstance; |
| } |
|
|
| |
| |
| |
| initApiKey().catch((error) => { |
| logger.error('Failed to initialize API key:', error); |
| }); |
|
|