export interface PackOptions { removeComments: boolean; removeEmptyLines: boolean; showLineNumbers: boolean; fileSummary?: boolean; directoryStructure?: boolean; includePatterns?: string; ignorePatterns?: string; outputParsable?: boolean; compress?: boolean; } export interface FileInfo { path: string; charCount: number; selected?: boolean; } export interface PackRequest { url: string; format: 'xml' | 'markdown' | 'plain'; options: PackOptions; file?: File; } export interface SuspiciousFile { filePath: string; messages: string[]; } export interface PackResult { content: string; format: string; metadata: { repository: string; timestamp: string; summary: { totalFiles: number; totalCharacters: number; totalTokens: number; }; topFiles: { path: string; charCount: number; tokenCount: number; }[]; allFiles?: FileInfo[]; suspiciousFiles?: SuspiciousFile[]; }; } export interface ErrorResponse { error: string; } export class ApiError extends Error { constructor(message: string) { super(message); this.name = 'ApiError'; } } // Wire-protocol stages — must stay aligned with the server-emitted SSE // values (`website/server/src/types.ts:PackProgressStage`). `onProgress` // callbacks receive only these, so any new server stage requires a // deliberate type update on both sides. export type PackProgressStage = 'cache-check' | 'cloning' | 'repository-fetch' | 'extracting' | 'processing'; // Display-only superset. `verifying` is a client-only synthetic stage // shown while the server runs Turnstile siteverify (before any SSE event // arrives); usePackRequest sets it locally between `takeToken()` returning // and the first onProgress callback firing, so the loading UI shows a // meaningful step instead of a generic "...". Keeping it out of // `PackProgressStage` prevents the wire contract from drifting silently // when display-only stages get added or renamed. export type DisplayProgressStage = PackProgressStage | 'verifying'; export interface PackStreamCallbacks { onProgress?: (stage: PackProgressStage, message?: string) => void; signal?: AbortSignal; } const API_BASE_URL = import.meta.env.PROD ? 'https://api.repomix.com' : 'http://localhost:8080'; // NDJSON stream event types interface ProgressEvent { type: 'progress'; stage: PackProgressStage; message?: string; } interface ResultEvent { type: 'result'; data: PackResult; } interface StreamErrorEvent { type: 'error'; message: string; } type StreamEvent = ProgressEvent | ResultEvent | StreamErrorEvent; export async function packRepository( request: PackRequest, callbacks?: PackStreamCallbacks, turnstileToken?: string, ): Promise { const formData = new FormData(); if (request.file) { formData.append('file', request.file); } else { formData.append('url', request.url); } formData.append('format', request.format); formData.append('options', JSON.stringify(request.options)); // Token rides as a header rather than a form field to keep packRequestSchema // free of cross-cutting concerns; the server-side turnstileMiddleware reads // it before the schema validation runs. const headers: HeadersInit = turnstileToken ? { 'X-Turnstile-Token': turnstileToken } : {}; const response = await fetch(`${API_BASE_URL}/api/pack`, { method: 'POST', headers, body: formData, signal: callbacks?.signal, }); // Handle non-streaming error responses (validation errors return JSON) if (!response.ok) { const data = await response.json(); throw new ApiError((data as ErrorResponse).error); } // Handle NDJSON stream if (!response.body) { throw new ApiError('No response body received'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let result: PackResult | null = null; try { while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // Parse complete lines from buffer const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.trim()) continue; const event = JSON.parse(line) as StreamEvent; if (event.type === 'progress') { callbacks?.onProgress?.(event.stage, event.message); } else if (event.type === 'result') { result = event.data; } else if (event.type === 'error') { throw new ApiError(event.message); } } } } finally { reader.releaseLock(); } if (!result) { throw new ApiError('No result received from server'); } return result; }