mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
- Added A2UITypes for defining question structures and answers. - Created A2UIWebSocketHandler for managing WebSocket connections and message handling. - Developed ask-question tool for interactive user questions via A2UI. - Introduced platformUtils for platform detection and shell command handling. - Centralized TypeScript types in index.ts for better organization. - Implemented compatibility checks for hook templates based on platform requirements.
1929 lines
47 KiB
TypeScript
1929 lines
47 KiB
TypeScript
// ========================================
|
|
// 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 } from '../types/store';
|
|
|
|
// Re-export types for backward compatibility
|
|
export type { IndexStatus, IndexRebuildRequest, Rule, RuleCreateInput, RulesResponse, Prompt, PromptInsight, Pattern, Suggestion };
|
|
|
|
|
|
/**
|
|
* 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 ==========
|
|
|
|
/**
|
|
* Get CSRF token from cookie
|
|
*/
|
|
function getCsrfToken(): string | null {
|
|
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
}
|
|
|
|
// ========== Base Fetch Wrapper ==========
|
|
|
|
/**
|
|
* Base fetch wrapper with CSRF token and error handling
|
|
*/
|
|
async function fetchApi<T>(
|
|
url: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const headers = new Headers(options.headers);
|
|
|
|
// Add CSRF token for mutating requests
|
|
if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) {
|
|
const csrfToken = getCsrfToken();
|
|
if (csrfToken) {
|
|
headers.set('X-CSRF-Token', csrfToken);
|
|
}
|
|
}
|
|
|
|
// 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',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error: ApiError = {
|
|
message: response.statusText || 'Request failed',
|
|
status: response.status,
|
|
};
|
|
|
|
try {
|
|
const body = await response.json();
|
|
if (body.message) error.message = body.message;
|
|
if (body.code) error.code = body.code;
|
|
} catch (parseError) {
|
|
// Log parse errors instead of silently ignoring
|
|
console.warn('[API] Failed to parse error response:', parseError);
|
|
}
|
|
|
|
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 ==========
|
|
|
|
/**
|
|
* 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<string, SessionMetadata['status']> = {
|
|
'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();
|
|
}
|
|
|
|
return {
|
|
session_id: backendSession.session_id,
|
|
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: (backendSession as unknown as { has_review?: boolean }).has_review,
|
|
review: (backendSession as unknown as { review?: SessionMetadata['review'] }).review,
|
|
summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries,
|
|
tasks: (backendSession as unknown as { tasks?: TaskData[] }).tasks,
|
|
};
|
|
}
|
|
|
|
// ========== Dashboard API ==========
|
|
|
|
/**
|
|
* Fetch dashboard statistics
|
|
*/
|
|
export async function fetchDashboardStats(): Promise<DashboardStats> {
|
|
try {
|
|
const data = await fetchApi<{ statistics?: DashboardStats }>('/api/data');
|
|
|
|
// 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)
|
|
* Applies transformation layer to map backend data to frontend SessionMetadata interface
|
|
*/
|
|
export async function fetchSessions(): Promise<SessionsResponse> {
|
|
try {
|
|
const data = await fetchApi<{
|
|
activeSessions?: BackendSessionData[];
|
|
archivedSessions?: BackendSessionData[];
|
|
}>('/api/data');
|
|
|
|
// 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<SessionMetadata> {
|
|
return fetchApi<SessionMetadata>(`/api/sessions/${encodeURIComponent(sessionId)}`);
|
|
}
|
|
|
|
/**
|
|
* Create a new session
|
|
*/
|
|
export async function createSession(input: CreateSessionInput): Promise<SessionMetadata> {
|
|
return fetchApi<SessionMetadata>('/api/sessions', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update a session
|
|
*/
|
|
export async function updateSession(
|
|
sessionId: string,
|
|
input: UpdateSessionInput
|
|
): Promise<SessionMetadata> {
|
|
return fetchApi<SessionMetadata>(`/api/sessions/${encodeURIComponent(sessionId)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Archive a session
|
|
*/
|
|
export async function archiveSession(sessionId: string): Promise<SessionMetadata> {
|
|
return fetchApi<SessionMetadata>(`/api/sessions/${encodeURIComponent(sessionId)}/archive`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a session
|
|
*/
|
|
export async function deleteSession(sessionId: string): Promise<void> {
|
|
return fetchApi<void>(`/api/sessions/${encodeURIComponent(sessionId)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// ========== Tasks API ==========
|
|
|
|
/**
|
|
* Fetch tasks for a session
|
|
*/
|
|
export async function fetchSessionTasks(sessionId: string): Promise<TaskData[]> {
|
|
return fetchApi<TaskData[]>(`/api/sessions/${encodeURIComponent(sessionId)}/tasks`);
|
|
}
|
|
|
|
/**
|
|
* Update a task status
|
|
*/
|
|
export async function updateTask(
|
|
sessionId: string,
|
|
taskId: string,
|
|
updates: Partial<TaskData>
|
|
): Promise<TaskData> {
|
|
return fetchApi<TaskData>(
|
|
`/api/sessions/${encodeURIComponent(sessionId)}/tasks/${encodeURIComponent(taskId)}`,
|
|
{
|
|
method: 'PATCH',
|
|
body: JSON.stringify(updates),
|
|
}
|
|
);
|
|
}
|
|
|
|
// ========== Path Management API ==========
|
|
|
|
/**
|
|
* Fetch recent paths
|
|
*/
|
|
export async function fetchRecentPaths(): Promise<string[]> {
|
|
const data = await fetchApi<{ paths?: string[] }>('/api/recent-paths');
|
|
return data.paths ?? [];
|
|
}
|
|
|
|
/**
|
|
* Remove a recent path
|
|
*/
|
|
export async function removeRecentPath(path: string): Promise<string[]> {
|
|
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<SwitchWorkspaceResponse> {
|
|
return fetchApi<SwitchWorkspaceResponse>(`/api/switch-path?path=${encodeURIComponent(path)}`);
|
|
}
|
|
|
|
/**
|
|
* Fetch data for a specific path
|
|
*/
|
|
export async function fetchDataForPath(path: string): Promise<FetchDataForPathResponse> {
|
|
return fetchApi<FetchDataForPathResponse>(`/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
|
|
*/
|
|
export async function fetchLoops(): Promise<LoopsResponse> {
|
|
const data = await fetchApi<{ loops?: Loop[] }>('/api/loops');
|
|
return {
|
|
loops: data.loops ?? [],
|
|
total: data.loops?.length ?? 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch a single loop by ID
|
|
*/
|
|
export async function fetchLoop(loopId: string): Promise<Loop> {
|
|
return fetchApi<Loop>(`/api/loops/${encodeURIComponent(loopId)}`);
|
|
}
|
|
|
|
/**
|
|
* Create a new loop
|
|
*/
|
|
export async function createLoop(input: {
|
|
prompt: string;
|
|
tool?: string;
|
|
mode?: string;
|
|
}): Promise<Loop> {
|
|
return fetchApi<Loop>('/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<Loop> {
|
|
return fetchApi<Loop>(`/api/loops/${encodeURIComponent(loopId)}/${action}`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a loop
|
|
*/
|
|
export async function deleteLoop(loopId: string): Promise<void> {
|
|
return fetchApi<void>(`/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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export interface IssueQueue {
|
|
tasks: string[];
|
|
solutions: string[];
|
|
conflicts: string[];
|
|
execution_groups: string[];
|
|
grouped_items: Record<string, string[]>;
|
|
}
|
|
|
|
export interface IssuesResponse {
|
|
issues: Issue[];
|
|
}
|
|
|
|
/**
|
|
* Fetch all issues
|
|
*/
|
|
export async function fetchIssues(projectPath?: string): Promise<IssuesResponse> {
|
|
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<IssuesResponse> {
|
|
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<IssueQueue> {
|
|
const url = projectPath
|
|
? `/api/queue?path=${encodeURIComponent(projectPath)}`
|
|
: '/api/queue';
|
|
return fetchApi<IssueQueue>(url);
|
|
}
|
|
|
|
/**
|
|
* Create a new issue
|
|
*/
|
|
export async function createIssue(input: {
|
|
title: string;
|
|
context?: string;
|
|
priority?: Issue['priority'];
|
|
}): Promise<Issue> {
|
|
return fetchApi<Issue>('/api/issues', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update an issue
|
|
*/
|
|
export async function updateIssue(
|
|
issueId: string,
|
|
input: Partial<Issue>
|
|
): Promise<Issue> {
|
|
return fetchApi<Issue>(`/api/issues/${encodeURIComponent(issueId)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete an issue
|
|
*/
|
|
export async function deleteIssue(issueId: string): Promise<void> {
|
|
return fetchApi<void>(`/api/issues/${encodeURIComponent(issueId)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// ========== Skills API ==========
|
|
|
|
export interface Skill {
|
|
name: string;
|
|
description: string;
|
|
enabled: boolean;
|
|
triggers: string[];
|
|
category?: string;
|
|
source?: 'builtin' | 'custom' | 'community';
|
|
version?: string;
|
|
author?: string;
|
|
}
|
|
|
|
export interface SkillsResponse {
|
|
skills: Skill[];
|
|
}
|
|
|
|
/**
|
|
* Fetch all skills
|
|
*/
|
|
export async function fetchSkills(): Promise<SkillsResponse> {
|
|
const data = await fetchApi<{ skills?: Skill[] }>('/api/skills');
|
|
return {
|
|
skills: data.skills ?? [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Toggle skill enabled status
|
|
*/
|
|
export async function toggleSkill(skillName: string, enabled: boolean): Promise<Skill> {
|
|
return fetchApi<Skill>(`/api/skills/${encodeURIComponent(skillName)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
}
|
|
|
|
// ========== Commands API ==========
|
|
|
|
export interface Command {
|
|
name: string;
|
|
description: string;
|
|
usage?: string;
|
|
examples?: string[];
|
|
category?: string;
|
|
aliases?: string[];
|
|
source?: 'builtin' | 'custom';
|
|
}
|
|
|
|
export interface CommandsResponse {
|
|
commands: Command[];
|
|
}
|
|
|
|
/**
|
|
* Fetch all commands
|
|
*/
|
|
export async function fetchCommands(): Promise<CommandsResponse> {
|
|
const data = await fetchApi<{ commands?: Command[] }>('/api/commands');
|
|
return {
|
|
commands: data.commands ?? [],
|
|
};
|
|
}
|
|
|
|
// ========== Memory API ==========
|
|
|
|
export interface CoreMemory {
|
|
id: string;
|
|
content: string;
|
|
createdAt: string;
|
|
updatedAt?: string;
|
|
source?: string;
|
|
tags?: string[];
|
|
size?: number;
|
|
}
|
|
|
|
export interface MemoryResponse {
|
|
memories: CoreMemory[];
|
|
totalSize: number;
|
|
claudeMdCount: number;
|
|
}
|
|
|
|
/**
|
|
* Fetch all memories
|
|
*/
|
|
export async function fetchMemories(): Promise<MemoryResponse> {
|
|
const data = await fetchApi<{
|
|
memories?: CoreMemory[];
|
|
totalSize?: number;
|
|
claudeMdCount?: number;
|
|
}>('/api/memory');
|
|
return {
|
|
memories: data.memories ?? [],
|
|
totalSize: data.totalSize ?? 0,
|
|
claudeMdCount: data.claudeMdCount ?? 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a new memory entry
|
|
*/
|
|
export async function createMemory(input: {
|
|
content: string;
|
|
tags?: string[];
|
|
}): Promise<CoreMemory> {
|
|
return fetchApi<CoreMemory>('/api/memory', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update a memory entry
|
|
*/
|
|
export async function updateMemory(
|
|
memoryId: string,
|
|
input: Partial<CoreMemory>
|
|
): Promise<CoreMemory> {
|
|
return fetchApi<CoreMemory>(`/api/memory/${encodeURIComponent(memoryId)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a memory entry
|
|
*/
|
|
export async function deleteMemory(memoryId: string): Promise<void> {
|
|
return fetchApi<void>(`/api/memory/${encodeURIComponent(memoryId)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// ========== 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<string, string[]>;
|
|
constraints?: Record<string, string[]>;
|
|
quality_rules?: GuidelineEntry[];
|
|
learnings?: LearningEntry[];
|
|
}
|
|
|
|
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
|
|
*/
|
|
export async function fetchProjectOverview(): Promise<ProjectOverview | null> {
|
|
const data = await fetchApi<{ projectOverview?: ProjectOverview }>('/api/ccw');
|
|
return data.projectOverview ?? null;
|
|
}
|
|
|
|
// ========== Session Detail API ==========
|
|
|
|
export interface SessionDetailContext {
|
|
requirements?: string[];
|
|
focus_paths?: string[];
|
|
artifacts?: string[];
|
|
shared_context?: {
|
|
tech_stack?: string[];
|
|
conventions?: string[];
|
|
};
|
|
}
|
|
|
|
export interface SessionDetailResponse {
|
|
session: SessionMetadata;
|
|
context?: SessionDetailContext;
|
|
summary?: string;
|
|
implPlan?: unknown;
|
|
conflicts?: unknown[];
|
|
review?: unknown;
|
|
}
|
|
|
|
/**
|
|
* Fetch session detail
|
|
* First fetches session list to get the session path, then fetches detail data
|
|
*/
|
|
export async function fetchSessionDetail(sessionId: string): Promise<SessionDetailResponse> {
|
|
// Step 1: Fetch all sessions to get the session path
|
|
const sessionsData = await fetchSessions();
|
|
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 path parameter, not sessionId
|
|
const sessionPath = (session as any).path || session.session_id;
|
|
const detailData = await fetchApi<any>(`/api/session-detail?path=${encodeURIComponent(sessionPath)}&type=all`);
|
|
|
|
// Step 3: Transform the response to match SessionDetailResponse interface
|
|
return {
|
|
session,
|
|
context: detailData.context,
|
|
summary: detailData.summary,
|
|
implPlan: detailData.implPlan,
|
|
conflicts: detailData.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;
|
|
}
|
|
|
|
export interface HistoryResponse {
|
|
executions: CliExecution[];
|
|
}
|
|
|
|
/**
|
|
* Fetch CLI execution history
|
|
*/
|
|
export async function fetchHistory(): Promise<HistoryResponse> {
|
|
const data = await fetchApi<{ executions?: CliExecution[] }>('/api/cli/history');
|
|
return {
|
|
executions: data.executions ?? [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Delete a CLI execution record
|
|
*/
|
|
export async function deleteExecution(executionId: string): Promise<void> {
|
|
await fetchApi<void>(`/api/cli/history/${encodeURIComponent(executionId)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete CLI executions by tool
|
|
*/
|
|
export async function deleteExecutionsByTool(tool: string): Promise<void> {
|
|
await fetchApi<void>(`/api/cli/history/tool/${encodeURIComponent(tool)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete all CLI execution history
|
|
*/
|
|
export async function deleteAllHistory(): Promise<void> {
|
|
await fetchApi<void>('/api/cli/history', {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch CLI execution detail (conversation records)
|
|
*/
|
|
export async function fetchExecutionDetail(
|
|
executionId: string,
|
|
sourceDir?: string
|
|
): Promise<ConversationRecord> {
|
|
const params = new URLSearchParams({ id: executionId });
|
|
if (sourceDir) params.set('path', sourceDir);
|
|
|
|
const data = await fetchApi<ConversationRecord>(
|
|
`/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;
|
|
structured?: unknown[];
|
|
};
|
|
timestamp: string;
|
|
duration_ms: number;
|
|
status?: 'success' | 'error' | 'timeout';
|
|
}
|
|
|
|
// ========== CLI Tools Config API ==========
|
|
|
|
export interface CliToolsConfigResponse {
|
|
version: string;
|
|
tools: Record<string, {
|
|
enabled: boolean;
|
|
primaryModel: string;
|
|
secondaryModel: string;
|
|
tags: string[];
|
|
type: string;
|
|
}>;
|
|
}
|
|
|
|
/**
|
|
* Fetch CLI tools configuration
|
|
*/
|
|
export async function fetchCliToolsConfig(): Promise<CliToolsConfigResponse> {
|
|
return fetchApi<CliToolsConfigResponse>('/api/cli/tools-config');
|
|
}
|
|
|
|
/**
|
|
* Update CLI tools configuration
|
|
*/
|
|
export async function updateCliToolsConfig(
|
|
config: Partial<CliToolsConfigResponse>
|
|
): Promise<CliToolsConfigResponse> {
|
|
return fetchApi<CliToolsConfigResponse>('/api/cli/tools-config', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(config),
|
|
});
|
|
}
|
|
|
|
// ========== Lite Tasks API ==========
|
|
|
|
export interface ImplementationStep {
|
|
step: number;
|
|
title?: string;
|
|
description?: string;
|
|
modification_points?: string[];
|
|
logic_flow?: string[];
|
|
depends_on?: number[];
|
|
output?: string;
|
|
}
|
|
|
|
export interface FlowControl {
|
|
pre_analysis?: Array<{
|
|
step: string;
|
|
action: string;
|
|
commands?: string[];
|
|
output_to: string;
|
|
on_error?: 'fail' | 'continue' | 'skip';
|
|
}>;
|
|
implementation_approach?: ImplementationStep[];
|
|
target_files?: string[];
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export interface LiteTaskSession {
|
|
id: string;
|
|
session_id?: string;
|
|
type: 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
|
|
title?: string;
|
|
description?: string;
|
|
tasks?: LiteTask[];
|
|
metadata?: Record<string, unknown>;
|
|
latestSynthesis?: {
|
|
title?: string | { en?: string; zh?: string };
|
|
status?: string;
|
|
};
|
|
roundCount?: number;
|
|
status?: string;
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
}
|
|
|
|
export interface LiteTasksResponse {
|
|
litePlan?: LiteTaskSession[];
|
|
liteFix?: LiteTaskSession[];
|
|
multiCliPlan?: LiteTaskSession[];
|
|
}
|
|
|
|
/**
|
|
* Fetch all lite tasks sessions
|
|
*/
|
|
export async function fetchLiteTasks(): Promise<LiteTasksResponse> {
|
|
const data = await fetchApi<{ liteTasks?: LiteTasksResponse }>('/api/data');
|
|
return data.liteTasks || {};
|
|
}
|
|
|
|
/**
|
|
* Fetch a single lite task session by ID
|
|
*/
|
|
export async function fetchLiteTaskSession(
|
|
sessionId: string,
|
|
type: 'lite-plan' | 'lite-fix' | 'multi-cli-plan'
|
|
): Promise<LiteTaskSession | null> {
|
|
const data = await fetchLiteTasks();
|
|
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;
|
|
}
|
|
|
|
// ========== 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<string, unknown>;
|
|
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[];
|
|
}
|
|
|
|
/**
|
|
* Fetch all review sessions
|
|
*/
|
|
export async function fetchReviewSessions(): Promise<ReviewSession[]> {
|
|
const data = await fetchApi<ReviewSessionsResponse>('/api/data');
|
|
return data.reviewSessions || [];
|
|
}
|
|
|
|
/**
|
|
* Fetch a single review session by ID
|
|
*/
|
|
export async function fetchReviewSession(sessionId: string): Promise<ReviewSession | null> {
|
|
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<string, string>;
|
|
enabled: boolean;
|
|
scope: 'project' | 'global';
|
|
}
|
|
|
|
export interface McpServersResponse {
|
|
project: McpServer[];
|
|
global: McpServer[];
|
|
}
|
|
|
|
/**
|
|
* Fetch all MCP servers (project and global scope)
|
|
*/
|
|
export async function fetchMcpServers(): Promise<McpServersResponse> {
|
|
const data = await fetchApi<{ project?: McpServer[]; global?: McpServer[] }>('/api/mcp/servers');
|
|
return {
|
|
project: data.project ?? [],
|
|
global: data.global ?? [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update MCP server configuration
|
|
*/
|
|
export async function updateMcpServer(
|
|
serverName: string,
|
|
config: Partial<McpServer>
|
|
): Promise<McpServer> {
|
|
return fetchApi<McpServer>(`/api/mcp/servers/${encodeURIComponent(serverName)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(config),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new MCP server
|
|
*/
|
|
export async function createMcpServer(
|
|
server: Omit<McpServer, 'name'>
|
|
): Promise<McpServer> {
|
|
return fetchApi<McpServer>('/api/mcp/servers', {
|
|
method: 'POST',
|
|
body: JSON.stringify(server),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete an MCP server
|
|
*/
|
|
export async function deleteMcpServer(serverName: string): Promise<void> {
|
|
await fetchApi<void>(`/api/mcp/servers/${encodeURIComponent(serverName)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle MCP server enabled status
|
|
*/
|
|
export async function toggleMcpServer(
|
|
serverName: string,
|
|
enabled: boolean
|
|
): Promise<McpServer> {
|
|
return fetchApi<McpServer>(`/api/mcp/servers/${encodeURIComponent(serverName)}/toggle`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
}
|
|
|
|
// ========== 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<CodexMcpServersResponse> {
|
|
return fetchApi<CodexMcpServersResponse>('/api/mcp/codex-servers');
|
|
}
|
|
|
|
/**
|
|
* Add a new MCP server to Codex config
|
|
* Note: This requires write access to Codex config.toml
|
|
*/
|
|
export async function addCodexMcpServer(server: Omit<McpServer, 'name'>): Promise<CodexMcpServer> {
|
|
return fetchApi<CodexMcpServer>('/api/mcp/codex-add', {
|
|
method: 'POST',
|
|
body: JSON.stringify(server),
|
|
});
|
|
}
|
|
|
|
// ========== CLI Endpoints API ==========
|
|
|
|
export interface CliEndpoint {
|
|
id: string;
|
|
name: string;
|
|
type: 'litellm' | 'custom' | 'wrapper';
|
|
enabled: boolean;
|
|
config: Record<string, unknown>;
|
|
}
|
|
|
|
export interface CliEndpointsResponse {
|
|
endpoints: CliEndpoint[];
|
|
}
|
|
|
|
/**
|
|
* Fetch all CLI endpoints
|
|
*/
|
|
export async function fetchCliEndpoints(): Promise<CliEndpointsResponse> {
|
|
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<CliEndpoint>
|
|
): Promise<CliEndpoint> {
|
|
return fetchApi<CliEndpoint>(`/api/cli/endpoints/${encodeURIComponent(endpointId)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(config),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new CLI endpoint
|
|
*/
|
|
export async function createCliEndpoint(
|
|
endpoint: Omit<CliEndpoint, 'id'>
|
|
): Promise<CliEndpoint> {
|
|
return fetchApi<CliEndpoint>('/api/cli/endpoints', {
|
|
method: 'POST',
|
|
body: JSON.stringify(endpoint),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a CLI endpoint
|
|
*/
|
|
export async function deleteCliEndpoint(endpointId: string): Promise<void> {
|
|
await fetchApi<void>(`/api/cli/endpoints/${encodeURIComponent(endpointId)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle CLI endpoint enabled status
|
|
*/
|
|
export async function toggleCliEndpoint(
|
|
endpointId: string,
|
|
enabled: boolean
|
|
): Promise<CliEndpoint> {
|
|
return fetchApi<CliEndpoint>(`/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<CliInstallationsResponse> {
|
|
const data = await fetchApi<{ tools?: CliInstallation[] }>('/api/cli/installations');
|
|
return {
|
|
tools: data.tools ?? [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Install a CLI tool
|
|
*/
|
|
export async function installCliTool(toolName: string): Promise<CliInstallation> {
|
|
return fetchApi<CliInstallation>(`/api/cli/installations/${encodeURIComponent(toolName)}/install`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Uninstall a CLI tool
|
|
*/
|
|
export async function uninstallCliTool(toolName: string): Promise<void> {
|
|
await fetchApi<void>(`/api/cli/installations/${encodeURIComponent(toolName)}/uninstall`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Upgrade a CLI tool
|
|
*/
|
|
export async function upgradeCliTool(toolName: string): Promise<CliInstallation> {
|
|
return fetchApi<CliInstallation>(`/api/cli/installations/${encodeURIComponent(toolName)}/upgrade`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check CLI tool installation status
|
|
*/
|
|
export async function checkCliToolStatus(toolName: string): Promise<CliInstallation> {
|
|
return fetchApi<CliInstallation>(`/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;
|
|
}
|
|
|
|
export interface HooksResponse {
|
|
hooks: Hook[];
|
|
}
|
|
|
|
/**
|
|
* Fetch all hooks
|
|
*/
|
|
export async function fetchHooks(): Promise<HooksResponse> {
|
|
const data = await fetchApi<{ hooks?: Hook[] }>('/api/hooks');
|
|
return {
|
|
hooks: data.hooks ?? [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update hook configuration
|
|
*/
|
|
export async function updateHook(
|
|
hookName: string,
|
|
config: Partial<Hook>
|
|
): Promise<Hook> {
|
|
return fetchApi<Hook>(`/api/hooks/${encodeURIComponent(hookName)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(config),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle hook enabled status
|
|
*/
|
|
export async function toggleHook(
|
|
hookName: string,
|
|
enabled: boolean
|
|
): Promise<Hook> {
|
|
return fetchApi<Hook>(`/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<Hook> {
|
|
return fetchApi<Hook>('/api/hooks/create', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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<Hook> {
|
|
return fetchApi<Hook>('/api/hooks/update', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name: hookName, ...input }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a hook
|
|
*/
|
|
export async function deleteHook(hookName: string): Promise<void> {
|
|
return fetchApi<void>(`/api/hooks/delete/${encodeURIComponent(hookName)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Install a hook from predefined template
|
|
*/
|
|
export async function installHookTemplate(templateId: string): Promise<Hook> {
|
|
return fetchApi<Hook>('/api/hooks/install-template', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ templateId }),
|
|
});
|
|
}
|
|
|
|
// ========== Rules API ==========
|
|
|
|
/**
|
|
* Fetch all rules
|
|
*/
|
|
export async function fetchRules(): Promise<RulesResponse> {
|
|
const data = await fetchApi<{ rules?: Rule[] }>('/api/rules');
|
|
return {
|
|
rules: data.rules ?? [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update rule configuration
|
|
*/
|
|
export async function updateRule(
|
|
ruleId: string,
|
|
config: Partial<Rule>
|
|
): Promise<Rule> {
|
|
return fetchApi<Rule>(`/api/rules/${encodeURIComponent(ruleId)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(config),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle rule enabled status
|
|
*/
|
|
export async function toggleRule(
|
|
ruleId: string,
|
|
enabled: boolean
|
|
): Promise<Rule> {
|
|
return fetchApi<Rule>(`/api/rules/${encodeURIComponent(ruleId)}/toggle`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new rule
|
|
*/
|
|
export async function createRule(input: RuleCreateInput): Promise<Rule> {
|
|
return fetchApi<Rule>('/api/rules/create', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a rule
|
|
*/
|
|
export async function deleteRule(
|
|
ruleId: string,
|
|
location?: string
|
|
): Promise<void> {
|
|
return fetchApi<void>(`/api/rules/${encodeURIComponent(ruleId)}`, {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({ location }),
|
|
});
|
|
}
|
|
|
|
// ========== CCW Tools MCP API ==========
|
|
|
|
/**
|
|
* CCW MCP configuration interface
|
|
*/
|
|
export interface CcwMcpConfig {
|
|
isInstalled: boolean;
|
|
enabledTools: string[];
|
|
projectRoot?: string;
|
|
allowedDirs?: string;
|
|
disableSandbox?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Fetch CCW Tools MCP configuration
|
|
*/
|
|
export async function fetchCcwMcpConfig(): Promise<CcwMcpConfig> {
|
|
const data = await fetchApi<CcwMcpConfig>('/api/mcp/ccw-config');
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Update CCW Tools MCP configuration
|
|
*/
|
|
export async function updateCcwConfig(config: {
|
|
enabledTools?: string[];
|
|
projectRoot?: string;
|
|
allowedDirs?: string;
|
|
disableSandbox?: boolean;
|
|
}): Promise<CcwMcpConfig> {
|
|
return fetchApi<CcwMcpConfig>('/api/mcp/ccw-config', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(config),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Install CCW Tools MCP server
|
|
*/
|
|
export async function installCcwMcp(): Promise<CcwMcpConfig> {
|
|
return fetchApi<CcwMcpConfig>('/api/mcp/ccw-install', {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Uninstall CCW Tools MCP server
|
|
*/
|
|
export async function uninstallCcwMcp(): Promise<void> {
|
|
await fetchApi<void>('/api/mcp/ccw-uninstall', {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
// ========== Index Management API ==========
|
|
|
|
/**
|
|
* Fetch current index status
|
|
*/
|
|
export async function fetchIndexStatus(): Promise<IndexStatus> {
|
|
return fetchApi<IndexStatus>('/api/index/status');
|
|
}
|
|
|
|
/**
|
|
* Rebuild index
|
|
*/
|
|
export async function rebuildIndex(request: IndexRebuildRequest = {}): Promise<IndexStatus> {
|
|
return fetchApi<IndexStatus>('/api/index/rebuild', {
|
|
method: 'POST',
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
// ========== 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[];
|
|
}
|
|
|
|
/**
|
|
* Analyze prompts request
|
|
*/
|
|
export interface AnalyzePromptsRequest {
|
|
tool?: 'gemini' | 'qwen' | 'codex';
|
|
promptIds?: string[];
|
|
limit?: number;
|
|
}
|
|
|
|
/**
|
|
* Fetch all prompts from history
|
|
*/
|
|
export async function fetchPrompts(): Promise<PromptsResponse> {
|
|
return fetchApi<PromptsResponse>('/api/memory/prompts');
|
|
}
|
|
|
|
/**
|
|
* Fetch prompt insights from backend
|
|
*/
|
|
export async function fetchPromptInsights(): Promise<PromptInsightsResponse> {
|
|
return fetchApi<PromptInsightsResponse>('/api/memory/insights');
|
|
}
|
|
|
|
/**
|
|
* Analyze prompts using AI tool
|
|
*/
|
|
export async function analyzePrompts(request: AnalyzePromptsRequest = {}): Promise<PromptInsightsResponse> {
|
|
return fetchApi<PromptInsightsResponse>('/api/memory/analyze', {
|
|
method: 'POST',
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a prompt from history
|
|
*/
|
|
export async function deletePrompt(promptId: string): Promise<void> {
|
|
await fetchApi<void>('/api/memory/prompts/' + encodeURIComponent(promptId), {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// ========== 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<FileTreeResponse> {
|
|
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<FileTreeResponse>(`/api/explorer/tree?${params.toString()}`);
|
|
}
|
|
|
|
/**
|
|
* Fetch file content
|
|
*/
|
|
export async function fetchFileContent(filePath: string, options: {
|
|
encoding?: 'utf8' | 'ascii' | 'base64';
|
|
maxSize?: number;
|
|
} = {}): Promise<import('../types/file-explorer').FileContent> {
|
|
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<import('../types/file-explorer').FileContent>(`/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<SearchFilesResponse> {
|
|
return fetchApi<SearchFilesResponse>('/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<RootDirectory[]> {
|
|
return fetchApi<RootDirectory[]>('/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<GraphDependenciesResponse> {
|
|
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<GraphDependenciesResponse>(`/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<GraphImpactResponse> {
|
|
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<GraphImpactResponse>(`/api/graph/impact?${params.toString()}`);
|
|
}
|
|
|