// ======================================== // API Client // ======================================== // Typed fetch functions for API communication with CSRF token handling import type { SessionMetadata, TaskData, IndexStatus, IndexRebuildRequest, Rule, RuleCreateInput, RulesResponse, Prompt, PromptInsight, Pattern, Suggestion, McpTemplate, McpTemplateInstallRequest, AllProjectsResponse, OtherProjectsServersResponse, CrossCliCopyRequest, CrossCliCopyResponse } from '../types/store'; import type { TeamArtifactsResponse } from '../types/team'; // Re-export types for backward compatibility export type { IndexStatus, IndexRebuildRequest, Rule, RuleCreateInput, RulesResponse, Prompt, PromptInsight, Pattern, Suggestion, McpTemplate, McpTemplateInstallRequest, AllProjectsResponse, OtherProjectsServersResponse, CrossCliCopyRequest, CrossCliCopyResponse }; /** * Raw backend session data structure matching the backend API response. * * @remarks * This interface represents the exact schema returned by the backend `/api/data` endpoint. * It is used internally during transformation to `SessionMetadata` in the frontend. * * **Field mappings to frontend SessionMetadata:** * - `project` → `title` and `description` (split on ':' separator) * - `status: 'active'` → `status: 'in_progress'` (other statuses remain unchanged) * - `location` is added based on which array (activeSessions/archivedSessions) the data comes from * * **Backend schema location:** `ccw/src/data-aggregator.ts` * **Transformation function:** {@link transformBackendSession} * **Frontend type:** {@link SessionMetadata} * * @warning If backend schema changes, update this interface AND the transformation logic in {@link transformBackendSession} */ interface BackendSessionData { session_id: string; project?: string; status: 'active' | 'completed' | 'archived' | 'planning' | 'paused'; type?: string; created_at: string; updated_at?: string; [key: string]: unknown; } /** * Dashboard statistics mapped from backend statistics response. * * @remarks * This interface represents the frontend statistics type displayed on the dashboard. * The data is extracted from the backend `/api/data` response's `statistics` field. * * **Backend response structure:** * ```json * { * "statistics": { * "totalSessions": number, * "activeSessions": number, * "archivedSessions": number, * "totalTasks": number, * "completedTasks": number, * "pendingTasks": number, * "failedTasks": number, * "todayActivity": number * } * } * ``` * * **Mapping function:** {@link fetchDashboardStats} * **Fallback:** Returns zero-initialized stats on error via {@link getEmptyDashboardStats} * * @see {@link fetchDashboardStats} for the transformation logic */ export interface DashboardStats { totalSessions: number; activeSessions: number; archivedSessions: number; totalTasks: number; completedTasks: number; pendingTasks: number; failedTasks: number; todayActivity: number; } export interface SessionsResponse { activeSessions: SessionMetadata[]; archivedSessions: SessionMetadata[]; } export interface CreateSessionInput { session_id: string; title?: string; description?: string; type?: 'workflow' | 'review' | 'lite-plan' | 'lite-fix'; } export interface UpdateSessionInput { title?: string; description?: string; status?: SessionMetadata['status']; } export interface ApiError { message: string; status: number; code?: string; } // ========== CSRF Token Handling ========== /** * In-memory CSRF token storage * The token is obtained from X-CSRF-Token response header and stored here * because the XSRF-TOKEN cookie is HttpOnly and cannot be read by JavaScript */ let csrfToken: string | null = null; /** * Get CSRF token from memory */ function getCsrfToken(): string | null { return csrfToken; } /** * Set CSRF token from response header */ function updateCsrfToken(response: Response): void { const token = response.headers.get('X-CSRF-Token'); if (token) { csrfToken = token; } } /** * Initialize CSRF token by fetching from server * Should be called once on app initialization */ export async function initializeCsrfToken(): Promise { try { const response = await fetch('/api/csrf-token', { credentials: 'same-origin', }); updateCsrfToken(response); } catch (error) { console.error('[CSRF] Failed to initialize CSRF token:', error); } } // ========== Base Fetch Wrapper ========== /** * Base fetch wrapper with CSRF token and error handling */ async function fetchApi( url: string, options: RequestInit = {} ): Promise { const headers = new Headers(options.headers); // Add CSRF token for mutating requests if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) { const token = getCsrfToken(); if (token) { headers.set('X-CSRF-Token', token); } } // Set content type for JSON requests if (options.body && typeof options.body === 'string') { headers.set('Content-Type', 'application/json'); } const response = await fetch(url, { ...options, headers, credentials: 'same-origin', }); // Update CSRF token from response header updateCsrfToken(response); if (!response.ok) { const error: ApiError = { message: response.statusText || 'Request failed', status: response.status, }; // Only try to parse JSON if the content type indicates JSON const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { try { const body = await response.json(); // Check both 'message' and 'error' fields for error message if (body.message) error.message = body.message; else if (body.error) error.message = body.error; if (body.code) error.code = body.code; } catch (parseError) { // Silently ignore JSON parse errors for non-JSON responses } } throw error; } // Handle no-content responses if (response.status === 204) { return undefined as T; } // Wrap response.json() with try-catch for better error messages try { return await response.json(); } catch (parseError) { const message = parseError instanceof Error ? parseError.message : 'Unknown error'; throw new Error(`Failed to parse JSON response: ${message}`); } } // ========== Transformation Helpers ========== /** * Infer session type from session_id pattern (matches backend logic) * Used as fallback when backend.type field is missing * * @param sessionId - Session ID to analyze * @returns Inferred session type * * @see ccw/src/core/session-scanner.ts:inferTypeFromName for backend implementation */ function inferTypeFromName(sessionId: string): SessionMetadata['type'] { const name = sessionId.toLowerCase(); if (name.includes('-review-') || name.includes('-code-review-')) { return 'review'; } if (name.includes('-tdd-') || name.includes('-test-driven-')) { return 'tdd'; } if (name.includes('-test-') || name.includes('-testing-')) { return 'test'; } if (name.includes('-docs-') || name.includes('-doc-') || name.includes('-documentation-')) { return 'docs'; } if (name.includes('-lite-plan-')) { return 'lite-plan'; } if (name.includes('-lite-fix-') || name.includes('-fix-')) { return 'lite-fix'; } // Default to workflow for standard sessions return 'workflow'; } /** * Transform backend session data to frontend SessionMetadata interface * Maps backend schema (project, status: 'active') to frontend schema (title, description, status: 'in_progress', location) * * @param backendSession - Raw session data from backend * @param location - Whether this session is from active or archived list * @returns Transformed SessionMetadata object */ function transformBackendSession( backendSession: BackendSessionData, location: 'active' | 'archived' ): SessionMetadata { // Map backend 'active' status to frontend 'in_progress' // Other statuses remain the same const statusMap: Record = { 'active': 'in_progress', 'completed': 'completed', 'archived': 'archived', 'planning': 'planning', 'paused': 'paused', }; const transformedStatus = statusMap[backendSession.status] || backendSession.status as SessionMetadata['status']; // Extract title and description from project field // Backend sends 'project' as a string, frontend expects 'title' and optional 'description' let title = backendSession.project || backendSession.session_id; let description: string | undefined; if (backendSession.project && backendSession.project.includes(':')) { const parts = backendSession.project.split(':'); title = parts[0].trim(); description = parts.slice(1).join(':').trim(); } // Preserve type field from backend, or infer from session_id pattern // Multi-level type detection: backend.type > hasReview (for review sessions) > infer from name let sessionType = (backendSession.type as SessionMetadata['type']) || inferTypeFromName(backendSession.session_id); // Transform backend review data to frontend format // Backend has: hasReview, reviewSummary, reviewDimensions (separate fields) // Frontend expects: review object with dimensions, findings count, etc. const backendData = backendSession as unknown as { hasReview?: boolean; reviewSummary?: { phase?: string; severityDistribution?: Record; criticalFiles?: string[]; status?: string; }; reviewDimensions?: Array<{ name: string; findings?: Array<{ severity?: string }>; summary?: unknown; status?: string; }>; }; let review: SessionMetadata['review'] | undefined; if (backendData.hasReview) { // If session has review data but type is not 'review', auto-fix the type if (sessionType !== 'review') { sessionType = 'review'; } // Build review object from backend data const dimensions = backendData.reviewDimensions || []; const totalFindings = dimensions.reduce( (sum, dim) => sum + (dim.findings?.length || 0), 0 ); review = { dimensions: dimensions.map(dim => ({ name: dim.name, findings: dim.findings || [] })), dimensions_count: dimensions.length, findings: totalFindings, iterations: undefined, fixes: undefined }; } return { session_id: backendSession.session_id, type: sessionType, title, description, status: transformedStatus, created_at: backendSession.created_at, updated_at: backendSession.updated_at, location, path: (backendSession as unknown as { path?: string }).path, // Preserve additional fields if they exist has_plan: (backendSession as unknown as { has_plan?: boolean }).has_plan, plan_updated_at: (backendSession as unknown as { plan_updated_at?: string }).plan_updated_at, has_review: backendData.hasReview, review, summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries, tasks: ((backendSession as unknown as { tasks?: TaskData[] }).tasks || []) .map(t => normalizeTask(t as unknown as Record)), }; } // ========== Dashboard API ========== /** * Fetch dashboard statistics for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchDashboardStats(projectPath?: string): Promise { try { const url = projectPath ? `/api/data?path=${encodeURIComponent(projectPath)}` : '/api/data'; const data = await fetchApi<{ statistics?: DashboardStats }>(url); // Validate response structure if (!data) { console.warn('[API] No data received from /api/data for dashboard stats'); return getEmptyDashboardStats(); } // Extract statistics from response, with defaults return { totalSessions: data.statistics?.totalSessions ?? 0, activeSessions: data.statistics?.activeSessions ?? 0, archivedSessions: data.statistics?.archivedSessions ?? 0, totalTasks: data.statistics?.totalTasks ?? 0, completedTasks: data.statistics?.completedTasks ?? 0, pendingTasks: data.statistics?.pendingTasks ?? 0, failedTasks: data.statistics?.failedTasks ?? 0, todayActivity: data.statistics?.todayActivity ?? 0, }; } catch (error) { console.error('[API] Failed to fetch dashboard stats:', error); return getEmptyDashboardStats(); } } /** * Get empty dashboard stats with zero values */ function getEmptyDashboardStats(): DashboardStats { return { totalSessions: 0, activeSessions: 0, archivedSessions: 0, totalTasks: 0, completedTasks: 0, pendingTasks: 0, failedTasks: 0, todayActivity: 0, }; } // ========== Sessions API ========== /** * Fetch all sessions (active and archived) for a specific workspace * Applies transformation layer to map backend data to frontend SessionMetadata interface * @param projectPath - Optional project path to filter data by workspace */ export async function fetchSessions(projectPath?: string): Promise { try { const url = projectPath ? `/api/data?path=${encodeURIComponent(projectPath)}` : '/api/data'; const data = await fetchApi<{ activeSessions?: BackendSessionData[]; archivedSessions?: BackendSessionData[]; }>(url); // Validate response structure if (!data) { console.warn('[API] No data received from /api/data for sessions'); return { activeSessions: [], archivedSessions: [] }; } // Transform active sessions with location = 'active' const activeSessions = (data.activeSessions ?? []).map((session) => { try { return transformBackendSession(session, 'active'); } catch (error) { console.error('[API] Failed to transform active session:', session, error); // Return a minimal valid session to prevent crashes return { session_id: session.session_id, title: session.project || session.session_id, status: 'in_progress' as const, created_at: session.created_at, location: 'active' as const, }; } }); // Transform archived sessions with location = 'archived' const archivedSessions = (data.archivedSessions ?? []).map((session) => { try { return transformBackendSession(session, 'archived'); } catch (error) { console.error('[API] Failed to transform archived session:', session, error); // Return a minimal valid session to prevent crashes return { session_id: session.session_id, title: session.project || session.session_id, status: session.status === 'active' ? 'in_progress' : session.status as SessionMetadata['status'], created_at: session.created_at, location: 'archived' as const, }; } }); return { activeSessions, archivedSessions }; } catch (error) { console.error('[API] Failed to fetch sessions:', error); // Return empty arrays on error to prevent crashes return { activeSessions: [], archivedSessions: [] }; } } /** * Fetch a single session by ID */ export async function fetchSession(sessionId: string): Promise { return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}`); } /** * Create a new session */ export async function createSession(input: CreateSessionInput): Promise { return fetchApi('/api/sessions', { method: 'POST', body: JSON.stringify(input), }); } /** * Update a session */ export async function updateSession( sessionId: string, input: UpdateSessionInput ): Promise { return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'PATCH', body: JSON.stringify(input), }); } /** * Archive a session */ export async function archiveSession(sessionId: string): Promise { return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}/archive`, { method: 'POST', }); } /** * Delete a session */ export async function deleteSession(sessionId: string): Promise { return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE', }); } // ========== Tasks API ========== /** * Fetch tasks for a session */ export async function fetchSessionTasks(sessionId: string): Promise { return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}/tasks`); } /** * Update a task status */ export async function updateTask( sessionId: string, taskId: string, updates: Partial ): Promise { return fetchApi( `/api/sessions/${encodeURIComponent(sessionId)}/tasks/${encodeURIComponent(taskId)}`, { method: 'PATCH', body: JSON.stringify(updates), } ); } // ========== Path Management API ========== /** * Fetch recent paths */ export async function fetchRecentPaths(): Promise { const data = await fetchApi<{ paths?: string[] }>('/api/recent-paths'); return data.paths ?? []; } /** * Remove a recent path */ export async function removeRecentPath(path: string): Promise { const data = await fetchApi<{ paths: string[] }>('/api/remove-recent-path', { method: 'POST', body: JSON.stringify({ path }), }); return data.paths; } /** * Switch workspace response */ export interface SwitchWorkspaceResponse { projectPath: string; recentPaths: string[]; activeSessions: SessionMetadata[]; archivedSessions: SessionMetadata[]; statistics: DashboardStats; } /** * Remove recent path response */ export interface RemoveRecentPathResponse { paths: string[]; } /** * Fetch data for path response */ export interface FetchDataForPathResponse { projectOverview?: ProjectOverview | null; sessions?: SessionsResponse; statistics?: DashboardStats; } /** * Switch to a different project path and load its data */ export async function loadDashboardData(path: string): Promise<{ activeSessions: SessionMetadata[]; archivedSessions: SessionMetadata[]; statistics: DashboardStats; projectPath: string; recentPaths: string[]; }> { return fetchApi(`/api/data?path=${encodeURIComponent(path)}`); } /** * Switch workspace to a different project path */ export async function switchWorkspace(path: string): Promise { return fetchApi(`/api/switch-path?path=${encodeURIComponent(path)}`); } /** * Fetch data for a specific path */ export async function fetchDataForPath(path: string): Promise { return fetchApi(`/api/data?path=${encodeURIComponent(path)}`); } // ========== Loops API ========== export interface Loop { id: string; name?: string; status: 'created' | 'running' | 'paused' | 'completed' | 'failed'; currentStep: number; totalSteps: number; createdAt: string; updatedAt?: string; startedAt?: string; completedAt?: string; prompt?: string; tool?: string; error?: string; context?: { workingDir?: string; mode?: string; }; } export interface LoopsResponse { loops: Loop[]; total: number; } /** * Fetch all loops for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchLoops(projectPath?: string): Promise { const url = projectPath ? `/api/loops?path=${encodeURIComponent(projectPath)}` : '/api/loops'; const data = await fetchApi<{ loops?: Loop[] }>(url); return { loops: data.loops ?? [], total: data.loops?.length ?? 0, }; } /** * Fetch a single loop by ID for a specific workspace * @param loopId - The loop ID to fetch * @param projectPath - Optional project path to filter data by workspace */ export async function fetchLoop(loopId: string, projectPath?: string): Promise { const url = projectPath ? `/api/loops/${encodeURIComponent(loopId)}?path=${encodeURIComponent(projectPath)}` : `/api/loops/${encodeURIComponent(loopId)}`; return fetchApi(url); } /** * Create a new loop */ export async function createLoop(input: { prompt: string; tool?: string; mode?: string; }): Promise { return fetchApi('/api/loops', { method: 'POST', body: JSON.stringify(input), }); } /** * Update a loop's status (pause, resume, stop) */ export async function updateLoopStatus( loopId: string, action: 'pause' | 'resume' | 'stop' ): Promise { return fetchApi(`/api/loops/${encodeURIComponent(loopId)}/${action}`, { method: 'POST', }); } /** * Delete a loop */ export async function deleteLoop(loopId: string): Promise { return fetchApi(`/api/loops/${encodeURIComponent(loopId)}`, { method: 'DELETE', }); } // ========== Issues API ========== export interface IssueSolution { id: string; description: string; approach?: string; status: 'pending' | 'in_progress' | 'completed' | 'rejected'; estimatedEffort?: string; } /** * Attachment entity for file uploads */ export interface Attachment { id: string; filename: string; path: string; type: string; size: number; uploaded_at: string; } export interface Issue { id: string; title: string; context?: string; status: 'open' | 'in_progress' | 'resolved' | 'closed' | 'completed'; priority: 'low' | 'medium' | 'high' | 'critical'; createdAt: string; updatedAt?: string; solutions?: IssueSolution[]; labels?: string[]; assignee?: string; attachments?: Attachment[]; } export interface QueueItem { item_id: string; issue_id: string; solution_id: string; task_id?: string; status: 'pending' | 'ready' | 'executing' | 'completed' | 'failed' | 'blocked'; execution_order: number; execution_group: string; depends_on: string[]; semantic_priority: number; files_touched?: string[]; task_count?: number; } export interface IssueQueue { id?: string; tasks?: QueueItem[]; solutions?: QueueItem[]; conflicts: string[]; execution_groups: string[]; grouped_items: Record; } export interface IssuesResponse { issues: Issue[]; } /** * Fetch all issues */ export async function fetchIssues(projectPath?: string): Promise { const url = projectPath ? `/api/issues?path=${encodeURIComponent(projectPath)}` : '/api/issues'; const data = await fetchApi<{ issues?: Issue[] }>(url); return { issues: data.issues ?? [], }; } /** * Fetch issue history */ export async function fetchIssueHistory(projectPath?: string): Promise { const url = projectPath ? `/api/issues/history?path=${encodeURIComponent(projectPath)}` : '/api/issues/history'; const data = await fetchApi<{ issues?: Issue[] }>(url); return { issues: data.issues ?? [], }; } /** * Fetch issue queue */ export async function fetchIssueQueue(projectPath?: string): Promise { const url = projectPath ? `/api/queue?path=${encodeURIComponent(projectPath)}` : '/api/queue'; return fetchApi(url); } /** * Create a new issue */ export async function createIssue(input: { title: string; context?: string; priority?: Issue['priority']; }): Promise { return fetchApi('/api/issues', { method: 'POST', body: JSON.stringify(input), }); } /** * Update an issue */ export async function updateIssue( issueId: string, input: Partial ): Promise { return fetchApi(`/api/issues/${encodeURIComponent(issueId)}`, { method: 'PATCH', body: JSON.stringify(input), }); } /** * Delete an issue */ export async function deleteIssue(issueId: string): Promise { return fetchApi(`/api/issues/${encodeURIComponent(issueId)}`, { method: 'DELETE', }); } // ========== Attachment API ========== export interface UploadAttachmentsResponse { success: boolean; issueId: string; attachments: Attachment[]; count: number; } export interface ListAttachmentsResponse { success: boolean; issueId: string; attachments: Attachment[]; count: number; } /** * Upload attachments to an issue */ export async function uploadAttachments( issueId: string, files: File[] ): Promise { const formData = new FormData(); files.forEach((file) => { formData.append('files', file); }); const response = await fetch(`/api/issues/${encodeURIComponent(issueId)}/attachments`, { method: 'POST', body: formData, credentials: 'same-origin', }); if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Upload failed' })); throw new Error(error.error || 'Failed to upload attachments'); } return response.json(); } /** * List attachments for an issue */ export async function listAttachments(issueId: string): Promise { return fetchApi(`/api/issues/${encodeURIComponent(issueId)}/attachments`); } /** * Delete an attachment */ export async function deleteAttachment(issueId: string, attachmentId: string): Promise { return fetchApi(`/api/issues/${encodeURIComponent(issueId)}/attachments/${encodeURIComponent(attachmentId)}`, { method: 'DELETE', }); } /** * Get attachment download URL */ export function getAttachmentUrl(issueId: string, filename: string): string { return `/api/issues/files/${encodeURIComponent(issueId)}/${encodeURIComponent(filename)}`; } /** * Pull issues from GitHub */ export interface GitHubPullOptions { state?: 'open' | 'closed' | 'all'; limit?: number; labels?: string; downloadImages?: boolean; } export interface GitHubPullResponse { imported: number; updated: number; skipped: number; images_downloaded: number; total: number; } export async function pullIssuesFromGitHub(options: GitHubPullOptions = {}): Promise { const params = new URLSearchParams(); if (options.state) params.set('state', options.state); if (options.limit) params.set('limit', String(options.limit)); if (options.labels) params.set('labels', options.labels); if (options.downloadImages) params.set('downloadImages', 'true'); const url = `/api/issues/pull${params.toString() ? '?' + params.toString() : ''}`; return fetchApi(url, { method: 'POST', }); } // ========== Queue History (Multi-Queue) ========== export interface QueueHistoryEntry { id: string; created_at?: string; updated_at?: string; status?: string; issue_ids?: string[]; total_tasks?: number; completed_tasks?: number; total_solutions?: number; completed_solutions?: number; [key: string]: unknown; } export interface QueueHistoryIndex { queues: QueueHistoryEntry[]; active_queue_id: string | null; active_queue_ids: string[]; } export async function fetchQueueHistory(projectPath: string): Promise { return fetchApi(`/api/queue/history?path=${encodeURIComponent(projectPath)}`); } /** * Fetch a specific queue by ID */ export async function fetchQueueById(queueId: string, projectPath: string): Promise { return fetchApi(`/api/queue/${encodeURIComponent(queueId)}?path=${encodeURIComponent(projectPath)}`); } /** * Activate a queue */ export async function activateQueue(queueId: string, projectPath: string): Promise { return fetchApi(`/api/queue/activate?path=${encodeURIComponent(projectPath)}`, { method: 'POST', body: JSON.stringify({ queueId }), }); } /** * Deactivate the current queue */ export async function deactivateQueue(projectPath: string): Promise { return fetchApi(`/api/queue/deactivate?path=${encodeURIComponent(projectPath)}`, { method: 'POST', }); } /** * Reorder items within a single execution group */ export async function reorderQueueGroup( projectPath: string, input: { groupId: string; newOrder: string[] } ): Promise<{ success: boolean; groupId: string; reordered: number }> { return fetchApi<{ success: boolean; groupId: string; reordered: number }>( `/api/queue/reorder?path=${encodeURIComponent(projectPath)}`, { method: 'POST', body: JSON.stringify(input) } ); } /** * Move an item across execution groups (and optionally insert at index) */ export async function moveQueueItem( projectPath: string, input: { itemId: string; toGroupId: string; toIndex?: number } ): Promise<{ success: boolean; itemId: string; fromGroupId: string; toGroupId: string }> { return fetchApi<{ success: boolean; itemId: string; fromGroupId: string; toGroupId: string }>( `/api/queue/move?path=${encodeURIComponent(projectPath)}`, { method: 'POST', body: JSON.stringify(input) } ); } /** * Delete a queue */ export async function deleteQueue(queueId: string, projectPath: string): Promise { return fetchApi(`/api/queue/${encodeURIComponent(queueId)}?path=${encodeURIComponent(projectPath)}`, { method: 'DELETE', }); } /** * Merge queues */ export async function mergeQueues(sourceId: string, targetId: string, projectPath: string): Promise { return fetchApi(`/api/queue/merge?path=${encodeURIComponent(projectPath)}`, { method: 'POST', body: JSON.stringify({ sourceQueueId: sourceId, targetQueueId: targetId }), }); } /** * Split queue - split items from source queue into a new queue */ export async function splitQueue(sourceQueueId: string, itemIds: string[], projectPath: string): Promise { return fetchApi(`/api/queue/split?path=${encodeURIComponent(projectPath)}`, { method: 'POST', body: JSON.stringify({ sourceQueueId, itemIds }), }); } // ========== Discovery API ========== export interface DiscoverySession { id: string; name: string; status: 'running' | 'completed' | 'failed'; progress: number; // 0-100 findings_count: number; created_at: string; completed_at?: string; } export interface Finding { id: string; sessionId: string; severity: 'critical' | 'high' | 'medium' | 'low'; type: string; title: string; description: string; file?: string; line?: number; code_snippet?: string; created_at: string; issue_id?: string; // Associated issue ID if exported exported?: boolean; // Whether this finding has been exported as an issue } export async function fetchDiscoveries(projectPath?: string): Promise { const url = projectPath ? `/api/discoveries?path=${encodeURIComponent(projectPath)}` : '/api/discoveries'; const data = await fetchApi<{ discoveries?: any[]; sessions?: DiscoverySession[] }>(url); // Backend returns 'discoveries' with different schema, transform to frontend format const rawDiscoveries = data.discoveries ?? data.sessions ?? []; // Map backend schema to frontend DiscoverySession interface return rawDiscoveries.map((d: any) => { // Map phase to status let status: 'running' | 'completed' | 'failed' = 'running'; if (d.phase === 'complete' || d.phase === 'completed') { status = 'completed'; } else if (d.phase === 'failed') { status = 'failed'; } // Extract progress percentage from nested progress object const progress = d.progress?.perspective_analysis?.percent_complete ?? 0; return { id: d.discovery_id || d.id, name: d.target_pattern || d.discovery_id || d.name || 'Discovery', status, progress, findings_count: d.total_findings ?? d.findings_count ?? 0, created_at: d.created_at, completed_at: d.completed_at }; }); } export async function fetchDiscoveryDetail( sessionId: string, projectPath?: string ): Promise { const url = projectPath ? `/api/discoveries/${encodeURIComponent(sessionId)}?path=${encodeURIComponent(projectPath)}` : `/api/discoveries/${encodeURIComponent(sessionId)}`; return fetchApi(url); } export async function fetchDiscoveryFindings( sessionId: string, projectPath?: string ): Promise { const url = projectPath ? `/api/discoveries/${encodeURIComponent(sessionId)}/findings?path=${encodeURIComponent(projectPath)}` : `/api/discoveries/${encodeURIComponent(sessionId)}/findings`; const data = await fetchApi<{ findings?: Finding[] }>(url); return data.findings ?? []; } /** * Export findings as issues * @param sessionId - Discovery session ID * @param findingIds - Array of finding IDs to export * @param exportAll - Export all findings if true * @param projectPath - Optional project path */ export async function exportDiscoveryFindingsAsIssues( sessionId: string, { findingIds, exportAll }: { findingIds?: string[]; exportAll?: boolean }, projectPath?: string ): Promise<{ success: boolean; message?: string; exported?: number }> { const url = projectPath ? `/api/discoveries/${encodeURIComponent(sessionId)}/export?path=${encodeURIComponent(projectPath)}` : `/api/discoveries/${encodeURIComponent(sessionId)}/export`; return fetchApi<{ success: boolean; message?: string; exported?: number }>(url, { method: 'POST', body: JSON.stringify({ finding_ids: findingIds, export_all: exportAll }), }); } // ========== Skills API ========== export interface Skill { name: string; description: string; enabled: boolean; triggers: string[]; category?: string; source?: 'builtin' | 'custom' | 'community'; version?: string; author?: string; location?: 'project' | 'user'; folderName?: string; path?: string; allowedTools?: string[]; supportingFiles?: string[]; } export interface SkillsResponse { skills: Skill[]; } /** * Fetch all skills for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchSkills(projectPath?: string, cliType: 'claude' | 'codex' = 'claude'): Promise { // Response type from backend when includeDisabled=true interface ExtendedSkillsResponse { skills?: Skill[]; projectSkills?: Skill[]; userSkills?: Skill[]; disabledProjectSkills?: Skill[]; disabledUserSkills?: Skill[]; } // Helper to add location and enabled status to skills const addEnabledMetadata = (skills: Skill[], location: 'project' | 'user'): Skill[] => skills.map(skill => ({ ...skill, location, enabled: true })); const addDisabledMetadata = (skills: Skill[], location: 'project' | 'user'): Skill[] => skills.map(skill => ({ ...skill, location, enabled: false })); const buildSkillsList = (data: ExtendedSkillsResponse): Skill[] => { const projectSkillsEnabled = addEnabledMetadata(data.projectSkills ?? [], 'project'); const userSkillsEnabled = addEnabledMetadata(data.userSkills ?? [], 'user'); const projectSkillsDisabled = addDisabledMetadata(data.disabledProjectSkills ?? [], 'project'); const userSkillsDisabled = addDisabledMetadata(data.disabledUserSkills ?? [], 'user'); return [...projectSkillsEnabled, ...userSkillsEnabled, ...projectSkillsDisabled, ...userSkillsDisabled]; }; const cliTypeParam = cliType === 'codex' ? '&cliType=codex' : ''; // Try with project path first, fall back to global on 403/404 if (projectPath) { try { const url = `/api/skills?path=${encodeURIComponent(projectPath)}&includeDisabled=true${cliTypeParam}`; const data = await fetchApi(url); return { skills: buildSkillsList(data) }; } catch (error: unknown) { const apiError = error as ApiError; if (apiError.status === 403 || apiError.status === 404) { // Fall back to global skills list console.warn('[fetchSkills] 403/404 for project path, falling back to global skills'); } else { throw error; } } } // Fallback: fetch global skills const data = await fetchApi(`/api/skills?includeDisabled=true${cliTypeParam}`); return { skills: buildSkillsList(data) }; } /** * Enable a skill */ export async function enableSkill( skillName: string, location: 'project' | 'user', projectPath?: string, cliType: 'claude' | 'codex' = 'claude' ): Promise { return fetchApi(`/api/skills/${encodeURIComponent(skillName)}/enable`, { method: 'POST', body: JSON.stringify({ location, projectPath, cliType }), }); } /** * Disable a skill */ export async function disableSkill( skillName: string, location: 'project' | 'user', projectPath?: string, cliType: 'claude' | 'codex' = 'claude' ): Promise { return fetchApi(`/api/skills/${encodeURIComponent(skillName)}/disable`, { method: 'POST', body: JSON.stringify({ location, projectPath, cliType }), }); } /** * Fetch detailed information about a specific skill * @param skillName - Name of the skill to fetch * @param location - Location of the skill (project or user) * @param projectPath - Optional project path */ export async function fetchSkillDetail( skillName: string, location: 'project' | 'user', projectPath?: string, cliType: 'claude' | 'codex' = 'claude' ): Promise<{ skill: Skill }> { const cliTypeParam = cliType === 'codex' ? '&cliType=codex' : ''; const url = projectPath ? `/api/skills/${encodeURIComponent(skillName)}?location=${location}&path=${encodeURIComponent(projectPath)}${cliTypeParam}` : `/api/skills/${encodeURIComponent(skillName)}?location=${location}${cliTypeParam}`; return fetchApi<{ skill: Skill }>(url); } /** * Delete a skill * @param skillName - Name of the skill to delete * @param location - Location of the skill (project or user) * @param projectPath - Optional project path * @param cliType - CLI type (claude or codex) */ export async function deleteSkill( skillName: string, location: 'project' | 'user', projectPath?: string, cliType: 'claude' | 'codex' = 'claude' ): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(`/api/skills/${encodeURIComponent(skillName)}`, { method: 'DELETE', body: JSON.stringify({ location, projectPath, cliType }), }); } /** * Validate a skill folder for import */ export async function validateSkillImport(sourcePath: string): Promise<{ valid: boolean; errors?: string[]; skillInfo?: { name: string; description: string; version?: string; supportingFiles?: string[] }; }> { return fetchApi('/api/skills/validate-import', { method: 'POST', body: JSON.stringify({ sourcePath }), }); } /** * Create/import a skill */ export async function createSkill(params: { mode: 'import' | 'cli-generate'; location: 'project' | 'user'; sourcePath?: string; skillName?: string; description?: string; generationType?: 'description' | 'template'; projectPath?: string; cliType?: 'claude' | 'codex'; }): Promise<{ skillName: string; path: string }> { return fetchApi('/api/skills/create', { method: 'POST', body: JSON.stringify(params), }); } /** * Read a skill file content */ export async function readSkillFile(params: { skillName: string; fileName: string; location: 'project' | 'user'; projectPath?: string; cliType?: 'claude' | 'codex'; }): Promise<{ content: string; fileName: string; path: string }> { const { skillName, fileName, location, projectPath, cliType = 'claude' } = params; const encodedSkillName = encodeURIComponent(skillName); const url = `/api/skills/${encodedSkillName}/file?filename=${encodeURIComponent(fileName)}&location=${location}&cliType=${cliType}${projectPath ? `&path=${encodeURIComponent(projectPath)}` : ''}`; return fetchApi(url); } /** * Write a skill file content */ export async function writeSkillFile(params: { skillName: string; fileName: string; content: string; location: 'project' | 'user'; projectPath?: string; cliType?: 'claude' | 'codex'; }): Promise<{ success: boolean; fileName: string; path: string }> { const { skillName, fileName, content, location, projectPath, cliType = 'claude' } = params; const encodedSkillName = encodeURIComponent(skillName); const url = `/api/skills/${encodedSkillName}/file`; return fetchApi(url, { method: 'POST', body: JSON.stringify({ content, fileName, location, projectPath, cliType }), }); } // ========== Commands API ========== export interface Command { name: string; description: string; usage?: string; examples?: string[]; category?: string; aliases?: string[]; source?: 'builtin' | 'custom'; group?: string; enabled?: boolean; location?: 'project' | 'user'; path?: string; relativePath?: string; argumentHint?: string; allowedTools?: string[]; } export interface CommandsResponse { commands: Command[]; groups?: string[]; projectGroupsConfig?: Record; userGroupsConfig?: Record; } /** * Fetch all commands for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchCommands(projectPath?: string): Promise { // Try with project path first, fall back to global on errors if (projectPath) { try { const url = `/api/commands?path=${encodeURIComponent(projectPath)}`; const data = await fetchApi<{ commands?: Command[]; projectCommands?: Command[]; userCommands?: Command[]; groups?: string[]; projectGroupsConfig?: Record; userGroupsConfig?: Record; }>(url); const allCommands = [...(data.projectCommands ?? []), ...(data.userCommands ?? [])]; return { commands: data.commands ?? allCommands, groups: data.groups, projectGroupsConfig: data.projectGroupsConfig, userGroupsConfig: data.userGroupsConfig, }; } catch (error: unknown) { const apiError = error as ApiError; if (apiError.status === 403 || apiError.status === 404 || apiError.status === 400) { // Fall back to global commands list on path validation errors console.warn('[fetchCommands] Path validation failed, falling back to global commands'); } else { throw error; } } } // Fallback: fetch global commands try { const data = await fetchApi<{ commands?: Command[]; projectCommands?: Command[]; userCommands?: Command[]; groups?: string[]; projectGroupsConfig?: Record; userGroupsConfig?: Record; }>('/api/commands'); const allCommands = [...(data.projectCommands ?? []), ...(data.userCommands ?? [])]; return { commands: data.commands ?? allCommands, groups: data.groups, projectGroupsConfig: data.projectGroupsConfig, userGroupsConfig: data.userGroupsConfig, }; } catch (error) { // Let errors propagate to React Query for proper error handling console.error('[fetchCommands] Failed to fetch commands:', error); throw error; } } /** * Toggle command enabled status */ export async function toggleCommand( commandName: string, enabled: boolean, location: 'project' | 'user', projectPath?: string ): Promise<{ success: boolean; message: string }> { return fetchApi<{ success: boolean; message: string }>(`/api/commands/${encodeURIComponent(commandName)}/toggle`, { method: 'POST', body: JSON.stringify({ enabled, location, projectPath }), }); } /** * Toggle all commands in a group */ export async function toggleCommandGroup( groupName: string, enable: boolean, location: 'project' | 'user', projectPath?: string ): Promise<{ success: boolean; results: any[]; message: string }> { return fetchApi<{ success: boolean; results: any[]; message: string }>(`/api/commands/group/${encodeURIComponent(groupName)}/toggle`, { method: 'POST', body: JSON.stringify({ enable, location, projectPath }), }); } /** * Get commands groups configuration */ export async function getCommandsGroupsConfig( location: 'project' | 'user', projectPath?: string ): Promise<{ groups: Record; assignments: Record }> { const params = new URLSearchParams({ location }); if (projectPath) params.set('path', projectPath); return fetchApi<{ groups: Record; assignments: Record }>(`/api/commands/groups/config?${params}`); } // ========== Memory API ========== export interface CoreMemory { id: string; content: string; createdAt: string; updatedAt?: string; source?: string; tags?: string[]; size?: number; metadata?: string | Record; archived?: boolean; } export interface MemoryResponse { memories: CoreMemory[]; totalSize: number; claudeMdCount: number; } /** * Fetch all memories for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchMemories(projectPath?: string): Promise { // Try with project path first, fall back to global on 403/404 if (projectPath) { try { const url = `/api/core-memory/memories?path=${encodeURIComponent(projectPath)}`; const data = await fetchApi<{ memories?: CoreMemory[]; totalSize?: number; claudeMdCount?: number; }>(url); return { memories: data.memories ?? [], totalSize: data.totalSize ?? 0, claudeMdCount: data.claudeMdCount ?? 0, }; } catch (error: unknown) { const apiError = error as ApiError; if (apiError.status === 403 || apiError.status === 404) { // Fall back to global memories list console.warn('[fetchMemories] 403/404 for project path, falling back to global memories'); } else { throw error; } } } // Fallback: fetch global memories const data = await fetchApi<{ memories?: CoreMemory[]; totalSize?: number; claudeMdCount?: number; }>('/api/core-memory/memories'); return { memories: data.memories ?? [], totalSize: data.totalSize ?? 0, claudeMdCount: data.claudeMdCount ?? 0, }; } /** * Create a new memory entry for a specific workspace * @param input - Memory input data * @param projectPath - Optional project path to filter data by workspace */ export async function createMemory(input: { content: string; tags?: string[]; metadata?: Record; }, projectPath?: string): Promise { const url = '/api/core-memory/memories'; return fetchApi<{ success: boolean; memory: CoreMemory }>(url, { method: 'POST', body: JSON.stringify({ ...input, path: projectPath, }), }).then(data => data.memory); } /** * Update a memory entry for a specific workspace * @param memoryId - Memory ID to update * @param input - Partial memory data * @param projectPath - Optional project path to filter data by workspace */ export async function updateMemory( memoryId: string, input: Partial, projectPath?: string ): Promise { const url = '/api/core-memory/memories'; return fetchApi<{ success: boolean; memory: CoreMemory }>(url, { method: 'POST', body: JSON.stringify({ id: memoryId, ...input, path: projectPath, }), }).then(data => data.memory); } /** * Delete a memory entry for a specific workspace * @param memoryId - Memory ID to delete * @param projectPath - Optional project path to filter data by workspace */ export async function deleteMemory(memoryId: string, projectPath?: string): Promise { const url = projectPath ? `/api/core-memory/memories/${encodeURIComponent(memoryId)}?path=${encodeURIComponent(projectPath)}` : `/api/core-memory/memories/${encodeURIComponent(memoryId)}`; return fetchApi(url, { method: 'DELETE', }); } /** * Archive a memory entry for a specific workspace * @param memoryId - Memory ID to archive * @param projectPath - Optional project path to filter data by workspace */ export async function archiveMemory(memoryId: string, projectPath?: string): Promise { const url = projectPath ? `/api/core-memory/memories/${encodeURIComponent(memoryId)}/archive?path=${encodeURIComponent(projectPath)}` : `/api/core-memory/memories/${encodeURIComponent(memoryId)}/archive`; return fetchApi(url, { method: 'POST', }); } /** * Unarchive a memory entry for a specific workspace * @param memoryId - Memory ID to unarchive * @param projectPath - Optional project path to filter data by workspace */ export async function unarchiveMemory(memoryId: string, projectPath?: string): Promise { const url = projectPath ? `/api/core-memory/memories/${encodeURIComponent(memoryId)}/unarchive?path=${encodeURIComponent(projectPath)}` : `/api/core-memory/memories/${encodeURIComponent(memoryId)}/unarchive`; return fetchApi(url, { method: 'POST', }); } // ========== Project Overview API ========== export interface TechnologyStack { languages: Array<{ name: string; file_count: number; primary?: boolean }>; frameworks: string[]; build_tools: string[]; test_frameworks?: string[]; } export interface Architecture { style: string; layers: string[]; patterns: string[]; } export interface KeyComponent { name: string; description?: string; importance: 'high' | 'medium' | 'low'; responsibility?: string[]; path?: string; } export interface DevelopmentIndexEntry { title: string; description?: string; sessionId?: string; sub_feature?: string; status?: string; tags?: string[]; archivedAt?: string; date?: string; implemented_at?: string; } export interface GuidelineEntry { rule: string; scope: string; enforced_by?: string; } export interface LearningEntry { insight: string; category?: string; session_id?: string; context?: string; date: string; } export interface ProjectGuidelines { conventions?: Record; constraints?: Record; quality_rules?: GuidelineEntry[]; learnings?: LearningEntry[]; _metadata?: { created_at?: string; updated_at?: string; version?: string; }; } export interface ProjectOverviewMetadata { analysis_mode?: string; [key: string]: unknown; } export interface ProjectOverview { projectName: string; description?: string; initializedAt: string; technologyStack: TechnologyStack; architecture: Architecture; keyComponents: KeyComponent[]; developmentIndex?: { feature?: DevelopmentIndexEntry[]; enhancement?: DevelopmentIndexEntry[]; bugfix?: DevelopmentIndexEntry[]; refactor?: DevelopmentIndexEntry[]; docs?: DevelopmentIndexEntry[]; [key: string]: DevelopmentIndexEntry[] | undefined; }; guidelines?: ProjectGuidelines; metadata?: ProjectOverviewMetadata; } /** * Fetch project overview for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchProjectOverview(projectPath?: string): Promise { const url = projectPath ? `/api/ccw?path=${encodeURIComponent(projectPath)}` : '/api/ccw'; const data = await fetchApi<{ projectOverview?: ProjectOverview }>(url); return data.projectOverview ?? null; } /** * Update project guidelines for a specific workspace */ export async function updateProjectGuidelines( guidelines: ProjectGuidelines, projectPath?: string ): Promise<{ success: boolean; guidelines?: ProjectGuidelines; error?: string }> { const url = projectPath ? `/api/ccw/guidelines?path=${encodeURIComponent(projectPath)}` : '/api/ccw/guidelines'; return fetchApi(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(guidelines), }); } // ========== Session Detail API ========== export interface SessionDetailContext { requirements?: string[]; focus_paths?: string[]; artifacts?: string[]; shared_context?: { tech_stack?: string[]; conventions?: string[]; }; // Extended context fields for context-package.json context?: { metadata?: { task_description?: string; session_id?: string; complexity?: string; keywords?: string[]; }; project_context?: { tech_stack?: { languages?: Array<{ name: string; file_count?: number }>; frameworks?: string[]; libraries?: string[]; }; architecture_patterns?: string[]; }; assets?: { documentation?: Array<{ path: string; relevance_score?: number; scope?: string; contains?: string[] }>; source_code?: Array<{ path: string; relevance_score?: number; scope?: string; contains?: string[] }>; tests?: Array<{ path: string; relevance_score?: number; scope?: string; contains?: string[] }>; }; dependencies?: { internal?: Array<{ from: string; type: string; to: string }>; external?: Array<{ package: string; version?: string; usage?: string }>; }; test_context?: { frameworks?: { backend?: { name?: string; plugins?: string[] }; frontend?: { name?: string }; }; existing_tests?: string[]; coverage_config?: Record; test_markers?: string[]; }; conflict_detection?: { risk_level?: 'low' | 'medium' | 'high' | 'critical'; mitigation_strategy?: string; risk_factors?: { test_gaps?: string[]; existing_implementations?: string[]; }; affected_modules?: string[]; }; }; explorations?: { manifest: { task_description: string; complexity?: string; exploration_count: number; }; data: Record; }; } export interface SessionDetailResponse { session: SessionMetadata; context?: SessionDetailContext; summary?: string; summaries?: Array<{ name: string; content: string }>; implPlan?: unknown; conflicts?: unknown[]; review?: unknown; } /** * Fetch session detail for a specific workspace * First fetches session list to get the session path, then fetches detail data * @param sessionId - Session ID to fetch details for * @param projectPath - Optional project path to filter data by workspace */ export async function fetchSessionDetail(sessionId: string, projectPath?: string): Promise { // Step 1: Fetch all sessions to get the session path const sessionsData = await fetchSessions(projectPath); const allSessions = [...sessionsData.activeSessions, ...sessionsData.archivedSessions]; const session = allSessions.find(s => s.session_id === sessionId); if (!session) { throw new Error(`Session not found: ${sessionId}`); } // Step 2: Use the session path to fetch detail data from the correct endpoint // Backend expects the actual session directory path, not the project path const sessionPath = (session as any).path || session.session_id; const detailData = await fetchApi(`/api/session-detail?path=${encodeURIComponent(sessionPath)}&type=all`); // Step 3: Transform the response to match SessionDetailResponse interface // Also check for summaries array and extract first one if summary is empty let finalSummary = detailData.summary; if (!finalSummary && detailData.summaries && detailData.summaries.length > 0) { finalSummary = detailData.summaries[0].content || detailData.summaries[0].name || ''; } // Step 4: Transform context to match SessionDetailContext interface // Backend returns raw context-package.json content, frontend expects it nested under 'context' field const transformedContext = detailData.context ? { context: detailData.context } : undefined; // Step 5: Merge tasks from detailData into session object // Backend returns tasks at root level, frontend expects them on session object const sessionWithTasks = { ...session, tasks: detailData.tasks || session.tasks || [], }; return { session: sessionWithTasks, context: transformedContext, summary: finalSummary, summaries: detailData.summaries, implPlan: detailData.implPlan, conflicts: detailData.conflictResolution, // Backend returns 'conflictResolution', not 'conflicts' review: detailData.review, }; } // ========== History / CLI Execution API ========== export interface CliExecution { id: string; tool: 'gemini' | 'qwen' | 'codex' | string; mode?: string; status: 'success' | 'error' | 'timeout'; prompt_preview: string; timestamp: string; duration_ms: number; sourceDir?: string; turn_count?: number; hasNativeSession?: boolean; nativeSessionId?: string; nativeSessionPath?: string; } export interface HistoryResponse { executions: CliExecution[]; } /** * Fetch CLI execution history with native session info * @param projectPath - Optional project path to filter data by workspace */ export async function fetchHistory(projectPath?: string): Promise { const url = projectPath ? `/api/cli/history-native?path=${encodeURIComponent(projectPath)}` : '/api/cli/history-native'; const data = await fetchApi<{ executions?: CliExecution[] }>(url); return { executions: data.executions ?? [], }; } /** * Delete a CLI execution record */ export async function deleteExecution(executionId: string): Promise { await fetchApi(`/api/cli/history/${encodeURIComponent(executionId)}`, { method: 'DELETE', }); } /** * Delete CLI executions by tool */ export async function deleteExecutionsByTool(tool: string): Promise { await fetchApi(`/api/cli/history/tool/${encodeURIComponent(tool)}`, { method: 'DELETE', }); } /** * Delete all CLI execution history */ export async function deleteAllHistory(): Promise { await fetchApi('/api/cli/history', { method: 'DELETE', }); } // ========== Task Status Update API ========== /** * Bulk update task status for multiple tasks * @param sessionPath - Path to session directory * @param taskIds - Array of task IDs to update * @param newStatus - New status to set */ export async function bulkUpdateTaskStatus( sessionPath: string, taskIds: string[], newStatus: TaskStatus ): Promise<{ success: boolean; updated: number; error?: string }> { return fetchApi('/api/bulk-update-task-status', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionPath, taskIds, newStatus }), }); } /** * Update single task status * @param sessionPath - Path to session directory * @param taskId - Task ID to update * @param newStatus - New status to set */ export async function updateTaskStatus( sessionPath: string, taskId: string, newStatus: TaskStatus ): Promise<{ success: boolean; error?: string }> { return fetchApi('/api/update-task-status', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionPath, taskId, newStatus }), }); } // Task status type (matches TaskData.status) export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'blocked' | 'skipped'; /** * Fetch CLI execution detail (conversation records) */ export async function fetchExecutionDetail( executionId: string, sourceDir?: string ): Promise { const params = new URLSearchParams({ id: executionId }); if (sourceDir) params.set('path', sourceDir); const data = await fetchApi( `/api/cli/execution?${params.toString()}` ); return data; } // ========== CLI Execution Types ========== /** * Conversation record for a CLI execution * Contains the full conversation history between user and CLI tool */ export interface ConversationRecord { id: string; tool: string; mode?: string; turns: ConversationTurn[]; turn_count: number; created_at: string; updated_at?: string; } /** * Single turn in a CLI conversation */ export interface ConversationTurn { turn: number; prompt: string; output: { stdout: string; stderr?: string; truncated?: boolean; cached?: boolean; stdout_full?: string; stderr_full?: string; parsed_output?: string; final_output?: string; structured?: unknown[]; }; timestamp: string; duration_ms: number; status?: 'success' | 'error' | 'timeout'; exit_code?: number; } // ========== Native Session Types ========== export interface NativeTokenInfo { input?: number; output?: number; cached?: number; total?: number; } export interface NativeToolCall { name: string; arguments?: string; output?: string; } export interface NativeSessionTurn { turnNumber: number; timestamp: string; role: 'user' | 'assistant'; content: string; thoughts?: string[]; toolCalls?: NativeToolCall[]; tokens?: NativeTokenInfo; } export interface NativeSession { sessionId: string; tool: string; model?: string; projectHash?: string; workingDir?: string; startTime: string; lastUpdated: string; turns: NativeSessionTurn[]; totalTokens?: NativeTokenInfo; } /** * Options for fetching native session */ export interface FetchNativeSessionOptions { executionId?: string; projectPath?: string; /** Direct file path to session file (bypasses ccw execution ID lookup) */ filePath?: string; /** Tool type for file path query: claude | opencode | codex | qwen | gemini | auto */ tool?: 'claude' | 'opencode' | 'codex' | 'qwen' | 'gemini' | 'auto'; /** Output format: json (default) | text | pairs */ format?: 'json' | 'text' | 'pairs'; /** Include thoughts in text format */ thoughts?: boolean; /** Include tool calls in text format */ tools?: boolean; /** Include token counts in text format */ tokens?: boolean; } /** * Fetch native CLI session content by execution ID or file path * @param executionId - CCW execution ID (backward compatible) * @param projectPath - Optional project path * @deprecated Use fetchNativeSessionWithOptions for new features */ export async function fetchNativeSession( executionId: string, projectPath?: string ): Promise { const params = new URLSearchParams({ id: executionId }); if (projectPath) params.set('path', projectPath); return fetchApi( `/api/cli/native-session?${params.toString()}` ); } /** * Fetch native CLI session content with full options * Supports both execution ID lookup and direct file path query */ export async function fetchNativeSessionWithOptions( options: FetchNativeSessionOptions ): Promise> { const params = new URLSearchParams(); // Priority: filePath > executionId if (options.filePath) { params.set('filePath', options.filePath); if (options.tool) params.set('tool', options.tool); } else if (options.executionId) { params.set('id', options.executionId); } else { throw new Error('Either executionId or filePath is required'); } if (options.projectPath) params.set('path', options.projectPath); if (options.format) params.set('format', options.format); if (options.thoughts) params.set('thoughts', 'true'); if (options.tools) params.set('tools', 'true'); if (options.tokens) params.set('tokens', 'true'); const url = `/api/cli/native-session?${params.toString()}`; // Text format returns string, others return JSON if (options.format === 'text') { const response = await fetch(url, { credentials: 'same-origin' }); if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Request failed' })); throw new Error(error.error || response.statusText); } return response.text(); } return fetchApi>(url); } // ========== Native Sessions List API ========== /** * Native session metadata for list endpoint */ export interface NativeSessionListItem { id: string; tool: string; path: string; title?: string; startTime: string; updatedAt: string; projectHash?: string; } /** * Native sessions list response */ export interface NativeSessionsListResponse { sessions: NativeSessionListItem[]; count: number; } /** * Fetch list of native CLI sessions * @param tool - Filter by tool type (optional) * @param project - Filter by project path (optional) */ export async function fetchNativeSessions( tool?: 'gemini' | 'qwen' | 'codex' | 'claude' | 'opencode', project?: string ): Promise { const params = new URLSearchParams(); if (tool) params.set('tool', tool); if (project) params.set('project', project); const query = params.toString(); return fetchApi( `/api/cli/native-sessions${query ? `?${query}` : ''}` ); } // ========== CLI Tools Config API ========== export interface CliToolsConfigResponse { version: string; tools: Record; } /** * Fetch CLI tools configuration */ export async function fetchCliToolsConfig(): Promise { return fetchApi('/api/cli/tools-config'); } /** * Update CLI tools configuration */ export async function updateCliToolsConfig( config: Partial ): Promise { return fetchApi('/api/cli/tools-config', { method: 'PUT', body: JSON.stringify(config), }); } // ========== Lite Tasks API ========== export interface ImplementationStep { step?: number | string; phase?: string; title?: string; action?: string; description?: string; modification_points?: string[] | Array<{ file: string; target: string; change: string }>; logic_flow?: string[]; depends_on?: number[] | string[]; output?: string; output_to?: string; commands?: string[]; steps?: string[]; test_patterns?: string; status?: 'pending' | 'in_progress' | 'completed' | 'blocked' | 'skipped'; [key: string]: unknown; } export interface PreAnalysisStep { step?: string; action?: string; output_to?: string; commands?: string[]; } export interface FlowControl { pre_analysis?: PreAnalysisStep[]; implementation_approach?: (ImplementationStep | string)[]; target_files?: Array<{ path: string; name?: string }>; [key: string]: unknown; } export interface LiteTask { id: string; task_id?: string; title?: string; description?: string; status: 'pending' | 'in_progress' | 'completed' | 'blocked' | 'failed'; priority?: string; flow_control?: FlowControl; meta?: { type?: string; scope?: string; }; context?: { focus_paths?: string[]; acceptance?: string[]; depends_on?: string[]; }; created_at?: string; updated_at?: string; } // ========== Normalized Task (Unified Flat Format) ========== /** * Normalized task type that unifies both old 6-field nested format * and new unified flat format into a single interface. * * Old format paths → New flat paths: * - context.acceptance[] → convergence.criteria[] * - context.focus_paths[] → focus_paths[] * - context.depends_on[] → depends_on[] * - context.requirements[] → description * - flow_control.pre_analysis[] → pre_analysis[] * - flow_control.implementation_approach[] → implementation[] * - flow_control.target_files[] → files[] */ export interface NormalizedTask extends TaskData { // Promoted from context focus_paths?: string[]; convergence?: { criteria?: string[]; verification?: string; definition_of_done?: string; }; // Promoted from flow_control pre_analysis?: PreAnalysisStep[]; implementation?: (ImplementationStep | string)[]; files?: Array<{ path: string; name?: string }>; // Promoted from meta type?: string; scope?: string; action?: string; // Original nested objects (preserved for long-term compat) flow_control?: FlowControl; context?: { focus_paths?: string[]; acceptance?: string[]; depends_on?: string[]; requirements?: string[]; }; meta?: { type?: string; scope?: string; [key: string]: unknown; }; // Raw data reference for JSON viewer / debugging _raw?: unknown; } /** * Normalize files field: handles both old string[] format and new {path}[] format. */ function normalizeFilesField(files: unknown): Array<{ path: string; name?: string }> | undefined { if (!Array.isArray(files) || files.length === 0) return undefined; return files.map((f: unknown) => typeof f === 'string' ? { path: f } : f) as Array<{ path: string; name?: string }>; } /** * Normalize a raw task object (old 6-field or new unified flat) into NormalizedTask. * Reads new flat fields first, falls back to old nested paths. * Long-term compatible: handles both formats permanently. */ export function normalizeTask(raw: Record): NormalizedTask { if (!raw || typeof raw !== 'object') { return { task_id: 'N/A', status: 'pending', _raw: raw } as NormalizedTask; } // Type-safe access helpers (use intersection for broad compat with old/new schemas) const rawContext = raw.context as (LiteTask['context'] & { requirements?: string[] }) | undefined; const rawFlowControl = raw.flow_control as FlowControl | undefined; const rawMeta = raw.meta as LiteTask['meta'] | undefined; const rawConvergence = raw.convergence as NormalizedTask['convergence'] | undefined; const rawModPoints = raw.modification_points as Array<{ file?: string; target?: string; change?: string }> | undefined; // Description: new flat field first, then join old context.requirements, then old details/scope const rawRequirements = rawContext?.requirements; const rawDetails = raw.details as string[] | undefined; const description = (raw.description as string | undefined) || (Array.isArray(rawRequirements) && rawRequirements.length > 0 ? rawRequirements.join('; ') : undefined) || (Array.isArray(rawDetails) && rawDetails.length > 0 ? rawDetails.join('; ') : undefined) || (raw.scope as string | undefined); // Normalize files: new flat files > flow_control.target_files > modification_points const normalizedFiles = normalizeFilesField(raw.files) || rawFlowControl?.target_files || (rawModPoints?.length ? rawModPoints.filter(m => m.file).map(m => ({ path: m.file!, name: m.target, change: m.change })) : undefined); // Normalize focus_paths: top-level > context > files paths const focusPaths = (raw.focus_paths as string[]) || rawContext?.focus_paths || (normalizedFiles?.length ? normalizedFiles.map(f => f.path).filter(Boolean) : undefined) || []; // Normalize acceptance: convergence > context.acceptance > top-level acceptance const rawAcceptance = raw.acceptance as string[] | undefined; const convergence = rawConvergence || (rawContext?.acceptance?.length ? { criteria: rawContext.acceptance } : undefined) || (rawAcceptance?.length ? { criteria: rawAcceptance } : undefined); return { // Identity task_id: (raw.task_id as string) || (raw.id as string) || 'N/A', title: raw.title as string | undefined, description, status: (raw.status as NormalizedTask['status']) || 'pending', priority: raw.priority as NormalizedTask['priority'], created_at: raw.created_at as string | undefined, updated_at: raw.updated_at as string | undefined, has_summary: raw.has_summary as boolean | undefined, estimated_complexity: raw.estimated_complexity as string | undefined, // Promoted from context (new first, old fallback) depends_on: (raw.depends_on as string[]) || rawContext?.depends_on || [], focus_paths: focusPaths, convergence, // Promoted from flow_control (new first, old fallback) pre_analysis: (raw.pre_analysis as PreAnalysisStep[]) || rawFlowControl?.pre_analysis, implementation: (raw.implementation as (ImplementationStep | string)[]) || rawFlowControl?.implementation_approach, files: normalizedFiles, // Promoted from meta (new first, old fallback) type: (raw.type as string) || rawMeta?.type, scope: (raw.scope as string) || rawMeta?.scope, action: (raw.action as string) || (rawMeta as Record | undefined)?.action as string | undefined, // Preserve original nested objects for backward compat flow_control: rawFlowControl, context: rawContext, meta: rawMeta, // Raw reference _raw: raw, }; } /** * Build a FlowControl object from NormalizedTask for backward-compatible components (e.g. Flowchart). */ export function buildFlowControl(task: NormalizedTask): FlowControl | undefined { const preAnalysis = task.pre_analysis; const implementation = task.implementation; const files = task.files; if (!preAnalysis?.length && !implementation?.length && !files?.length) { return task.flow_control; // Fall back to original if no flat fields } return { pre_analysis: preAnalysis || task.flow_control?.pre_analysis, implementation_approach: implementation || task.flow_control?.implementation_approach, target_files: files || task.flow_control?.target_files, }; } export interface LiteTaskSession { id: string; session_id?: string; type: 'lite-plan' | 'lite-fix' | 'multi-cli-plan'; title?: string; description?: string; path?: string; tasks?: LiteTask[]; metadata?: Record; latestSynthesis?: { title?: string | { en?: string; zh?: string }; status?: string; }; diagnoses?: { manifest?: Record; items?: Array>; }; plan?: Record; roundCount?: number; status?: string; createdAt?: string; updatedAt?: string; // Multi-cli-plan specific fields rounds?: RoundSynthesis[]; } // Multi-cli-plan synthesis types export interface SolutionFileAction { file: string; line: number; action: 'modify' | 'create' | 'delete'; } export interface SolutionTask { id: string; name: string; depends_on: string[]; files: SolutionFileAction[]; key_point: string | null; } export interface Solution { name: string; source_cli: string[]; feasibility: number; effort: string; risk: string; summary: string; pros: string[]; cons: string[]; affected_files: SolutionFileAction[]; implementation_plan: { approach: string; tasks: SolutionTask[]; execution_flow: string; milestones: string[]; }; dependencies: { internal: string[]; external: string[]; }; technical_concerns: string[]; } export interface SynthesisConvergence { score: number; new_insights: boolean; recommendation: 'converged' | 'continue' | 'user_input_needed'; rationale: string; } export interface SynthesisCrossVerification { agreements: string[]; disagreements: Array<{ topic: string; gemini: string; codex: string; resolution: string | null; }>; resolution: string; } export interface RoundSynthesis { round: number; timestamp: string; cli_executions: Record; solutions: Solution[]; convergence: SynthesisConvergence; cross_verification: SynthesisCrossVerification; clarification_questions: string[]; user_feedback_incorporated?: string; } // Multi-cli-plan context-package.json structure export interface MultiCliContextPackage { solution?: { name: string; source_cli: string[]; feasibility: number; effort: string; risk: string; summary: string; }; implementation_plan?: { approach: string; tasks: Array<{ id: string; name: string; depends_on: string[]; files: SolutionFileAction[]; key_point: string | null; }>; execution_flow: string; milestones: string[]; }; dependencies?: { internal: string[]; external: string[]; }; technical_concerns?: string[]; consensus?: { agreements: string[]; resolved_conflicts?: string; }; constraints?: string[]; task_description?: string; session_id?: string; } export interface LiteTasksResponse { litePlan?: LiteTaskSession[]; liteFix?: LiteTaskSession[]; multiCliPlan?: LiteTaskSession[]; } /** * Fetch all lite tasks sessions for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchLiteTasks(projectPath?: string): Promise { const url = projectPath ? `/api/data?path=${encodeURIComponent(projectPath)}` : '/api/data'; const data = await fetchApi<{ liteTasks?: LiteTasksResponse }>(url); return data.liteTasks || {}; } /** * Fetch a single lite task session by ID for a specific workspace * @param sessionId - Session ID to fetch * @param type - Type of lite task * @param projectPath - Optional project path to filter data by workspace */ export async function fetchLiteTaskSession( sessionId: string, type: 'lite-plan' | 'lite-fix' | 'multi-cli-plan', projectPath?: string ): Promise { const data = await fetchLiteTasks(projectPath); const sessions = type === 'lite-plan' ? (data.litePlan || []) : type === 'lite-fix' ? (data.liteFix || []) : (data.multiCliPlan || []); return sessions.find(s => s.id === sessionId || s.session_id === sessionId) || null; } /** * Fetch context data for a lite task session * Uses the session-detail API with type=context */ // Context package core type (compatible with lite and full context-package.json) export interface LiteContextPackage { // Basic fields (lite task context) task_description?: string; constraints?: string[]; focus_paths?: string[]; relevant_files?: Array; dependencies?: string[] | Array<{ name: string; type?: string; version?: string }>; conflict_risks?: string[] | Array<{ description: string; severity?: string }>; session_id?: string; metadata?: Record; // Extended fields (full context-package.json) project_context?: { tech_stack?: { languages?: Array<{ name: string; file_count?: number }>; frameworks?: string[]; libraries?: string[]; }; architecture_patterns?: string[]; }; assets?: { documentation?: Array<{ path: string; relevance_score?: number; scope?: string; contains?: string[] }>; source_code?: Array<{ path: string; relevance_score?: number; scope?: string; contains?: string[] }>; tests?: Array<{ path: string; relevance_score?: number; scope?: string; contains?: string[] }>; }; test_context?: Record; conflict_detection?: { risk_level?: 'low' | 'medium' | 'high' | 'critical'; mitigation_strategy?: string; risk_factors?: { test_gaps?: string[]; existing_implementations?: string[] }; affected_modules?: string[]; }; } export interface LiteExplorationAngle { project_structure?: string[]; relevant_files?: string[]; patterns?: string[]; dependencies?: string[]; integration_points?: string[]; testing?: string[]; findings?: string[]; recommendations?: string[]; risks?: string[]; } export interface LiteDiagnosisItem { id?: string; title?: string; description?: string; symptom?: string; root_cause?: string; issues?: Array<{ file: string; line?: number; severity?: string; message: string }>; affected_files?: string[]; fix_hints?: string[]; } export interface LiteSessionContext { context?: LiteContextPackage; explorations?: { manifest?: { task_description?: string; complexity?: string; exploration_count?: number; }; data?: Record; }; diagnoses?: { manifest?: Record; data?: Record; // Backend session-routes format items?: LiteDiagnosisItem[]; // lite-scanner format (compat) }; } export async function fetchLiteSessionContext( sessionPath: string ): Promise { const data = await fetchApi( `/api/session-detail?path=${encodeURIComponent(sessionPath)}&type=context` ); return data; } // ========== Review Session API ========== export interface ReviewFinding { id?: string; title: string; description?: string; severity: 'critical' | 'high' | 'medium' | 'low'; category?: string; file?: string; line?: string; code_context?: string; snippet?: string; recommendations?: string[]; recommendation?: string; root_cause?: string; impact?: string; references?: string[]; metadata?: Record; fix_status?: string | null; } export interface ReviewDimension { name: string; findings: ReviewFinding[]; } export interface ReviewSession { session_id: string; title?: string; description?: string; type: 'review'; phase?: string; reviewDimensions?: ReviewDimension[]; _isActive?: boolean; created_at?: string; updated_at?: string; status?: string; } export interface ReviewSessionsResponse { reviewSessions?: ReviewSession[]; reviewData?: { sessions?: Array<{ session_id: string; dimensions: Array<{ name: string; findings?: Array }>; findings?: Array; progress?: unknown; }>; }; } /** * Fetch all review sessions */ export async function fetchReviewSessions(): Promise { const data = await fetchApi('/api/data'); // If reviewSessions field exists (legacy format), use it if (data.reviewSessions && data.reviewSessions.length > 0) { return data.reviewSessions; } // Otherwise, transform reviewData.sessions into ReviewSession format if (data.reviewData?.sessions) { return data.reviewData.sessions.map(session => ({ session_id: session.session_id, title: session.session_id, description: '', type: 'review' as const, phase: 'in-progress', reviewDimensions: session.dimensions.map(dim => ({ name: dim.name, findings: dim.findings || [] })), _isActive: true, created_at: undefined, updated_at: undefined, status: 'active' })); } return []; } /** * Fetch a single review session by ID */ export async function fetchReviewSession(sessionId: string): Promise { const sessions = await fetchReviewSessions(); return sessions.find(s => s.session_id === sessionId) || null; } // ========== MCP API ========== export interface McpServer { name: string; command: string; args?: string[]; env?: Record; enabled: boolean; scope: 'project' | 'global'; } export interface McpServerConflict { name: string; projectServer: McpServer; globalServer: McpServer; /** Runtime effective scope */ effectiveScope: 'global' | 'project'; } export interface McpServersResponse { project: McpServer[]; global: McpServer[]; conflicts: McpServerConflict[]; } /** * Fetch complete MCP configuration from all sources * Returns raw config including projects, globalServers, userServers, enterpriseServers */ export async function fetchMcpConfig(): Promise<{ projects: Record; disabledMcpServers?: string[] }>; globalServers: Record; userServers: Record; enterpriseServers: Record; configSources: string[]; codex?: { servers: Record; configPath: string; exists?: boolean }; }> { return fetchApi('/api/mcp-config'); } type UnknownRecord = Record; function isUnknownRecord(value: unknown): value is UnknownRecord { return typeof value === 'object' && value !== null && !Array.isArray(value); } function normalizePathForCompare(inputPath: string): string { const trimmed = inputPath.trim(); if (!trimmed) return ''; let normalized = trimmed.replace(/\\/g, '/'); // Handle /d/path -> D:/path (matches backend normalization) if (/^\/[a-zA-Z]\//.test(normalized)) { normalized = normalized.charAt(1).toUpperCase() + ':' + normalized.slice(2); } // Normalize drive letter casing if (/^[a-zA-Z]:\//.test(normalized)) { normalized = normalized.charAt(0).toUpperCase() + normalized.slice(1); } return normalized.replace(/\/+$/, ''); } function findProjectConfigKey(projects: Record, projectPath?: string): string | null { if (!projectPath) return null; const desired = normalizePathForCompare(projectPath); if (!desired) return null; for (const key of Object.keys(projects)) { if (normalizePathForCompare(key) === desired) { return key; } } // Fallback to exact key match if present return projectPath in projects ? projectPath : null; } function normalizeServerConfig(config: unknown): { command: string; args?: string[]; env?: Record } { if (!isUnknownRecord(config)) { return { command: '' }; } const command = typeof config.command === 'string' ? config.command : typeof config.url === 'string' ? config.url : ''; const args = Array.isArray(config.args) ? config.args.filter((arg): arg is string => typeof arg === 'string') : undefined; const env = isUnknownRecord(config.env) ? Object.fromEntries( Object.entries(config.env).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : [] ) ) : undefined; return { command, args: args && args.length > 0 ? args : undefined, env: env && Object.keys(env).length > 0 ? env : undefined, }; } /** * Fetch all MCP servers (project and global scope) for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchMcpServers(projectPath?: string): Promise { const config = await fetchMcpConfig(); const projectsRecord = isUnknownRecord(config.projects) ? (config.projects as UnknownRecord) : {}; const projectKey = findProjectConfigKey(projectsRecord, projectPath); const projectConfig = projectKey && isUnknownRecord(projectsRecord[projectKey]) ? (projectsRecord[projectKey] as UnknownRecord) : null; const disabledServers = projectConfig && Array.isArray(projectConfig.disabledMcpServers) ? projectConfig.disabledMcpServers.filter((name): name is string => typeof name === 'string') : []; const disabledSet = new Set(disabledServers); const userServers = isUnknownRecord(config.userServers) ? (config.userServers as UnknownRecord) : {}; const projectServersRecord = projectConfig && isUnknownRecord(projectConfig.mcpServers) ? (projectConfig.mcpServers as UnknownRecord) : {}; const global: McpServer[] = Object.entries(userServers).map(([name, raw]) => { const normalized = normalizeServerConfig(raw); return { name, ...normalized, enabled: !disabledSet.has(name), scope: 'global', }; }); const project: McpServer[] = Object.entries(projectServersRecord) .map(([name, raw]) => { const normalized = normalizeServerConfig(raw); return { name, ...normalized, enabled: !disabledSet.has(name), scope: 'project' as const, }; }); // Detect conflicts: same name exists in both project and global const conflicts: McpServerConflict[] = []; for (const ps of project) { const gs = global.find(g => g.name === ps.name); if (gs) { conflicts.push({ name: ps.name, projectServer: ps, globalServer: gs, effectiveScope: 'global', }); } } return { project, global, conflicts, }; } export type McpProjectConfigType = 'mcp' | 'claude'; export interface McpServerMutationOptions { /** Required for project-scoped mutations and for enabled/disabled toggles */ projectPath?: string; /** Controls where project servers are stored (.mcp.json vs legacy .claude.json) */ configType?: McpProjectConfigType; } function requireProjectPath(projectPath: string | undefined, ctx: string): string { const trimmed = projectPath?.trim(); if (!trimmed) { throw new Error(`${ctx}: projectPath is required`); } return trimmed; } function toServerConfig(server: { command: string; args?: string[]; env?: Record }): UnknownRecord { const config: UnknownRecord = { command: server.command }; if (server.args && server.args.length > 0) config.args = server.args; if (server.env && Object.keys(server.env).length > 0) config.env = server.env; return config; } /** * Update MCP server configuration */ export async function updateMcpServer( serverName: string, config: Partial, options: McpServerMutationOptions = {} ): Promise { if (!config.scope) { throw new Error('updateMcpServer: scope is required'); } if (typeof config.command !== 'string' || !config.command.trim()) { throw new Error('updateMcpServer: command is required'); } const serverConfig = toServerConfig({ command: config.command, args: config.args, env: config.env, }); if (config.scope === 'global') { const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-add-global-server', { method: 'POST', body: JSON.stringify({ serverName, serverConfig }), }); if (result?.error) { throw new Error(result.error); } } else { const projectPath = requireProjectPath(options.projectPath, 'updateMcpServer'); const configType = options.configType ?? 'mcp'; const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-copy-server', { method: 'POST', body: JSON.stringify({ projectPath, serverName, serverConfig, configType }), }); if (result?.error) { throw new Error(result.error); } } if (typeof config.enabled === 'boolean') { const projectPath = options.projectPath?.trim(); if (projectPath) { const toggleRes = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-toggle', { method: 'POST', body: JSON.stringify({ projectPath, serverName, enable: config.enabled }), }); if (toggleRes?.error) { throw new Error(toggleRes.error); } } } if (options.projectPath) { const servers = await fetchMcpServers(options.projectPath); return [...servers.project, ...servers.global].find((s) => s.name === serverName) ?? { name: serverName, command: config.command, args: config.args, env: config.env, enabled: config.enabled ?? true, scope: config.scope, }; } return { name: serverName, command: config.command, args: config.args, env: config.env, enabled: config.enabled ?? true, scope: config.scope, }; } /** * Create a new MCP server */ export async function createMcpServer( server: McpServer, options: McpServerMutationOptions = {} ): Promise { if (!server.name?.trim()) { throw new Error('createMcpServer: name is required'); } if (!server.command?.trim()) { throw new Error('createMcpServer: command is required'); } const serverName = server.name.trim(); const serverConfig = toServerConfig(server); if (server.scope === 'global') { const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-add-global-server', { method: 'POST', body: JSON.stringify({ serverName, serverConfig }), }); if (result?.error) { throw new Error(result.error); } } else { const projectPath = requireProjectPath(options.projectPath, 'createMcpServer'); const configType = options.configType ?? 'mcp'; const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-copy-server', { method: 'POST', body: JSON.stringify({ projectPath, serverName, serverConfig, configType }), }); if (result?.error) { throw new Error(result.error); } } // Enforced enabled/disabled is project-scoped (via disabledMcpServers list) if (server.enabled === false) { const projectPath = requireProjectPath(options.projectPath, 'createMcpServer'); const toggleRes = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-toggle', { method: 'POST', body: JSON.stringify({ projectPath, serverName, enable: false }), }); if (toggleRes?.error) { throw new Error(toggleRes.error); } } if (options.projectPath) { const servers = await fetchMcpServers(options.projectPath); return [...servers.project, ...servers.global].find((s) => s.name === serverName) ?? server; } return server; } /** * Delete an MCP server */ export async function deleteMcpServer( serverName: string, scope: 'project' | 'global', options: McpServerMutationOptions = {} ): Promise { if (scope === 'global') { const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-remove-global-server', { method: 'POST', body: JSON.stringify({ serverName }), }); if (result?.error) { throw new Error(result.error); } return; } const projectPath = requireProjectPath(options.projectPath, 'deleteMcpServer'); const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-remove-server', { method: 'POST', body: JSON.stringify({ projectPath, serverName }), }); if (result?.error) { throw new Error(result.error); } } /** * Toggle MCP server enabled status */ export async function toggleMcpServer( serverName: string, enabled: boolean, options: McpServerMutationOptions = {} ): Promise { const projectPath = requireProjectPath(options.projectPath, 'toggleMcpServer'); const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-toggle', { method: 'POST', body: JSON.stringify({ projectPath, serverName, enable: enabled }), }); if (result?.error) { throw new Error(result.error); } const servers = await fetchMcpServers(projectPath); return [...servers.project, ...servers.global].find((s) => s.name === serverName) ?? { name: serverName, command: '', enabled, scope: 'project', }; } // ========== Codex MCP API ========== /** * Codex MCP Server - Read-only server with config path * Extends McpServer with optional configPath field */ export interface CodexMcpServer extends McpServer { configPath?: string; } export interface CodexMcpServersResponse { servers: CodexMcpServer[]; configPath: string; } /** * Fetch Codex MCP servers from config.toml * Codex MCP servers are read-only (managed via config file) */ export async function fetchCodexMcpServers(): Promise { const data = await fetchApi<{ servers?: Record; configPath: string; exists?: boolean }>('/api/codex-mcp-config'); const serversRecord = isUnknownRecord(data.servers) ? (data.servers as UnknownRecord) : {}; const servers: CodexMcpServer[] = Object.entries(serversRecord).map(([name, raw]) => { const normalized = normalizeServerConfig(raw); const enabled = isUnknownRecord(raw) ? (raw.enabled !== false) : true; return { name, ...normalized, enabled, // Codex config is global for the CLI; scope is only used for UI badges in Claude mode scope: 'global', configPath: data.configPath, }; }); return { servers, configPath: data.configPath }; } /** * Add a new MCP server to Codex config * Note: This requires write access to Codex config.toml */ export async function addCodexMcpServer( serverName: string, serverConfig: Record ): Promise<{ success?: boolean; error?: string }> { return fetchApi<{ success?: boolean; error?: string }>('/api/codex-mcp-add', { method: 'POST', body: JSON.stringify({ serverName, serverConfig }), }); } /** * Remove MCP server from Codex config.toml */ export async function codexRemoveServer(serverName: string): Promise<{ success: boolean; error?: string }> { return fetchApi<{ success: boolean; error?: string }>('/api/codex-mcp-remove', { method: 'POST', body: JSON.stringify({ serverName }), }); } /** * Toggle Codex MCP server enabled state */ export async function codexToggleServer( serverName: string, enabled: boolean ): Promise<{ success: boolean; error?: string }> { return fetchApi<{ success: boolean; error?: string }>('/api/codex-mcp-toggle', { method: 'POST', body: JSON.stringify({ serverName, enabled }), }); } // ========== MCP Templates API ========== /** * Fetch all MCP templates from database */ export async function fetchMcpTemplates(): Promise { const data = await fetchApi<{ success: boolean; templates: McpTemplate[] }>('/api/mcp-templates'); return data.templates ?? []; } /** * Save or update MCP template */ export async function saveMcpTemplate( template: Omit ): Promise<{ success: boolean; id?: number; error?: string }> { return fetchApi<{ success: boolean; id?: number; error?: string }>('/api/mcp-templates', { method: 'POST', body: JSON.stringify(template), }); } /** * Delete MCP template by name */ export async function deleteMcpTemplate(templateName: string): Promise<{ success: boolean; error?: string }> { return fetchApi<{ success: boolean; error?: string }>( `/api/mcp-templates/${encodeURIComponent(templateName)}`, { method: 'DELETE' } ); } /** * Install MCP template to project or global scope */ export async function installMcpTemplate( request: McpTemplateInstallRequest ): Promise<{ success: boolean; serverName?: string; error?: string }> { return fetchApi<{ success: boolean; serverName?: string; error?: string }>('/api/mcp-templates/install', { method: 'POST', body: JSON.stringify(request), }); } /** * Search MCP templates by keyword */ export async function searchMcpTemplates(keyword: string): Promise { const data = await fetchApi<{ success: boolean; templates: McpTemplate[] }>( `/api/mcp-templates/search?q=${encodeURIComponent(keyword)}` ); return data.templates ?? []; } /** * Get all MCP template categories */ export async function fetchMcpTemplateCategories(): Promise { const data = await fetchApi<{ success: boolean; categories: string[] }>('/api/mcp-templates/categories'); return data.categories ?? []; } /** * Get MCP templates by category */ export async function fetchMcpTemplatesByCategory(category: string): Promise { const data = await fetchApi<{ success: boolean; templates: McpTemplate[] }>( `/api/mcp-templates/category/${encodeURIComponent(category)}` ); return data.templates ?? []; } // ========== Projects API ========== /** * Fetch all projects for cross-project operations */ export async function fetchAllProjects(): Promise { const config = await fetchMcpConfig(); const projects = Object.keys(config.projects ?? {}).sort((a, b) => a.localeCompare(b)); return { projects }; } /** * Fetch MCP servers from other projects */ export async function fetchOtherProjectsServers( projectPaths?: string[] ): Promise { const config = await fetchMcpConfig(); const userServers = isUnknownRecord(config.userServers) ? (config.userServers as UnknownRecord) : {}; const enterpriseServers = isUnknownRecord(config.enterpriseServers) ? (config.enterpriseServers as UnknownRecord) : {}; const filterSet = projectPaths && projectPaths.length > 0 ? new Set(projectPaths.map((p) => normalizePathForCompare(p))) : null; const servers: OtherProjectsServersResponse['servers'] = {}; for (const [path, rawProjectConfig] of Object.entries(config.projects ?? {})) { const normalizedPath = normalizePathForCompare(path); if (filterSet && !filterSet.has(normalizedPath)) { continue; } const projectConfig = isUnknownRecord(rawProjectConfig) ? (rawProjectConfig as UnknownRecord) : {}; const projectServersRecord = isUnknownRecord(projectConfig.mcpServers) ? (projectConfig.mcpServers as UnknownRecord) : {}; const disabledServers = Array.isArray(projectConfig.disabledMcpServers) ? projectConfig.disabledMcpServers.filter((name): name is string => typeof name === 'string') : []; const disabledSet = new Set(disabledServers); servers[path] = Object.entries(projectServersRecord) // Exclude globally-defined servers; this section is meant for project-local discovery .filter(([name]) => !(name in userServers) && !(name in enterpriseServers)) .map(([name, raw]) => { const normalized = normalizeServerConfig(raw); return { name, ...normalized, enabled: !disabledSet.has(name), }; }); } return { servers }; } // ========== Cross-CLI Operations ========== /** * Copy MCP servers between Claude and Codex CLIs */ export async function crossCliCopy( request: CrossCliCopyRequest ): Promise { const serverNames = request.serverNames ?? []; if (serverNames.length === 0 || request.source === request.target) { return { success: true, copied: [], failed: [] }; } const copied: string[] = []; const failed: Array<{ name: string; error: string }> = []; // Claude -> Codex (upserts into ~/.codex/config.toml via backend) if (request.source === 'claude' && request.target === 'codex') { const config = await fetchMcpConfig(); const projectsRecord = isUnknownRecord(config.projects) ? (config.projects as UnknownRecord) : {}; const projectKey = findProjectConfigKey(projectsRecord, request.projectPath ?? undefined); const projectConfig = projectKey && isUnknownRecord(projectsRecord[projectKey]) ? (projectsRecord[projectKey] as UnknownRecord) : null; const projectServersRecord = projectConfig && isUnknownRecord(projectConfig.mcpServers) ? (projectConfig.mcpServers as UnknownRecord) : {}; for (const name of serverNames) { try { const rawConfig = projectServersRecord[name] ?? (config.userServers ? (config.userServers as UnknownRecord)[name] : undefined) ?? (config.enterpriseServers ? (config.enterpriseServers as UnknownRecord)[name] : undefined); if (!isUnknownRecord(rawConfig)) { failed.push({ name, error: 'Source server config not found' }); continue; } const result = await addCodexMcpServer(name, rawConfig); if (result?.error) { failed.push({ name, error: result.error }); continue; } copied.push(name); } catch (err: unknown) { failed.push({ name, error: err instanceof Error ? err.message : String(err) }); } } return { success: copied.length > 0, copied, failed }; } // Codex -> Claude (defaults to copying into current project via /api/mcp-copy-server) if (request.source === 'codex' && request.target === 'claude') { const projectPath = requireProjectPath(request.projectPath, 'crossCliCopy'); const codex = await fetchApi<{ servers?: Record }>('/api/codex-mcp-config'); const codexServers = isUnknownRecord(codex.servers) ? (codex.servers as UnknownRecord) : {}; for (const name of serverNames) { try { const rawConfig = codexServers[name]; if (!isUnknownRecord(rawConfig)) { failed.push({ name, error: 'Source server config not found' }); continue; } const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-copy-server', { method: 'POST', body: JSON.stringify({ projectPath, serverName: name, serverConfig: rawConfig, configType: 'mcp' }), }); if (result?.error) { failed.push({ name, error: result.error }); continue; } copied.push(name); } catch (err: unknown) { failed.push({ name, error: err instanceof Error ? err.message : String(err) }); } } return { success: copied.length > 0, copied, failed }; } return { success: false, copied: [], failed: serverNames.map((name) => ({ name, error: 'Unsupported copy direction' })), }; } // ========== CLI Endpoints API ========== export interface CliEndpoint { id: string; name: string; type: 'litellm' | 'custom' | 'wrapper'; enabled: boolean; config: Record; } export interface CliEndpointsResponse { endpoints: CliEndpoint[]; } /** * Fetch all CLI endpoints */ export async function fetchCliEndpoints(): Promise { const data = await fetchApi<{ endpoints?: CliEndpoint[] }>('/api/cli/endpoints'); return { endpoints: data.endpoints ?? [], }; } /** * Update CLI endpoint configuration */ export async function updateCliEndpoint( endpointId: string, config: Partial ): Promise { return fetchApi(`/api/cli/endpoints/${encodeURIComponent(endpointId)}`, { method: 'PATCH', body: JSON.stringify(config), }); } /** * Create a new CLI endpoint */ export async function createCliEndpoint( endpoint: Omit ): Promise { return fetchApi('/api/cli/endpoints', { method: 'POST', body: JSON.stringify(endpoint), }); } /** * Delete a CLI endpoint */ export async function deleteCliEndpoint(endpointId: string): Promise { await fetchApi(`/api/cli/endpoints/${encodeURIComponent(endpointId)}`, { method: 'DELETE', }); } /** * Toggle CLI endpoint enabled status */ export async function toggleCliEndpoint( endpointId: string, enabled: boolean ): Promise { return fetchApi(`/api/cli/endpoints/${encodeURIComponent(endpointId)}/toggle`, { method: 'POST', body: JSON.stringify({ enabled }), }); } // ========== CLI Installations API ========== export interface CliInstallation { name: string; version: string; installed: boolean; path?: string; status: 'active' | 'inactive' | 'error'; lastChecked?: string; } export interface CliInstallationsResponse { tools: CliInstallation[]; } /** * Fetch all CLI tool installations */ export async function fetchCliInstallations(): Promise { const data = await fetchApi<{ tools?: CliInstallation[] }>('/api/cli/installations'); return { tools: data.tools ?? [], }; } /** * Install a CLI tool */ export async function installCliTool(toolName: string): Promise { return fetchApi(`/api/cli/installations/${encodeURIComponent(toolName)}/install`, { method: 'POST', }); } /** * Uninstall a CLI tool */ export async function uninstallCliTool(toolName: string): Promise { await fetchApi(`/api/cli/installations/${encodeURIComponent(toolName)}/uninstall`, { method: 'POST', }); } /** * Upgrade a CLI tool */ export async function upgradeCliTool(toolName: string): Promise { return fetchApi(`/api/cli/installations/${encodeURIComponent(toolName)}/upgrade`, { method: 'POST', }); } /** * Check CLI tool installation status */ export async function checkCliToolStatus(toolName: string): Promise { return fetchApi(`/api/cli/installations/${encodeURIComponent(toolName)}/check`, { method: 'POST', }); } // ========== Hooks API ========== export interface Hook { name: string; description?: string; enabled: boolean; script?: string; command?: string; trigger: string; matcher?: string; scope?: 'global' | 'project'; index?: number; templateId?: string; } export interface HooksResponse { hooks: Hook[]; } /** * Raw hook entry as stored in settings.json * Format: { matcher?: string, hooks: [{ type: "command", command: "..." }] } */ interface RawHookEntry { matcher?: string; _templateId?: string; hooks?: Array<{ type?: string; command?: string; prompt?: string; timeout?: number; async?: boolean; }>; // Legacy flat format support command?: string; args?: string[]; script?: string; enabled?: boolean; description?: string; } /** * Parse raw hooks config from backend into flat Hook array */ function parseHooksConfig( data: { global?: { path?: string; hooks?: Record }; project?: { path?: string | null; hooks?: Record }; } ): Hook[] { const result: Hook[] = []; for (const scope of ['project', 'global'] as const) { const scopeData = data[scope]; if (!scopeData?.hooks || typeof scopeData.hooks !== 'object') continue; for (const [event, entries] of Object.entries(scopeData.hooks)) { if (!Array.isArray(entries)) continue; entries.forEach((entry, index) => { // Extract command from nested hooks array (official format) let command = ''; if (entry.hooks && Array.isArray(entry.hooks) && entry.hooks.length > 0) { command = entry.hooks.map(h => h.command || h.prompt || '').filter(Boolean).join(' && '); } // Legacy flat format fallback if (!command && entry.command) { command = entry.args ? `${entry.command} ${entry.args.join(' ')}` : entry.command; } if (!command && entry.script) { command = entry.script; } const name = `${scope}-${event}-${index}`; result.push({ name, description: entry.description, enabled: entry.enabled !== false, command, trigger: event, matcher: entry.matcher, scope, index, templateId: entry._templateId, }); }); } } return result; } /** * Fetch all hooks for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchHooks(projectPath?: string): Promise { const url = projectPath ? `/api/hooks?path=${encodeURIComponent(projectPath)}` : '/api/hooks'; const data = await fetchApi>(url); return { hooks: parseHooksConfig(data as Parameters[0]), }; } /** * Update hook configuration */ export async function updateHook( hookName: string, config: Partial ): Promise { return fetchApi(`/api/hooks/${encodeURIComponent(hookName)}`, { method: 'PATCH', body: JSON.stringify(config), }); } /** * Toggle hook enabled status */ export async function toggleHook( hookName: string, enabled: boolean ): Promise { return fetchApi(`/api/hooks/${encodeURIComponent(hookName)}/toggle`, { method: 'POST', body: JSON.stringify({ enabled }), }); } /** * Create a new hook */ export async function createHook( input: { name: string; description?: string; trigger: string; matcher?: string; command: string } ): Promise { return fetchApi('/api/hooks/create', { method: 'POST', body: JSON.stringify(input), }); } /** * Save a hook to settings file via POST /api/hooks * This writes directly to Claude Code's settings.json in the correct format */ export async function saveHook( scope: 'global' | 'project', event: string, hookData: Record ): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>('/api/hooks', { method: 'POST', body: JSON.stringify({ projectPath: '', scope, event, hookData }), }); } /** * Update hook using dedicated update endpoint with partial input */ export async function updateHookConfig( hookName: string, input: { description?: string; trigger?: string; matcher?: string; command?: string } ): Promise { return fetchApi('/api/hooks/update', { method: 'POST', body: JSON.stringify({ name: hookName, ...input }), }); } /** * Delete a hook */ export async function deleteHook(hookName: string): Promise { return fetchApi(`/api/hooks/delete/${encodeURIComponent(hookName)}`, { method: 'DELETE', }); } /** * Install a hook from predefined template * Converts template data to Claude Code's settings.json format: * { _templateId, matcher?, hooks: [{ type: "command", command: "full command string" }] } */ export async function installHookTemplate( trigger: string, templateData: { id: string; command: string; args?: string[]; matcher?: string } ): Promise<{ success: boolean }> { // Build full command string from command + args const fullCommand = templateData.args ? `${templateData.command} ${templateData.args.map(a => a.includes(' ') ? `'${a}'` : a).join(' ')}` : templateData.command; // Build hookData in Claude Code's official nested format // _templateId is ignored by Claude Code but used for installed detection const hookData: Record = { _templateId: templateData.id, hooks: [ { type: 'command', command: fullCommand, } ] }; if (templateData.matcher) { hookData.matcher = templateData.matcher; } return saveHook('project', trigger, hookData); } // ========== Rules API ========== /** * Fetch all rules for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchRules(projectPath?: string): Promise { // Try with project path first, fall back to global on 403/404 if (projectPath) { try { const url = `/api/rules?path=${encodeURIComponent(projectPath)}`; const data = await fetchApi<{ rules?: Rule[]; projectRules?: Rule[]; userRules?: Rule[] }>(url); const allRules = [...(data.projectRules ?? []), ...(data.userRules ?? [])]; return { rules: data.rules ?? allRules, }; } catch (error: unknown) { const apiError = error as ApiError; if (apiError.status === 403 || apiError.status === 404) { // Fall back to global rules list console.warn('[fetchRules] 403/404 for project path, falling back to global rules'); } else { throw error; } } } // Fallback: fetch global rules const data = await fetchApi<{ rules?: Rule[]; projectRules?: Rule[]; userRules?: Rule[] }>('/api/rules'); const allRules = [...(data.projectRules ?? []), ...(data.userRules ?? [])]; return { rules: data.rules ?? allRules, }; } /** * Update rule configuration */ export async function updateRule( ruleId: string, config: Partial ): Promise { return fetchApi(`/api/rules/${encodeURIComponent(ruleId)}`, { method: 'PATCH', body: JSON.stringify(config), }); } /** * Toggle rule enabled status */ export async function toggleRule( ruleId: string, enabled: boolean ): Promise { return fetchApi(`/api/rules/${encodeURIComponent(ruleId)}/toggle`, { method: 'POST', body: JSON.stringify({ enabled }), }); } /** * Create a new rule */ export async function createRule(input: RuleCreateInput): Promise { return fetchApi('/api/rules/create', { method: 'POST', body: JSON.stringify(input), }); } /** * Delete a rule */ export async function deleteRule( ruleId: string, location?: string ): Promise { return fetchApi(`/api/rules/${encodeURIComponent(ruleId)}`, { method: 'DELETE', body: JSON.stringify({ location }), }); } /** * Add MCP server to global scope (~/.claude.json mcpServers) */ export async function addGlobalMcpServer( serverName: string, serverConfig: { command: string; args?: string[]; env?: Record; type?: string; } ): Promise<{ success: boolean; error?: string }> { return fetchApi<{ success: boolean; error?: string }>('/api/mcp-add-global-server', { method: 'POST', body: JSON.stringify({ serverName, serverConfig }), }); } /** * Copy/Add MCP server to project (.mcp.json or .claude.json) */ export async function copyMcpServerToProject( serverName: string, serverConfig: { command: string; args?: string[]; env?: Record; type?: string; }, projectPath: string, configType: 'mcp' | 'claude' = 'mcp' ): Promise<{ success: boolean; error?: string }> { const path = requireProjectPath(projectPath, 'copyMcpServerToProject'); return fetchApi<{ success: boolean; error?: string }>('/api/mcp-copy-server', { method: 'POST', body: JSON.stringify({ projectPath: path, serverName, serverConfig, configType }), }); } // ========== CCW Tools MCP API ========== /** * CCW MCP configuration interface */ export interface CcwMcpConfig { isInstalled: boolean; enabledTools: string[]; projectRoot?: string; allowedDirs?: string; enableSandbox?: boolean; installedScopes: ('global' | 'project')[]; } /** * Platform detection for cross-platform MCP config */ const isWindows = typeof navigator !== 'undefined' && navigator.platform?.toLowerCase().includes('win'); /** * Build CCW MCP server config */ function buildCcwMcpServerConfig(config: { enabledTools?: string[]; projectRoot?: string; allowedDirs?: string; enableSandbox?: boolean; }): { command: string; args: string[]; env: Record } { const env: Record = {}; if (config.enabledTools && config.enabledTools.length > 0) { env.CCW_ENABLED_TOOLS = config.enabledTools.join(','); } else { env.CCW_ENABLED_TOOLS = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search'; } if (config.projectRoot) { env.CCW_PROJECT_ROOT = config.projectRoot; } if (config.allowedDirs) { env.CCW_ALLOWED_DIRS = config.allowedDirs; } if (config.enableSandbox) { env.CCW_ENABLE_SANDBOX = '1'; } // Cross-platform config if (isWindows) { return { command: 'cmd', args: ['/c', 'npx', '-y', 'ccw-mcp'], env }; } return { command: 'npx', args: ['-y', 'ccw-mcp'], env }; } /** * Fetch CCW Tools MCP configuration by checking if ccw-tools server exists */ export async function fetchCcwMcpConfig(currentProjectPath?: string): Promise { try { const config = await fetchMcpConfig(); const installedScopes: ('global' | 'project')[] = []; let ccwServer: any = null; // Check global/user servers if (config.globalServers?.['ccw-tools']) { installedScopes.push('global'); ccwServer = config.globalServers['ccw-tools']; } else if (config.userServers?.['ccw-tools']) { installedScopes.push('global'); ccwServer = config.userServers['ccw-tools']; } // Check project servers - only check current project if specified if (config.projects) { if (currentProjectPath) { // Normalize path for comparison (forward slashes) const normalizedCurrent = currentProjectPath.replace(/\\/g, '/'); for (const [key, proj] of Object.entries(config.projects)) { const normalizedKey = key.replace(/\\/g, '/'); if (normalizedKey === normalizedCurrent && proj.mcpServers?.['ccw-tools']) { installedScopes.push('project'); if (!ccwServer) ccwServer = proj.mcpServers['ccw-tools']; break; } } } else { // Fallback: check all projects (legacy behavior) for (const proj of Object.values(config.projects)) { if (proj.mcpServers?.['ccw-tools']) { installedScopes.push('project'); if (!ccwServer) ccwServer = proj.mcpServers['ccw-tools']; break; } } } } if (!ccwServer) { return { isInstalled: false, enabledTools: [], installedScopes: [], }; } // Parse enabled tools from env const env = ccwServer.env || {}; const enabledToolsStr = env.CCW_ENABLED_TOOLS || 'all'; const enabledTools = enabledToolsStr === 'all' ? ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question'] : enabledToolsStr.split(',').map((t: string) => t.trim()); return { isInstalled: true, enabledTools, projectRoot: env.CCW_PROJECT_ROOT, allowedDirs: env.CCW_ALLOWED_DIRS, enableSandbox: env.CCW_ENABLE_SANDBOX === '1', installedScopes, }; } catch { return { isInstalled: false, enabledTools: [], installedScopes: [], }; } } /** * Update CCW Tools MCP configuration (re-install with new config) */ export async function updateCcwConfig(config: { enabledTools?: string[]; projectRoot?: string; allowedDirs?: string; enableSandbox?: boolean; }): Promise { const serverConfig = buildCcwMcpServerConfig(config); // Install/update to global config const result = await addGlobalMcpServer('ccw-tools', serverConfig); if (!result.success) { throw new Error(result.error || 'Failed to update CCW config'); } return fetchCcwMcpConfig(); } /** * Install CCW Tools MCP server */ export async function installCcwMcp( scope: 'global' | 'project' = 'global', projectPath?: string ): Promise { const serverConfig = buildCcwMcpServerConfig({ enabledTools: ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'], }); if (scope === 'project' && projectPath) { const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-copy-server', { method: 'POST', body: JSON.stringify({ projectPath, serverName: 'ccw-tools', serverConfig, configType: 'mcp', }), }); if (result?.error) { throw new Error(result.error || 'Failed to install CCW MCP to project'); } } else { const result = await addGlobalMcpServer('ccw-tools', serverConfig); if (!result.success) { throw new Error(result.error || 'Failed to install CCW MCP'); } } return fetchCcwMcpConfig(); } /** * Uninstall CCW Tools MCP server from all scopes (global + projects) */ export async function uninstallCcwMcp(): Promise { // 1. Remove from global scope try { await fetchApi<{ success: boolean }>('/api/mcp-remove-global-server', { method: 'POST', body: JSON.stringify({ serverName: 'ccw-tools' }), }); } catch { // May not exist in global - continue } // 2. Remove from all projects that have ccw-tools try { const config = await fetchMcpConfig(); if (config.projects) { const removePromises = Object.entries(config.projects) .filter(([_, proj]) => proj.mcpServers?.['ccw-tools']) .map(([projectPath]) => fetchApi<{ success: boolean }>('/api/mcp-remove-server', { method: 'POST', body: JSON.stringify({ projectPath, serverName: 'ccw-tools' }), }).catch(() => {}) ); await Promise.all(removePromises); } } catch { // Best-effort cleanup } } /** * Uninstall CCW Tools MCP server from a specific scope */ export async function uninstallCcwMcpFromScope( scope: 'global' | 'project', projectPath?: string ): Promise { if (scope === 'global') { await fetchApi('/api/mcp-remove-global-server', { method: 'POST', body: JSON.stringify({ serverName: 'ccw-tools' }), }); } else { if (!projectPath) throw new Error('projectPath required for project scope uninstall'); await fetchApi('/api/mcp-remove-server', { method: 'POST', body: JSON.stringify({ projectPath, serverName: 'ccw-tools' }), }); } } // ========== CCW Tools MCP - Codex API ========== /** * Fetch CCW Tools MCP configuration from Codex config.toml */ export async function fetchCcwMcpConfigForCodex(): Promise { try { const { servers } = await fetchCodexMcpServers(); const ccwServer = servers.find((s) => s.name === 'ccw-tools'); if (!ccwServer) { return { isInstalled: false, enabledTools: [], installedScopes: [] }; } const env = ccwServer.env || {}; const enabledToolsStr = env.CCW_ENABLED_TOOLS || 'all'; const enabledTools = enabledToolsStr === 'all' ? ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'] : enabledToolsStr.split(',').map((t: string) => t.trim()); return { isInstalled: true, enabledTools, projectRoot: env.CCW_PROJECT_ROOT, allowedDirs: env.CCW_ALLOWED_DIRS, enableSandbox: env.CCW_ENABLE_SANDBOX === '1', installedScopes: ['global'], }; } catch { return { isInstalled: false, enabledTools: [], installedScopes: [] }; } } /** * Build CCW MCP server config for Codex (uses global ccw-mcp command) */ function buildCcwMcpServerConfigForCodex(config: { enabledTools?: string[]; projectRoot?: string; allowedDirs?: string; enableSandbox?: boolean; }): { command: string; args: string[]; env: Record } { const env: Record = {}; if (config.enabledTools && config.enabledTools.length > 0) { env.CCW_ENABLED_TOOLS = config.enabledTools.join(','); } else { env.CCW_ENABLED_TOOLS = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search'; } if (config.projectRoot) { env.CCW_PROJECT_ROOT = config.projectRoot; } if (config.allowedDirs) { env.CCW_ALLOWED_DIRS = config.allowedDirs; } if (config.enableSandbox) { env.CCW_ENABLE_SANDBOX = '1'; } return { command: 'ccw-mcp', args: [], env }; } /** * Install CCW Tools MCP to Codex config.toml */ export async function installCcwMcpToCodex(): Promise { const serverConfig = buildCcwMcpServerConfigForCodex({ enabledTools: ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'], }); const result = await addCodexMcpServer('ccw-tools', serverConfig); if (result.error) { throw new Error(result.error || 'Failed to install CCW MCP to Codex'); } return fetchCcwMcpConfigForCodex(); } /** * Uninstall CCW Tools MCP from Codex config.toml */ export async function uninstallCcwMcpFromCodex(): Promise { const result = await codexRemoveServer('ccw-tools'); if (!result.success) { throw new Error(result.error || 'Failed to uninstall CCW MCP from Codex'); } } /** * Update CCW Tools MCP configuration in Codex config.toml */ export async function updateCcwConfigForCodex(config: { enabledTools?: string[]; projectRoot?: string; allowedDirs?: string; enableSandbox?: boolean; }): Promise { const serverConfig = buildCcwMcpServerConfigForCodex(config); const result = await addCodexMcpServer('ccw-tools', serverConfig); if (result.error) { throw new Error(result.error || 'Failed to update CCW config in Codex'); } return fetchCcwMcpConfigForCodex(); } // ========== Index Management API ========== /** * Fetch current index status for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchIndexStatus(projectPath?: string): Promise { const url = projectPath ? `/api/codexlens/workspace-status?path=${encodeURIComponent(projectPath)}` : '/api/codexlens/workspace-status'; const resp = await fetchApi<{ success: boolean; hasIndex: boolean; fts?: { indexedFiles: number; totalFiles: number }; }>(url); return { totalFiles: resp.fts?.totalFiles ?? 0, lastUpdated: new Date().toISOString(), buildTime: 0, status: resp.hasIndex ? 'completed' : 'idle', }; } /** * Rebuild index */ export async function rebuildIndex(request: IndexRebuildRequest = {}): Promise { await fetchApi<{ success: boolean }>('/api/codexlens/init', { method: 'POST', body: JSON.stringify({ path: request.paths?.[0], indexType: 'vector', }), }); return { totalFiles: 0, lastUpdated: new Date().toISOString(), buildTime: 0, status: 'building', }; } // ========== Prompt History API ========== /** * Prompt history response from backend */ export interface PromptsResponse { prompts: Prompt[]; total: number; } /** * Prompt insights response from backend */ export interface PromptInsightsResponse { insights: PromptInsight[]; patterns: Pattern[]; suggestions: Suggestion[]; } /** * Insight history entry from CLI analysis */ export interface InsightHistory { /** Unique insight identifier */ id: string; /** Created timestamp */ created_at: string; /** AI tool used for analysis */ tool: 'gemini' | 'qwen' | 'codex' | string; /** Number of prompts analyzed */ prompt_count: number; /** Detected patterns */ patterns: Pattern[]; /** AI suggestions */ suggestions: Suggestion[]; /** Associated execution ID */ execution_id: string | null; /** Language preference */ lang: string; } /** * Insights history response from backend */ export interface InsightsHistoryResponse { insights: InsightHistory[]; } /** * Analyze prompts request */ export interface AnalyzePromptsRequest { tool?: 'gemini' | 'qwen' | 'codex'; promptIds?: string[]; limit?: number; } /** * Fetch all prompts from history for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchPrompts(projectPath?: string): Promise { const url = projectPath ? `/api/memory/prompts?path=${encodeURIComponent(projectPath)}` : '/api/memory/prompts'; return fetchApi(url); } /** * Fetch prompt insights from backend for a specific workspace * @param projectPath - Optional project path to filter data by workspace */ export async function fetchPromptInsights(projectPath?: string): Promise { const url = projectPath ? `/api/memory/insights?path=${encodeURIComponent(projectPath)}` : '/api/memory/insights'; return fetchApi(url); } /** * Fetch insights history (past CLI analyses) from backend * @param projectPath - Optional project path to filter data by workspace * @param limit - Maximum number of insights to fetch (default: 20) */ export async function fetchInsightsHistory(projectPath?: string, limit: number = 20): Promise { const url = projectPath ? `/api/memory/insights?limit=${limit}&path=${encodeURIComponent(projectPath)}` : `/api/memory/insights?limit=${limit}`; return fetchApi(url); } /** * Fetch a single insight detail by ID * @param insightId - Insight ID to fetch * @param projectPath - Optional project path to filter data by workspace */ export async function fetchInsightDetail(insightId: string, projectPath?: string): Promise { const url = projectPath ? `/api/memory/insights/${encodeURIComponent(insightId)}?path=${encodeURIComponent(projectPath)}` : `/api/memory/insights/${encodeURIComponent(insightId)}`; return fetchApi(url); } /** * Analyze prompts using AI tool */ export async function analyzePrompts(request: AnalyzePromptsRequest = {}): Promise { return fetchApi('/api/memory/analyze', { method: 'POST', body: JSON.stringify(request), }); } /** * Delete a prompt from history */ export async function deletePrompt(promptId: string): Promise { await fetchApi('/api/memory/prompts/' + encodeURIComponent(promptId), { method: 'DELETE', }); } /** * Delete an insight from history */ export async function deleteInsight(insightId: string, projectPath?: string): Promise<{ success: boolean }> { const url = projectPath ? `/api/memory/insights/${encodeURIComponent(insightId)}?path=${encodeURIComponent(projectPath)}` : `/api/memory/insights/${encodeURIComponent(insightId)}`; return fetchApi<{ success: boolean }>(url, { method: 'DELETE', }); } /** * Batch delete prompts from history */ export async function batchDeletePrompts(promptIds: string[]): Promise<{ deleted: number }> { return fetchApi<{ deleted: number }>('/api/memory/prompts/batch-delete', { method: 'POST', body: JSON.stringify({ promptIds }), }); } // ========== File Explorer API ========== /** * File tree response from backend */ export interface FileTreeResponse { rootNodes: import('../types/file-explorer').FileSystemNode[]; fileCount: number; directoryCount: number; totalSize: number; buildTime: number; } /** * Fetch file tree for a given root path */ export async function fetchFileTree(rootPath: string = '/', options: { maxDepth?: number; includeHidden?: boolean; excludePatterns?: string[]; } = {}): Promise { const params = new URLSearchParams(); params.append('rootPath', rootPath); if (options.maxDepth !== undefined) params.append('maxDepth', String(options.maxDepth)); if (options.includeHidden !== undefined) params.append('includeHidden', String(options.includeHidden)); if (options.excludePatterns) params.append('excludePatterns', options.excludePatterns.join(',')); return fetchApi(`/api/explorer/tree?${params.toString()}`); } /** * Fetch file content */ export async function fetchFileContent(filePath: string, options: { encoding?: 'utf8' | 'ascii' | 'base64'; maxSize?: number; } = {}): Promise { const params = new URLSearchParams(); params.append('path', filePath); if (options.encoding) params.append('encoding', options.encoding); if (options.maxSize !== undefined) params.append('maxSize', String(options.maxSize)); return fetchApi(`/api/explorer/file?${params.toString()}`); } /** * Search files request */ export interface SearchFilesRequest { rootPath?: string; query: string; filePatterns?: string[]; excludePatterns?: string[]; maxResults?: number; caseSensitive?: boolean; } /** * Search files response */ export interface SearchFilesResponse { results: Array<{ path: string; name: string; type: 'file' | 'directory'; matches: Array<{ line: number; column: number; context: string; }>; }>; totalMatches: number; searchTime: number; } /** * Search files by content or name */ export async function searchFiles(request: SearchFilesRequest): Promise { return fetchApi('/api/explorer/search', { method: 'POST', body: JSON.stringify(request), }); } /** * Get available root directories */ export interface RootDirectory { path: string; name: string; isWorkspace: boolean; isGitRoot: boolean; } export async function fetchRootDirectories(): Promise { return fetchApi('/api/explorer/roots'); } // ========== Graph Explorer API ========== /** * Graph dependencies request */ export interface GraphDependenciesRequest { rootPath?: string; maxDepth?: number; includeTypes?: string[]; excludePatterns?: string[]; } /** * Graph dependencies response */ export interface GraphDependenciesResponse { nodes: import('../types/graph-explorer').GraphNode[]; edges: import('../types/graph-explorer').GraphEdge[]; metadata: import('../types/graph-explorer').GraphMetadata; } /** * Fetch graph dependencies for code visualization */ export async function fetchGraphDependencies(request: GraphDependenciesRequest = {}): Promise { const params = new URLSearchParams(); if (request.rootPath) params.append('rootPath', request.rootPath); if (request.maxDepth !== undefined) params.append('maxDepth', String(request.maxDepth)); if (request.includeTypes) params.append('includeTypes', request.includeTypes.join(',')); if (request.excludePatterns) params.append('excludePatterns', request.excludePatterns.join(',')); return fetchApi(`/api/graph/dependencies?${params.toString()}`); } /** * Graph impact analysis request */ export interface GraphImpactRequest { nodeId: string; direction?: 'upstream' | 'downstream' | 'both'; maxDepth?: number; } /** * Graph impact analysis response */ export interface GraphImpactResponse { nodeId: string; dependencies: import('../types/graph-explorer').GraphNode[]; dependents: import('../types/graph-explorer').GraphNode[]; paths: Array<{ nodes: string[]; edges: string[]; }>; } /** * Fetch impact analysis for a specific node */ export async function fetchGraphImpact(request: GraphImpactRequest): Promise { const params = new URLSearchParams(); params.append('nodeId', request.nodeId); if (request.direction) params.append('direction', request.direction); if (request.maxDepth !== undefined) params.append('maxDepth', String(request.maxDepth)); return fetchApi(`/api/graph/impact?${params.toString()}`); } // ========== CodexLens API ========== /** * CodexLens venv status response */ export interface CodexLensVenvStatus { ready: boolean; installed: boolean; version?: string; pythonVersion?: string; venvPath?: string; error?: string; } /** * CodexLens status data */ export interface CodexLensStatusData { projects_count?: number; total_files?: number; total_chunks?: number; api_url?: string; api_ready?: boolean; [key: string]: unknown; } /** * CodexLens configuration */ export interface CodexLensConfig { index_dir: string; index_count: number; api_max_workers: number; api_batch_size: number; } /** * Semantic search status */ export interface CodexLensSemanticStatus { available: boolean; backend?: string; model?: string; hasEmbeddings?: boolean; [key: string]: unknown; } /** * Dashboard init response */ export interface CodexLensDashboardInitResponse { installed: boolean; status: CodexLensVenvStatus; config: CodexLensConfig; semantic: CodexLensSemanticStatus; statusData?: CodexLensStatusData; } /** * Workspace index status */ export interface CodexLensWorkspaceStatus { success: boolean; hasIndex: boolean; path?: string; fts: { percent: number; indexedFiles: number; totalFiles: number; }; vector: { percent: number; filesWithEmbeddings: number; totalFiles: number; totalChunks: number; }; } /** * GPU device info */ export interface CodexLensGpuDevice { name: string; type: 'integrated' | 'discrete'; index: number; device_id?: string; memory?: { total?: number; free?: number; }; } /** * GPU detect response */ export interface CodexLensGpuDetectResponse { success: boolean; supported: boolean; platform: string; deviceCount?: number; devices?: CodexLensGpuDevice[]; error?: string; } /** * GPU list response */ export interface CodexLensGpuListResponse { success: boolean; devices: CodexLensGpuDevice[]; selected_device_id?: string | number; } /** * Model info (normalized from CLI output) */ export interface CodexLensModel { profile: string; name: string; type: 'embedding' | 'reranker'; backend: string; size?: string; installed: boolean; cache_path?: string; /** Original HuggingFace model name */ model_name?: string; /** Model description */ description?: string; /** Use case description */ use_case?: string; /** Embedding dimensions */ dimensions?: number; /** Whether this model is recommended */ recommended?: boolean; /** Model source: 'predefined' | 'discovered' */ source?: string; /** Estimated size in MB */ estimated_size_mb?: number; /** Actual size in MB (when installed) */ actual_size_mb?: number | null; } /** * Model list response (normalized) */ export interface CodexLensModelsResponse { success: boolean; models: CodexLensModel[]; } /** * Model info response */ export interface CodexLensModelInfoResponse { success: boolean; profile: string; info: { name: string; backend: string; type: string; size?: string; path?: string; [key: string]: unknown; }; } /** * Download model response */ export interface CodexLensDownloadModelResponse { success: boolean; message?: string; profile?: string; progress?: number; error?: string; } /** * Delete model response */ export interface CodexLensDeleteModelResponse { success: boolean; message?: string; error?: string; } /** * Environment variables response */ export interface CodexLensEnvResponse { success: boolean; path?: string; env: Record; raw?: string; settings?: Record; } /** * Update environment request */ export interface CodexLensUpdateEnvRequest { env: Record; } /** * Update environment response */ export interface CodexLensUpdateEnvResponse { success: boolean; message?: string; path?: string; settingsPath?: string; } /** * Ignore patterns response */ export interface CodexLensIgnorePatternsResponse { success: boolean; patterns: string[]; extensionFilters: string[]; defaults: { patterns: string[]; extensionFilters: string[]; }; } /** * Update ignore patterns request */ export interface CodexLensUpdateIgnorePatternsRequest { patterns?: string[]; extensionFilters?: string[]; } /** * Bootstrap install response */ export interface CodexLensBootstrapResponse { success: boolean; message?: string; version?: string; error?: string; } /** * Uninstall response */ export interface CodexLensUninstallResponse { success: boolean; message?: string; error?: string; } /** * Fetch CodexLens dashboard initialization data */ export async function fetchCodexLensDashboardInit(): Promise { return fetchApi('/api/codexlens/dashboard-init'); } /** * Fetch CodexLens venv status */ export async function fetchCodexLensStatus(): Promise { return fetchApi('/api/codexlens/status'); } /** * Fetch CodexLens workspace index status */ export async function fetchCodexLensWorkspaceStatus(projectPath: string): Promise { const params = new URLSearchParams(); params.append('path', projectPath); return fetchApi(`/api/codexlens/workspace-status?${params.toString()}`); } /** * Fetch CodexLens configuration */ export async function fetchCodexLensConfig(): Promise { return fetchApi('/api/codexlens/config'); } /** * Update CodexLens configuration */ export async function updateCodexLensConfig(config: { index_dir: string; api_max_workers?: number; api_batch_size?: number; }): Promise<{ success: boolean; message?: string; error?: string }> { return fetchApi('/api/codexlens/config', { method: 'POST', body: JSON.stringify(config), }); } /** * Bootstrap/install CodexLens */ export async function bootstrapCodexLens(): Promise { return fetchApi('/api/codexlens/bootstrap', { method: 'POST', body: JSON.stringify({}), }); } /** * CodexLens semantic install response */ export interface CodexLensSemanticInstallResponse { success: boolean; message?: string; gpuMode?: string; available?: boolean; backend?: string; accelerator?: string; providers?: string[]; error?: string; } /** * Install CodexLens semantic dependencies with GPU mode */ export async function installCodexLensSemantic(gpuMode: 'cpu' | 'cuda' | 'directml' = 'cpu'): Promise { return fetchApi('/api/codexlens/semantic/install', { method: 'POST', body: JSON.stringify({ gpuMode }), }); } /** * Uninstall CodexLens */ export async function uninstallCodexLens(): Promise { return fetchApi('/api/codexlens/uninstall', { method: 'POST', body: JSON.stringify({}), }); } /** * Fetch CodexLens models list * Normalizes the CLI response format to match the frontend interface. * CLI returns: { success, result: { models: [{ model_name, estimated_size_mb, ... }] } } * Frontend expects: { success, models: [{ name, size, type, backend, ... }] } */ export async function fetchCodexLensModels(): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const raw = await fetchApi('/api/codexlens/models'); // Handle nested result structure from CLI const rawModels = raw?.result?.models ?? raw?.models ?? []; const models: CodexLensModel[] = rawModels.map((m: Record) => ({ profile: (m.profile as string) || '', name: (m.model_name as string) || (m.name as string) || (m.profile as string) || '', type: (m.type as 'embedding' | 'reranker') || 'embedding', backend: (m.source as string) || 'fastembed', size: m.installed && m.actual_size_mb ? `${(m.actual_size_mb as number).toFixed(0)} MB` : m.estimated_size_mb ? `~${m.estimated_size_mb} MB` : undefined, installed: (m.installed as boolean) ?? false, cache_path: m.cache_path as string | undefined, model_name: m.model_name as string | undefined, description: m.description as string | undefined, use_case: m.use_case as string | undefined, dimensions: m.dimensions as number | undefined, recommended: m.recommended as boolean | undefined, source: m.source as string | undefined, estimated_size_mb: m.estimated_size_mb as number | undefined, actual_size_mb: m.actual_size_mb as number | null | undefined, })); return { success: raw?.success ?? true, models, }; } /** * Fetch CodexLens model info by profile */ export async function fetchCodexLensModelInfo(profile: string): Promise { const params = new URLSearchParams(); params.append('profile', profile); return fetchApi(`/api/codexlens/models/info?${params.toString()}`); } /** * Download CodexLens model by profile */ export async function downloadCodexLensModel(profile: string): Promise { return fetchApi('/api/codexlens/models/download', { method: 'POST', body: JSON.stringify({ profile }), }); } /** * Download custom CodexLens model from HuggingFace */ export async function downloadCodexLensCustomModel(modelName: string, modelType: string = 'embedding'): Promise { return fetchApi('/api/codexlens/models/download-custom', { method: 'POST', body: JSON.stringify({ model_name: modelName, model_type: modelType }), }); } /** * Delete CodexLens model by profile */ export async function deleteCodexLensModel(profile: string): Promise { return fetchApi('/api/codexlens/models/delete', { method: 'POST', body: JSON.stringify({ profile }), }); } /** * Delete CodexLens model by cache path */ export async function deleteCodexLensModelByPath(cachePath: string): Promise { return fetchApi('/api/codexlens/models/delete-path', { method: 'POST', body: JSON.stringify({ cache_path: cachePath }), }); } /** * Fetch CodexLens environment variables */ export async function fetchCodexLensEnv(): Promise { return fetchApi('/api/codexlens/env'); } /** * Update CodexLens environment variables */ export async function updateCodexLensEnv(request: CodexLensUpdateEnvRequest): Promise { return fetchApi('/api/codexlens/env', { method: 'POST', body: JSON.stringify(request), }); } /** * Detect GPU support for CodexLens */ export async function fetchCodexLensGpuDetect(): Promise { return fetchApi('/api/codexlens/gpu/detect'); } /** * Fetch available GPU devices */ export async function fetchCodexLensGpuList(): Promise { return fetchApi('/api/codexlens/gpu/list'); } /** * Select GPU device for CodexLens */ export async function selectCodexLensGpu(deviceId: string | number): Promise<{ success: boolean; message?: string; error?: string }> { return fetchApi('/api/codexlens/gpu/select', { method: 'POST', body: JSON.stringify({ device_id: deviceId }), }); } /** * Reset GPU selection to auto-detection */ export async function resetCodexLensGpu(): Promise<{ success: boolean; message?: string; error?: string }> { return fetchApi('/api/codexlens/gpu/reset', { method: 'POST', body: JSON.stringify({}), }); } /** * Fetch CodexLens ignore patterns */ export async function fetchCodexLensIgnorePatterns(): Promise { return fetchApi('/api/codexlens/ignore-patterns'); } /** * Update CodexLens ignore patterns */ export async function updateCodexLensIgnorePatterns(request: CodexLensUpdateIgnorePatternsRequest): Promise { return fetchApi('/api/codexlens/ignore-patterns', { method: 'POST', body: JSON.stringify(request), }); } // ========== CodexLens Reranker Config API ========== /** * Reranker LiteLLM model info */ export interface RerankerLitellmModel { modelId: string; modelName: string; providers: string[]; } /** * Reranker configuration response from GET /api/codexlens/reranker/config */ export interface RerankerConfigResponse { success: boolean; backend: string; model_name: string; api_provider: string; api_key_set: boolean; available_backends: string[]; api_providers: string[]; litellm_endpoints: string[]; litellm_models?: RerankerLitellmModel[]; config_source: string; error?: string; } /** * Reranker configuration update request for POST /api/codexlens/reranker/config */ export interface RerankerConfigUpdateRequest { backend?: string; model_name?: string; api_provider?: string; api_key?: string; litellm_endpoint?: string; } /** * Reranker configuration update response */ export interface RerankerConfigUpdateResponse { success: boolean; message?: string; updates?: string[]; error?: string; } /** * Fetch reranker configuration (backends, models, providers) */ export async function fetchRerankerConfig(): Promise { return fetchApi('/api/codexlens/reranker/config'); } /** * Update reranker configuration */ export async function updateRerankerConfig( request: RerankerConfigUpdateRequest ): Promise { return fetchApi('/api/codexlens/reranker/config', { method: 'POST', body: JSON.stringify(request), }); } // ========== CodexLens Search API ========== /** * CodexLens search request parameters */ export interface CodexLensSearchParams { query: string; limit?: number; mode?: 'dense_rerank' | 'fts' | 'fuzzy'; max_content_length?: number; extra_files_count?: number; } /** * CodexLens search result */ export interface CodexLensSearchResult { path: string; score: number; content?: string; line_start?: number; line_end?: number; [key: string]: unknown; } /** * CodexLens search response */ export interface CodexLensSearchResponse { success: boolean; results: CodexLensSearchResult[]; total?: number; query: string; error?: string; } /** * CodexLens symbol search response */ export interface CodexLensSymbolSearchResponse { success: boolean; symbols: Array<{ name: string; kind: string; path: string; line: number; [key: string]: unknown; }>; error?: string; } /** * CodexLens file search response (returns file paths only) */ export interface CodexLensFileSearchResponse { success: boolean; query?: string; count?: number; files: string[]; error?: string; } /** * Perform content search using CodexLens */ export async function searchCodexLens(params: CodexLensSearchParams): Promise { const queryParams = new URLSearchParams(); queryParams.append('query', params.query); if (params.limit) queryParams.append('limit', String(params.limit)); if (params.mode) queryParams.append('mode', params.mode); if (params.max_content_length) queryParams.append('max_content_length', String(params.max_content_length)); if (params.extra_files_count) queryParams.append('extra_files_count', String(params.extra_files_count)); return fetchApi(`/api/codexlens/search?${queryParams.toString()}`); } /** * Perform file search using CodexLens */ export async function searchFilesCodexLens(params: CodexLensSearchParams): Promise { const queryParams = new URLSearchParams(); queryParams.append('query', params.query); if (params.limit) queryParams.append('limit', String(params.limit)); if (params.mode) queryParams.append('mode', params.mode); if (params.max_content_length) queryParams.append('max_content_length', String(params.max_content_length)); if (params.extra_files_count) queryParams.append('extra_files_count', String(params.extra_files_count)); return fetchApi(`/api/codexlens/search_files?${queryParams.toString()}`); } /** * Perform symbol search using CodexLens */ export async function searchSymbolCodexLens(params: Pick): Promise { const queryParams = new URLSearchParams(); queryParams.append('query', params.query); if (params.limit) queryParams.append('limit', String(params.limit)); return fetchApi(`/api/codexlens/symbol?${queryParams.toString()}`); } // ========== CodexLens LSP / Semantic Search API ========== /** * CodexLens LSP status response */ export interface CodexLensLspStatusResponse { available: boolean; semantic_available: boolean; vector_index: boolean; project_count?: number; embeddings?: Record; modes?: string[]; strategies?: string[]; error?: string; } /** * CodexLens semantic search params (Python API) */ export type CodexLensSemanticSearchMode = 'fusion' | 'vector' | 'structural'; export type CodexLensFusionStrategy = 'rrf' | 'staged' | 'binary' | 'hybrid' | 'dense_rerank'; export type CodexLensStagedStage2Mode = 'precomputed' | 'realtime' | 'static_global_graph'; export interface CodexLensSemanticSearchParams { query: string; path?: string; mode?: CodexLensSemanticSearchMode; fusion_strategy?: CodexLensFusionStrategy; staged_stage2_mode?: CodexLensStagedStage2Mode; vector_weight?: number; structural_weight?: number; keyword_weight?: number; kind_filter?: string[]; limit?: number; include_match_reason?: boolean; } /** * CodexLens semantic search result */ export interface CodexLensSemanticSearchResult { name?: string; kind?: string; file_path?: string; score?: number; match_reason?: string; range?: { start_line: number; end_line: number }; [key: string]: unknown; } /** * CodexLens semantic search response */ export interface CodexLensSemanticSearchResponse { success: boolean; results?: CodexLensSemanticSearchResult[]; query?: string; mode?: string; fusion_strategy?: string; count?: number; error?: string; } /** * Fetch CodexLens LSP status */ export async function fetchCodexLensLspStatus(): Promise { return fetchApi('/api/codexlens/lsp/status'); } /** * Start CodexLens LSP server */ export async function startCodexLensLsp(path?: string): Promise<{ success: boolean; message?: string; workspace_root?: string; error?: string }> { return fetchApi('/api/codexlens/lsp/start', { method: 'POST', body: JSON.stringify({ path }), }); } /** * Stop CodexLens LSP server */ export async function stopCodexLensLsp(path?: string): Promise<{ success: boolean; message?: string; error?: string }> { return fetchApi('/api/codexlens/lsp/stop', { method: 'POST', body: JSON.stringify({ path }), }); } /** * Restart CodexLens LSP server */ export async function restartCodexLensLsp(path?: string): Promise<{ success: boolean; message?: string; workspace_root?: string; error?: string }> { return fetchApi('/api/codexlens/lsp/restart', { method: 'POST', body: JSON.stringify({ path }), }); } /** * Perform semantic search using CodexLens Python API */ export async function semanticSearchCodexLens(params: CodexLensSemanticSearchParams): Promise { return fetchApi('/api/codexlens/lsp/search', { method: 'POST', body: JSON.stringify(params), }); } // ========== CodexLens Index Management API ========== /** * Index operation type */ export type CodexLensIndexOperation = 'fts_full' | 'fts_incremental' | 'vector_full' | 'vector_incremental'; /** * CodexLens index entry */ export interface CodexLensIndex { id: string; path: string; indexPath: string; size: number; sizeFormatted: string; fileCount: number; dirCount: number; hasVectorIndex: boolean; hasNormalIndex: boolean; status: string; lastModified: string | null; } /** * CodexLens index list response */ export interface CodexLensIndexesResponse { success: boolean; indexDir: string; indexes: CodexLensIndex[]; summary: { totalSize: number; totalSizeFormatted: string; vectorIndexCount: number; normalIndexCount: number; totalProjects?: number; totalFiles?: number; totalDirs?: number; indexSizeBytes?: number; indexSizeMb?: number; embeddings?: any; fullIndexDirSize?: number; fullIndexDirSizeFormatted?: string; }; error?: string; } /** * CodexLens index operation request */ export interface CodexLensIndexOperationRequest { path: string; operation: CodexLensIndexOperation; indexType?: 'normal' | 'vector'; embeddingModel?: string; embeddingBackend?: 'fastembed' | 'litellm'; maxWorkers?: number; } /** * CodexLens index operation response */ export interface CodexLensIndexOperationResponse { success: boolean; message?: string; error?: string; result?: any; output?: string; } /** * CodexLens indexing status response */ export interface CodexLensIndexingStatusResponse { success: boolean; inProgress: boolean; error?: string; } /** * Fetch all CodexLens indexes */ export async function fetchCodexLensIndexes(): Promise { return fetchApi('/api/codexlens/indexes'); } /** * Rebuild CodexLens index (full rebuild) * @param projectPath - Project path to index * @param options - Index options */ export async function rebuildCodexLensIndex( projectPath: string, options: { indexType?: 'normal' | 'vector'; embeddingModel?: string; embeddingBackend?: 'fastembed' | 'litellm'; maxWorkers?: number; } = {} ): Promise { return fetchApi('/api/codexlens/init', { method: 'POST', body: JSON.stringify({ path: projectPath, indexType: options.indexType || 'vector', embeddingModel: options.embeddingModel || 'code', embeddingBackend: options.embeddingBackend || 'fastembed', maxWorkers: options.maxWorkers || 1 }), }); } /** * Incremental update CodexLens index * @param projectPath - Project path to update * @param options - Index options */ export async function updateCodexLensIndex( projectPath: string, options: { indexType?: 'normal' | 'vector'; embeddingModel?: string; embeddingBackend?: 'fastembed' | 'litellm'; maxWorkers?: number; } = {} ): Promise { return fetchApi('/api/codexlens/update', { method: 'POST', body: JSON.stringify({ path: projectPath, indexType: options.indexType || 'vector', embeddingModel: options.embeddingModel || 'code', embeddingBackend: options.embeddingBackend || 'fastembed', maxWorkers: options.maxWorkers || 1 }), }); } /** * Cancel ongoing CodexLens indexing */ export async function cancelCodexLensIndexing(): Promise<{ success: boolean; error?: string }> { return fetchApi('/api/codexlens/cancel', { method: 'POST', body: JSON.stringify({}), }); } /** * Check if CodexLens indexing is in progress */ export async function checkCodexLensIndexingStatus(): Promise { return fetchApi('/api/codexlens/indexing-status'); } /** * Clean CodexLens indexes * @param options - Clean options */ export async function cleanCodexLensIndexes(options: { all?: boolean; path?: string; } = {}): Promise<{ success: boolean; message?: string; error?: string }> { return fetchApi('/api/codexlens/clean', { method: 'POST', body: JSON.stringify(options), }); } // ========== CodexLens File Watcher API ========== /** * CodexLens watcher status response */ export interface CodexLensWatcherStatusResponse { success: boolean; running: boolean; root_path: string; events_processed: number; start_time: string | null; uptime_seconds: number; } /** * Fetch CodexLens file watcher status */ export async function fetchCodexLensWatcherStatus(): Promise { return fetchApi('/api/codexlens/watch/status'); } /** * Start CodexLens file watcher */ export async function startCodexLensWatcher(path?: string, debounceMs?: number): Promise<{ success: boolean; message?: string; path?: string; pid?: number; error?: string }> { return fetchApi('/api/codexlens/watch/start', { method: 'POST', body: JSON.stringify({ path, debounce_ms: debounceMs }), }); } /** * Stop CodexLens file watcher */ export async function stopCodexLensWatcher(): Promise<{ success: boolean; message?: string; events_processed?: number; uptime_seconds?: number; error?: string }> { return fetchApi('/api/codexlens/watch/stop', { method: 'POST', body: JSON.stringify({}), }); } // ========== LiteLLM API Settings API ========== /** * Provider credential types */ export type ProviderType = 'openai' | 'anthropic' | 'custom'; /** * Advanced provider settings */ export interface ProviderAdvancedSettings { timeout?: number; maxRetries?: number; organization?: string; apiVersion?: string; customHeaders?: Record; rpm?: number; tpm?: number; proxy?: string; } /** * Routing strategy types */ export type RoutingStrategy = 'simple-shuffle' | 'weighted' | 'latency-based' | 'cost-based' | 'least-busy'; /** * Individual API key entry */ export interface ApiKeyEntry { id: string; key: string; label?: string; weight?: number; enabled: boolean; healthStatus?: 'healthy' | 'unhealthy' | 'unknown'; lastHealthCheck?: string; lastError?: string; lastLatencyMs?: number; } /** * Health check configuration */ export interface HealthCheckConfig { enabled: boolean; intervalSeconds: number; cooldownSeconds: number; failureThreshold: number; } /** * Model capabilities */ export interface ModelCapabilities { streaming?: boolean; functionCalling?: boolean; vision?: boolean; contextWindow?: number; embeddingDimension?: number; maxOutputTokens?: number; } /** * Model endpoint settings */ export interface ModelEndpointSettings { baseUrl?: string; timeout?: number; maxRetries?: number; customHeaders?: Record; cacheStrategy?: CacheStrategy; } /** * Model definition */ export interface ModelDefinition { id: string; name: string; type: 'llm' | 'embedding' | 'reranker'; series: string; enabled: boolean; capabilities?: ModelCapabilities; endpointSettings?: ModelEndpointSettings; description?: string; createdAt: string; updatedAt: string; } /** * Provider credential */ export interface ProviderCredential { id: string; name: string; type: ProviderType; apiKey: string; apiBase?: string; enabled: boolean; advancedSettings?: ProviderAdvancedSettings; apiKeys?: ApiKeyEntry[]; routingStrategy?: RoutingStrategy; healthCheck?: HealthCheckConfig; llmModels?: ModelDefinition[]; embeddingModels?: ModelDefinition[]; rerankerModels?: ModelDefinition[]; createdAt: string; updatedAt: string; } /** * Cache strategy */ export interface CacheStrategy { enabled: boolean; ttlMinutes: number; maxSizeKB: number; filePatterns: string[]; } /** * Custom endpoint */ export interface CustomEndpoint { id: string; name: string; providerId: string; model: string; description?: string; cacheStrategy: CacheStrategy; enabled: boolean; createdAt: string; updatedAt: string; } /** * Global cache settings */ export interface GlobalCacheSettings { enabled: boolean; cacheDir: string; maxTotalSizeMB: number; } /** * Cache statistics */ export interface CacheStats { totalSize: number; maxSize: number; entries: number; } /** * Model pool type */ export type ModelPoolType = 'embedding' | 'llm' | 'reranker'; /** * Model pool config */ export interface ModelPoolConfig { id: string; modelType: ModelPoolType; enabled: boolean; targetModel: string; strategy: 'round_robin' | 'latency_aware' | 'weighted_random'; autoDiscover: boolean; excludedProviderIds?: string[]; defaultCooldown: number; defaultMaxConcurrentPerKey: number; name?: string; description?: string; } /** * Provider for model pool discovery */ export interface DiscoveredProvider { providerId: string; providerName: string; models: string[]; } /** * CLI settings mode */ export type CliSettingsMode = 'provider-based' | 'direct'; /** * CLI settings */ export interface CliSettings { id: string; name: string; description?: string; enabled: boolean; mode: CliSettingsMode; providerId?: string; settings?: Record; createdAt: string; updatedAt: string; } // ========== Provider Management ========== /** * Fetch all providers */ export async function fetchProviders(): Promise<{ providers: ProviderCredential[]; count: number }> { return fetchApi('/api/litellm-api/providers'); } /** * Create provider */ export async function createProvider(provider: Omit): Promise<{ success: boolean; provider: ProviderCredential }> { return fetchApi('/api/litellm-api/providers', { method: 'POST', body: JSON.stringify(provider), }); } /** * Update provider */ export async function updateProvider(providerId: string, updates: Partial>): Promise<{ success: boolean; provider: ProviderCredential }> { return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}`, { method: 'PUT', body: JSON.stringify(updates), }); } /** * Delete provider */ export async function deleteProvider(providerId: string): Promise<{ success: boolean; message: string }> { return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}`, { method: 'DELETE', }); } /** * Test provider connection */ export async function testProvider(providerId: string): Promise<{ success: boolean; provider: string; latencyMs?: number; error?: string }> { return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/test`, { method: 'POST', }); } /** * Test specific API key */ export async function testProviderKey(providerId: string, keyId: string): Promise<{ valid: boolean; error?: string; latencyMs?: number; keyLabel?: string }> { return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/test-key`, { method: 'POST', body: JSON.stringify({ keyId }), }); } /** * Get provider health status */ export async function getProviderHealthStatus(providerId: string): Promise<{ providerId: string; providerName: string; keys: Array<{ keyId: string; label: string; status: string; lastCheck?: string; lastLatencyMs?: number; consecutiveFailures?: number; inCooldown?: boolean; lastError?: string }> }> { return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/health-status`); } /** * Trigger health check now */ export async function triggerProviderHealthCheck(providerId: string): Promise<{ success: boolean; providerId: string; providerName?: string; keys: Array; checkedAt: string }> { return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/health-check-now`, { method: 'POST', }); } // ========== Endpoint Management ========== /** * Fetch all endpoints */ export async function fetchEndpoints(): Promise<{ endpoints: CustomEndpoint[]; count: number }> { return fetchApi('/api/litellm-api/endpoints'); } /** * Create endpoint */ export async function createEndpoint(endpoint: Omit): Promise<{ success: boolean; endpoint: CustomEndpoint }> { return fetchApi('/api/litellm-api/endpoints', { method: 'POST', body: JSON.stringify(endpoint), }); } /** * Update endpoint */ export async function updateEndpoint(endpointId: string, updates: Partial>): Promise<{ success: boolean; endpoint: CustomEndpoint }> { return fetchApi(`/api/litellm-api/endpoints/${encodeURIComponent(endpointId)}`, { method: 'PUT', body: JSON.stringify(updates), }); } /** * Delete endpoint */ export async function deleteEndpoint(endpointId: string): Promise<{ success: boolean; message: string }> { return fetchApi(`/api/litellm-api/endpoints/${encodeURIComponent(endpointId)}`, { method: 'DELETE', }); } // ========== Model Discovery ========== /** * Get available models for provider type */ export async function getProviderModels(providerType: string): Promise<{ providerType: string; models: Array<{ id: string; name: string; provider: string; description?: string }>; count: number }> { return fetchApi(`/api/litellm-api/models/${encodeURIComponent(providerType)}`); } // ========== Cache Management ========== /** * Fetch cache statistics */ export async function fetchCacheStats(): Promise { return fetchApi('/api/litellm-api/cache/stats'); } /** * Clear cache */ export async function clearCache(): Promise<{ success: boolean; removed: number }> { return fetchApi('/api/litellm-api/cache/clear', { method: 'POST', }); } /** * Update cache settings */ export async function updateCacheSettings(settings: Partial<{ enabled: boolean; cacheDir: string; maxTotalSizeMB: number }>): Promise<{ success: boolean; settings: GlobalCacheSettings }> { return fetchApi('/api/litellm-api/config/cache', { method: 'PUT', body: JSON.stringify(settings), }); } // ========== Model Pool Management ========== /** * Fetch all model pools */ export async function fetchModelPools(): Promise<{ pools: ModelPoolConfig[] }> { return fetchApi('/api/litellm-api/model-pools'); } /** * Fetch single model pool */ export async function fetchModelPool(poolId: string): Promise<{ pool: ModelPoolConfig }> { return fetchApi(`/api/litellm-api/model-pools/${encodeURIComponent(poolId)}`); } /** * Create model pool */ export async function createModelPool(pool: Omit): Promise<{ success: boolean; poolId: string; syncResult?: any }> { return fetchApi('/api/litellm-api/model-pools', { method: 'POST', body: JSON.stringify(pool), }); } /** * Update model pool */ export async function updateModelPool(poolId: string, updates: Partial): Promise<{ success: boolean; poolId?: string; syncResult?: any }> { return fetchApi(`/api/litellm-api/model-pools/${encodeURIComponent(poolId)}`, { method: 'PUT', body: JSON.stringify(updates), }); } /** * Delete model pool */ export async function deleteModelPool(poolId: string): Promise<{ success: boolean; syncResult?: any }> { return fetchApi(`/api/litellm-api/model-pools/${encodeURIComponent(poolId)}`, { method: 'DELETE', }); } /** * Get available models for pool type */ export async function getAvailableModelsForPool(modelType: ModelPoolType): Promise<{ availableModels: Array<{ modelId: string; modelName: string; providers: string[] }> }> { return fetchApi(`/api/litellm-api/model-pools/available-models/${encodeURIComponent(modelType)}`); } /** * Discover providers for model */ export async function discoverModelsForPool(modelType: ModelPoolType, targetModel: string): Promise<{ modelType: string; targetModel: string; discovered: DiscoveredProvider[]; count: number }> { return fetchApi(`/api/litellm-api/model-pools/discover/${encodeURIComponent(modelType)}/${encodeURIComponent(targetModel)}`); } // ========== Config Management ========== /** * Get full config */ export async function fetchApiConfig(): Promise { return fetchApi('/api/litellm-api/config'); } /** * Sync config to YAML */ export async function syncApiConfig(): Promise<{ success: boolean; message: string; yamlPath?: string }> { return fetchApi('/api/litellm-api/config/sync', { method: 'POST', }); } /** * Preview YAML config */ export async function previewYamlConfig(): Promise<{ success: boolean; config: string }> { return fetchApi('/api/litellm-api/config/yaml-preview'); } // ========== CCW-LiteLLM Package Management ========== export interface CcwLitellmEnvCheck { python: string; installed: boolean; version?: string; error?: string; } export interface CcwLitellmStatus { /** * Whether ccw-litellm is installed in the CodexLens venv. * This is the environment used for the LiteLLM embedding backend. */ installed: boolean; version?: string; error?: string; checks?: { codexLensVenv: CcwLitellmEnvCheck; systemPython?: CcwLitellmEnvCheck; }; } /** * Check ccw-litellm status */ export async function checkCcwLitellmStatus(refresh = false): Promise { return fetchApi(`/api/litellm-api/ccw-litellm/status${refresh ? '?refresh=true' : ''}`); } /** * Install ccw-litellm */ export async function installCcwLitellm(): Promise<{ success: boolean; message?: string; error?: string; path?: string }> { return fetchApi('/api/litellm-api/ccw-litellm/install', { method: 'POST', }); } /** * Uninstall ccw-litellm */ export async function uninstallCcwLitellm(): Promise<{ success: boolean; message?: string; error?: string }> { return fetchApi('/api/litellm-api/ccw-litellm/uninstall', { method: 'POST', }); } // ========== CLI Settings Management ========== /** * CLI Settings (Claude CLI endpoint configuration) * Maps to backend EndpointSettings from /api/cli/settings */ /** * CLI Provider type */ export type CliProvider = 'claude' | 'codex' | 'gemini'; /** * Base settings fields shared across all providers */ export interface CliSettingsBase { env: Record; model?: string; tags?: string[]; availableModels?: string[]; } /** * Claude-specific settings */ export interface ClaudeCliSettingsApi extends CliSettingsBase { settingsFile?: string; } /** * Codex-specific settings */ export interface CodexCliSettingsApi extends CliSettingsBase { profile?: string; authJson?: string; configToml?: string; } /** * Gemini-specific settings */ export interface GeminiCliSettingsApi extends CliSettingsBase { } export interface CliSettingsEndpoint { id: string; name: string; description?: string; /** CLI provider type (defaults to 'claude' for backward compat) */ provider: CliProvider; settings: ClaudeCliSettingsApi | CodexCliSettingsApi | GeminiCliSettingsApi; enabled: boolean; createdAt: string; updatedAt: string; } /** * CLI Settings list response */ export interface CliSettingsListResponse { endpoints: CliSettingsEndpoint[]; total: number; } /** * Save CLI Settings request */ export interface SaveCliSettingsRequest { id?: string; name: string; description?: string; /** CLI provider type */ provider?: CliProvider; settings: ClaudeCliSettingsApi | CodexCliSettingsApi | GeminiCliSettingsApi; enabled?: boolean; } /** * Fetch all CLI settings endpoints */ export async function fetchCliSettings(): Promise { return fetchApi('/api/cli/settings'); } /** * Fetch single CLI settings endpoint */ export async function fetchCliSettingsEndpoint(endpointId: string): Promise<{ endpoint: CliSettingsEndpoint; filePath?: string }> { return fetchApi(`/api/cli/settings/${encodeURIComponent(endpointId)}`); } /** * Create CLI settings endpoint */ export async function createCliSettings(request: SaveCliSettingsRequest): Promise<{ success: boolean; endpoint?: CliSettingsEndpoint; filePath?: string; message?: string }> { return fetchApi('/api/cli/settings', { method: 'POST', body: JSON.stringify(request), }); } /** * Update CLI settings endpoint */ export async function updateCliSettings(endpointId: string, request: Partial): Promise<{ success: boolean; endpoint?: CliSettingsEndpoint; message?: string }> { return fetchApi(`/api/cli/settings/${encodeURIComponent(endpointId)}`, { method: 'PUT', body: JSON.stringify(request), }); } /** * Delete CLI settings endpoint */ export async function deleteCliSettings(endpointId: string): Promise<{ success: boolean; message?: string }> { return fetchApi(`/api/cli/settings/${encodeURIComponent(endpointId)}`, { method: 'DELETE', }); } /** * Toggle CLI settings enabled status */ export async function toggleCliSettingsEnabled(endpointId: string, enabled: boolean): Promise<{ success: boolean; message?: string }> { return fetchApi(`/api/cli/settings/${encodeURIComponent(endpointId)}`, { method: 'PUT', body: JSON.stringify({ enabled }), }); } /** * Get CLI settings file path */ export async function getCliSettingsPath(endpointId: string): Promise<{ endpointId: string; filePath: string; enabled: boolean }> { return fetchApi(`/api/cli/settings/${encodeURIComponent(endpointId)}/path`); } // ========== CLI Config Preview API ========== /** * Codex config preview response */ export interface CodexConfigPreviewResponse { /** Whether preview was successful */ success: boolean; /** Path to config.toml */ configPath: string; /** Path to auth.json */ authPath: string; /** config.toml content with sensitive values masked */ configToml: string | null; /** auth.json content with API keys masked */ authJson: string | null; /** Error messages if any files could not be read */ errors?: string[]; } /** * Gemini config preview response */ export interface GeminiConfigPreviewResponse { /** Whether preview was successful */ success: boolean; /** Path to settings.json */ settingsPath: string; /** settings.json content with sensitive values masked */ settingsJson: string | null; /** Error messages if file could not be read */ errors?: string[]; } /** * Fetch Codex config files preview (config.toml and auth.json) */ export async function fetchCodexConfigPreview(): Promise { return fetchApi('/api/cli/settings/codex/preview'); } /** * Fetch Gemini settings file preview (settings.json) */ export async function fetchGeminiConfigPreview(): Promise { return fetchApi('/api/cli/settings/gemini/preview'); } // ========== Orchestrator Execution Monitoring API ========== /** * Execution state response from orchestrator */ export interface ExecutionStateResponse { execId: string; flowId: string; status: 'pending' | 'running' | 'paused' | 'completed' | 'failed'; currentNodeId?: string; startedAt: string; completedAt?: string; elapsedMs: number; } /** * Coordinator pipeline details response */ export interface CoordinatorPipelineDetails { id: string; name: string; description?: string; nodes: Array<{ id: string; name: string; description?: string; command: string; status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped'; startedAt?: string; completedAt?: string; result?: unknown; error?: string; output?: string; parentId?: string; children?: Array; }>; totalSteps: number; estimatedDuration?: number; logs?: Array<{ id: string; timestamp: string; level: 'info' | 'warn' | 'error' | 'debug' | 'success'; message: string; nodeId?: string; source?: 'system' | 'node' | 'user'; }>; status: 'idle' | 'initializing' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled'; createdAt: string; } /** * Execution log entry */ export interface ExecutionLogEntry { timestamp: string; level: 'info' | 'warn' | 'error' | 'debug'; nodeId?: string; message: string; } /** * Execution logs response */ export interface ExecutionLogsResponse { execId: string; logs: ExecutionLogEntry[]; total: number; limit: number; offset: number; hasMore: boolean; } // ========== Orchestrator Flow API (Create/Execute) ========== export interface OrchestratorFlowDto { id: string; name: string; description?: string; version: string; created_at: string; updated_at: string; nodes: Array>; edges: Array>; variables: Record; metadata: Record; } export interface CreateOrchestratorFlowRequest { name: string; description?: string; version?: string; nodes?: Array>; edges?: Array>; variables?: Record; metadata?: Record; } export async function createOrchestratorFlow( request: CreateOrchestratorFlowRequest, projectPath?: string ): Promise<{ success: boolean; data: OrchestratorFlowDto }> { return fetchApi(withPath('/api/orchestrator/flows', projectPath), { method: 'POST', body: JSON.stringify(request), }); } export async function executeOrchestratorFlow( flowId: string, request?: { variables?: Record }, projectPath?: string ): Promise<{ success: boolean; data: { execId: string; flowId: string; status: string; startedAt: string } }> { return fetchApi(withPath(`/api/orchestrator/flows/${encodeURIComponent(flowId)}/execute`, projectPath), { method: 'POST', body: JSON.stringify(request ?? {}), }); } /** * Fetch execution state by execId * @param execId - Execution ID */ export async function fetchExecutionState(execId: string): Promise<{ success: boolean; data: ExecutionStateResponse }> { return fetchApi(`/api/orchestrator/executions/${encodeURIComponent(execId)}`); } /** * Fetch coordinator pipeline details by execution ID * @param execId - Execution/Pipeline ID */ export async function fetchCoordinatorPipeline(execId: string): Promise<{ success: boolean; data: CoordinatorPipelineDetails }> { return fetchApi(`/api/coordinator/pipeline/${encodeURIComponent(execId)}`); } /** * Fetch execution logs with pagination and filtering * @param execId - Execution ID * @param options - Query options */ export async function fetchExecutionLogs( execId: string, options?: { limit?: number; offset?: number; level?: 'info' | 'warn' | 'error' | 'debug'; nodeId?: string; } ): Promise<{ success: boolean; data: ExecutionLogsResponse }> { const params = new URLSearchParams(); if (options?.limit) params.append('limit', String(options.limit)); if (options?.offset) params.append('offset', String(options.offset)); if (options?.level) params.append('level', options.level); if (options?.nodeId) params.append('nodeId', options.nodeId); const queryString = params.toString(); return fetchApi(`/api/orchestrator/executions/${encodeURIComponent(execId)}/logs${queryString ? `?${queryString}` : ''}`); } // ========== System Settings API ========== /** * Chinese response setting status */ export interface ChineseResponseStatus { enabled: boolean; claudeEnabled: boolean; codexEnabled: boolean; codexNeedsMigration: boolean; guidelinesPath: string; guidelinesExists: boolean; userClaudeMdExists: boolean; userCodexAgentsExists: boolean; } /** * Fetch Chinese response setting status */ export async function fetchChineseResponseStatus(): Promise { return fetchApi('/api/language/chinese-response'); } /** * Toggle Chinese response setting */ export async function toggleChineseResponse( enabled: boolean, target: 'claude' | 'codex' = 'claude' ): Promise<{ success: boolean; enabled: boolean; target: string }> { return fetchApi('/api/language/chinese-response', { method: 'POST', body: JSON.stringify({ enabled, target }), }); } /** * Windows platform setting status */ export interface WindowsPlatformStatus { enabled: boolean; guidelinesPath: string; guidelinesExists: boolean; userClaudeMdExists: boolean; } /** * Fetch Windows platform setting status */ export async function fetchWindowsPlatformStatus(): Promise { return fetchApi('/api/language/windows-platform'); } /** * Toggle Windows platform setting */ export async function toggleWindowsPlatform( enabled: boolean ): Promise<{ success: boolean; enabled: boolean }> { return fetchApi('/api/language/windows-platform', { method: 'POST', body: JSON.stringify({ enabled }), }); } /** * Codex CLI Enhancement setting status */ export interface CodexCliEnhancementStatus { enabled: boolean; guidelinesPath: string; guidelinesExists: boolean; userCodexAgentsExists: boolean; } /** * Fetch Codex CLI Enhancement setting status */ export async function fetchCodexCliEnhancementStatus(): Promise { return fetchApi('/api/language/codex-cli-enhancement'); } /** * Toggle Codex CLI Enhancement setting */ export async function toggleCodexCliEnhancement( enabled: boolean ): Promise<{ success: boolean; enabled: boolean }> { return fetchApi('/api/language/codex-cli-enhancement', { method: 'POST', body: JSON.stringify({ enabled }), }); } /** * Refresh Codex CLI Enhancement content */ export async function refreshCodexCliEnhancement(): Promise<{ success: boolean; refreshed: boolean }> { return fetchApi('/api/language/codex-cli-enhancement', { method: 'POST', body: JSON.stringify({ action: 'refresh' }), }); } /** * CCW Install status */ export interface CcwInstallStatus { installed: boolean; workflowsInstalled: boolean; missingFiles: string[]; installPath: string; } /** * Aggregated status response */ export interface AggregatedStatus { cli: Record; codexLens: { ready: boolean }; semantic: { available: boolean; backend: string | null }; ccwInstall: CcwInstallStatus; timestamp: string; } /** * Fetch aggregated system status (includes CCW install status) */ export async function fetchAggregatedStatus(): Promise { return fetchApi('/api/status/all'); } /** * Fetch CLI tool availability status */ export async function fetchCliToolStatus(): Promise> { return fetchApi('/api/cli/status'); } /** * CCW Installation manifest */ export interface CcwInstallationManifest { manifest_id: string; version: string; installation_mode: string; installation_path: string; installation_date: string; installer_version: string; manifest_file: string; application_version: string; files_count: number; directories_count: number; } /** * Fetch CCW installation manifests */ export async function fetchCcwInstallations(): Promise<{ installations: CcwInstallationManifest[] }> { return fetchApi('/api/ccw/installations'); } /** * Upgrade CCW installation */ export async function upgradeCcwInstallation( path?: string ): Promise<{ success: boolean; message?: string; error?: string; output?: string }> { return fetchApi('/api/ccw/upgrade', { method: 'POST', body: JSON.stringify({ path }), }); } // ========== CLI Settings Export/Import API ========== /** * Exported settings structure from backend */ export interface ExportedSettings { version: string; exportedAt: string; settings: { cliTools?: Record; chineseResponse?: { claudeEnabled: boolean; codexEnabled: boolean; }; windowsPlatform?: { enabled: boolean; }; codexCliEnhancement?: { enabled: boolean; }; }; } /** * Import options for settings import */ export interface ImportOptions { overwrite?: boolean; dryRun?: boolean; } /** * Import result from backend */ export interface ImportResult { success: boolean; imported: number; skipped: number; errors: string[]; importedIds: string[]; } /** * Export CLI settings to JSON file */ export async function exportSettings(): Promise { return fetchApi('/api/cli/settings/export'); } /** * Import CLI settings from JSON data */ export async function importSettings( data: ExportedSettings, options?: ImportOptions ): Promise { return fetchApi('/api/cli/settings/import', { method: 'POST', body: JSON.stringify({ data, options }), }); } // ========== CCW Tools API ========== /** * CCW tool info returned by /api/ccw/tools */ export interface CcwToolInfo { name: string; description: string; parameters?: Record; } /** * Fetch all registered CCW tools */ export async function fetchCcwTools(): Promise { const data = await fetchApi<{ tools: CcwToolInfo[] }>('/api/ccw/tools'); return data.tools; } // ========== Team API ========== export async function fetchTeams(location?: string): Promise<{ teams: Array<{ name: string; messageCount: number; lastActivity: string; status: string; created_at: string; updated_at: string; archived_at?: string; pipeline_mode?: string; memberCount: number; members?: string[] }> }> { const params = new URLSearchParams(); if (location) params.set('location', location); const qs = params.toString(); return fetchApi(`/api/teams${qs ? `?${qs}` : ''}`); } export async function archiveTeam(teamName: string): Promise<{ success: boolean; team: string; status: string }> { return fetchApi(`/api/teams/${encodeURIComponent(teamName)}/archive`, { method: 'POST' }); } export async function unarchiveTeam(teamName: string): Promise<{ success: boolean; team: string; status: string }> { return fetchApi(`/api/teams/${encodeURIComponent(teamName)}/unarchive`, { method: 'POST' }); } export async function deleteTeam(teamName: string): Promise { return fetchApi(`/api/teams/${encodeURIComponent(teamName)}`, { method: 'DELETE' }); } export async function fetchTeamMessages( teamName: string, params?: { from?: string; to?: string; type?: string; last?: number; offset?: number } ): Promise<{ total: number; showing: number; messages: Array> }> { const searchParams = new URLSearchParams(); if (params?.from) searchParams.set('from', params.from); if (params?.to) searchParams.set('to', params.to); if (params?.type) searchParams.set('type', params.type); if (params?.last) searchParams.set('last', String(params.last)); if (params?.offset) searchParams.set('offset', String(params.offset)); const qs = searchParams.toString(); return fetchApi(`/api/teams/${encodeURIComponent(teamName)}/messages${qs ? `?${qs}` : ''}`); } export async function fetchTeamStatus( teamName: string ): Promise<{ members: Array<{ member: string; lastSeen: string; lastAction: string; messageCount: number }>; total_messages: number }> { return fetchApi(`/api/teams/${encodeURIComponent(teamName)}/status`); } export async function fetchTeamArtifacts( teamName: string ): Promise { return fetchApi(`/api/teams/${encodeURIComponent(teamName)}/artifacts`); } export async function fetchArtifactContent( teamName: string, artifactPath: string ): Promise<{ content: string; contentType: string; path: string }> { return fetchApi(`/api/teams/${encodeURIComponent(teamName)}/artifacts/${encodeURIComponent(artifactPath)}`); } // ========== CLI Sessions (PTY) API ========== export interface CliSession { sessionKey: string; shellKind: string; workingDir: string; tool?: string; model?: string; resumeKey?: string; createdAt: string; updatedAt: string; isPaused: boolean; /** When set, this session is a native CLI interactive process. */ cliTool?: string; } export interface CreateCliSessionInput { workingDir?: string; cols?: number; rows?: number; /** Shell to use for spawning CLI tools on Windows. */ preferredShell?: 'bash' | 'pwsh' | 'cmd'; tool?: string; model?: string; resumeKey?: string; /** Launch mode for native CLI sessions (default or yolo). */ launchMode?: 'default' | 'yolo'; /** Settings endpoint ID for injecting env vars and settings into CLI process. */ settingsEndpointId?: string; } function withPath(url: string, projectPath?: string): string { if (!projectPath) return url; const sep = url.includes('?') ? '&' : '?'; return `${url}${sep}path=${encodeURIComponent(projectPath)}`; } export async function fetchCliSessions(projectPath?: string): Promise<{ sessions: CliSession[] }> { return fetchApi<{ sessions: CliSession[] }>(withPath('/api/cli-sessions', projectPath)); } export async function createCliSession( input: CreateCliSessionInput, projectPath?: string ): Promise<{ success: boolean; session: CliSession }> { return fetchApi<{ success: boolean; session: CliSession }>(withPath('/api/cli-sessions', projectPath), { method: 'POST', body: JSON.stringify(input), }); } export async function fetchCliSessionBuffer( sessionKey: string, projectPath?: string ): Promise<{ session: CliSession; buffer: string }> { return fetchApi<{ session: CliSession; buffer: string }>( withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/buffer`, projectPath) ); } export async function sendCliSessionText( sessionKey: string, input: { text: string; appendNewline?: boolean }, projectPath?: string ): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/send`, projectPath), { method: 'POST', body: JSON.stringify(input), }); } export interface ExecuteInCliSessionInput { tool: string; prompt: string; mode?: 'analysis' | 'write' | 'auto'; model?: string; workingDir?: string; category?: 'user' | 'internal' | 'insight'; resumeKey?: string; resumeStrategy?: 'nativeResume' | 'promptConcat'; instructionType?: 'prompt' | 'skill' | 'command'; skillName?: string; } export async function executeInCliSession( sessionKey: string, input: ExecuteInCliSessionInput, projectPath?: string ): Promise<{ success: boolean; executionId: string; command: string }> { return fetchApi<{ success: boolean; executionId: string; command: string }>( withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/execute`, projectPath), { method: 'POST', body: JSON.stringify(input) } ); } export async function resizeCliSession( sessionKey: string, input: { cols: number; rows: number }, projectPath?: string ): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/resize`, projectPath), { method: 'POST', body: JSON.stringify(input), }); } export async function closeCliSession(sessionKey: string, projectPath?: string): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/close`, projectPath), { method: 'POST', body: JSON.stringify({}), }); } export async function pauseCliSession(sessionKey: string, projectPath?: string): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/pause`, projectPath), { method: 'POST', body: JSON.stringify({}), }); } export async function resumeCliSession(sessionKey: string, projectPath?: string): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/resume`, projectPath), { method: 'POST', body: JSON.stringify({}), }); } export async function createCliSessionShareToken( sessionKey: string, input: { mode?: 'read' | 'write'; ttlMs?: number }, projectPath?: string ): Promise<{ success: boolean; shareToken: string; expiresAt: string; mode: 'read' | 'write' }> { return fetchApi<{ success: boolean; shareToken: string; expiresAt: string; mode: 'read' | 'write' }>( withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/share`, projectPath), { method: 'POST', body: JSON.stringify(input) } ); } export async function fetchCliSessionShares( sessionKey: string, projectPath?: string ): Promise<{ shares: Array<{ shareToken: string; expiresAt: string; mode: 'read' | 'write' }> }> { return fetchApi<{ shares: Array<{ shareToken: string; expiresAt: string; mode: 'read' | 'write' }> }>( withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/shares`, projectPath) ); } export async function revokeCliSessionShareToken( sessionKey: string, input: { shareToken: string }, projectPath?: string ): Promise<{ success: boolean; revoked: boolean }> { return fetchApi<{ success: boolean; revoked: boolean }>( withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/share/revoke`, projectPath), { method: 'POST', body: JSON.stringify(input) } ); } // ========== Audit (Observability) API ========== export type CliSessionAuditEventType = | 'session_created' | 'session_closed' | 'session_send' | 'session_execute' | 'session_resize' | 'session_share_created' | 'session_share_revoked' | 'session_idle_reaped'; export interface CliSessionAuditEvent { type: CliSessionAuditEventType; timestamp: string; projectRoot: string; sessionKey?: string; tool?: string; resumeKey?: string; workingDir?: string; ip?: string; userAgent?: string; details?: Record; } export interface CliSessionAuditListResponse { events: CliSessionAuditEvent[]; total: number; limit: number; offset: number; hasMore: boolean; } export async function fetchCliSessionAudit( options?: { projectPath?: string; sessionKey?: string; type?: CliSessionAuditEventType | CliSessionAuditEventType[]; q?: string; limit?: number; offset?: number; } ): Promise<{ success: boolean; data: CliSessionAuditListResponse }> { const params = new URLSearchParams(); if (options?.sessionKey) params.set('sessionKey', options.sessionKey); if (options?.q) params.set('q', options.q); if (typeof options?.limit === 'number') params.set('limit', String(options.limit)); if (typeof options?.offset === 'number') params.set('offset', String(options.offset)); if (options?.type) { const types = Array.isArray(options.type) ? options.type : [options.type]; params.set('type', types.join(',')); } const queryString = params.toString(); return fetchApi<{ success: boolean; data: CliSessionAuditListResponse }>( withPath(`/api/audit/cli-sessions${queryString ? `?${queryString}` : ''}`, options?.projectPath) ); } // ========== Unified Memory API ========== export interface UnifiedSearchResult { source_id: string; source_type: string; score: number; content: string; category: string; rank_sources: { vector_rank?: number; vector_score?: number; fts_rank?: number; heat_score?: number; }; } export interface UnifiedSearchResponse { success: boolean; query: string; total: number; results: UnifiedSearchResult[]; } export interface UnifiedMemoryStats { core_memories: { total: number; archived: number; }; stage1_outputs: number; entities: number; prompts: number; conversations: number; vector_index: { available: boolean; total_chunks: number; hnsw_available: boolean; hnsw_count: number; dimension: number; categories?: Record; }; } export interface RecommendationResult { source_id: string; source_type: string; score: number; content: string; category: string; } export interface ReindexResponse { success: boolean; hnsw_count?: number; elapsed_time?: number; error?: string; } /** * Search unified memory using vector + FTS5 fusion (RRF) * @param query - Search query text * @param options - Search options (topK, minScore, category) * @param projectPath - Optional project path for workspace isolation */ export async function fetchUnifiedSearch( query: string, options?: { topK?: number; minScore?: number; category?: string; }, projectPath?: string ): Promise { const params = new URLSearchParams(); params.set('q', query); if (options?.topK) params.set('topK', String(options.topK)); if (options?.minScore) params.set('minScore', String(options.minScore)); if (options?.category) params.set('category', options.category); const data = await fetchApi( withPath(`/api/unified-memory/search?${params.toString()}`, projectPath) ); if (data.success === false) { throw new Error(data.error || 'Search failed'); } return data; } /** * Fetch unified memory statistics (core memories, entities, vectors, etc.) * @param projectPath - Optional project path for workspace isolation */ export async function fetchUnifiedStats( projectPath?: string ): Promise<{ success: boolean; stats: UnifiedMemoryStats }> { const data = await fetchApi<{ success: boolean; stats: UnifiedMemoryStats; error?: string }>( withPath('/api/unified-memory/stats', projectPath) ); if (data.success === false) { throw new Error(data.error || 'Failed to load unified stats'); } return data; } /** * Get KNN-based recommendations for a specific memory * @param memoryId - Core memory ID (CMEM-*) * @param limit - Number of recommendations (default: 5) * @param projectPath - Optional project path for workspace isolation */ export async function fetchRecommendations( memoryId: string, limit?: number, projectPath?: string ): Promise<{ success: boolean; memory_id: string; total: number; recommendations: RecommendationResult[] }> { const params = new URLSearchParams(); if (limit) params.set('limit', String(limit)); const queryString = params.toString(); const data = await fetchApi<{ success: boolean; memory_id: string; total: number; recommendations: RecommendationResult[]; error?: string }>( withPath( `/api/unified-memory/recommendations/${encodeURIComponent(memoryId)}${queryString ? `?${queryString}` : ''}`, projectPath ) ); if (data.success === false) { throw new Error(data.error || 'Failed to load recommendations'); } return data; } /** * Trigger vector index rebuild * @param projectPath - Optional project path for workspace isolation */ export async function triggerReindex( projectPath?: string ): Promise { return fetchApi( '/api/unified-memory/reindex', { method: 'POST', body: JSON.stringify({ path: projectPath }), } ); } // ========== Analysis API ========== import type { AnalysisSessionSummary, AnalysisSessionDetail } from '../types/analysis'; /** * Fetch list of analysis sessions */ export async function fetchAnalysisSessions( projectPath?: string, options?: { limit?: number; offset?: number } ): Promise { const params = new URLSearchParams(); if (options?.limit) params.set('limit', String(options.limit)); if (options?.offset) params.set('offset', String(options.offset)); const queryString = params.toString(); const path = queryString ? `${withPath('/api/analysis', projectPath)}&${queryString}` : withPath('/api/analysis', projectPath); const data = await fetchApi<{ success: boolean; data: AnalysisSessionSummary[]; error?: string }>(path); if (!data.success) { throw new Error(data.error || 'Failed to fetch analysis sessions'); } return data.data; } /** * Fetch analysis session detail */ export async function fetchAnalysisDetail( sessionId: string, projectPath?: string ): Promise { const data = await fetchApi<{ success: boolean; data: AnalysisSessionDetail; error?: string }>( withPath(`/api/analysis/${encodeURIComponent(sessionId)}`, projectPath) ); if (!data.success) { throw new Error(data.error || 'Failed to fetch analysis detail'); } return data.data; }