import type { AppConfig, FileStat, GraphSnapshot, ReMeHealth, ReMeResponse, StreamChunk, } from "./types"; import { decodeSseEvent } from "./chat-stream"; import { healthFromResponse } from "./health-status"; import { translate, useLanguageStore, type TranslationKey } from "./i18n"; import { WORKSPACE_FILE_LIMIT, workspaceFileListing, type WorkspaceFileListing, } from "./workspace-files"; import { displayReMeApiEndpoint, normalizeReMeApiUrl } from "./api-endpoint"; export const REME_API_URL = normalizeReMeApiUrl( process.env.NEXT_PUBLIC_REME_API_URL || "http://127.0.0.1:2333", ); export const REME_API_ENDPOINT = displayReMeApiEndpoint( REME_API_URL, typeof window === "undefined" ? undefined : window.location.origin, ); const message = (key: TranslationKey, status: number) => translate(useLanguageStore.getState().language, key, { status: String(status), }); async function parseResponse(response: Response): Promise> { let payload: ReMeResponse; try { payload = (await response.json()) as ReMeResponse; } catch { throw new Error(message("invalidResponse", response.status)); } if (!response.ok || !payload.success) { const detail = typeof payload.answer === "string" ? payload.answer : message("requestFailed", response.status); throw new Error(detail); } return payload; } export async function callReMe( action: string, body: Record = {}, ): Promise> { const response = await fetch(`${REME_API_URL}/${action}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); return parseResponse(response); } export async function getAppConfig(): Promise { return (await callReMe("app_config")).answer; } export async function getReMeVersion(): Promise { return String((await callReMe("version")).answer); } export async function getReMeStatus(): Promise> { return callReMe("status"); } export async function getReMeHealth(): Promise { const response = await callReMe("health_check"); return healthFromResponse(response); } export async function rebuildReMeIndex(): Promise> { return callReMe("reindex"); } export async function getGraphSnapshot(): Promise { return (await callReMe("graph_snapshot")).answer; } export async function listWorkspaceFiles( extensions: string[], ): Promise { const response = await callReMe("list", { path: "", recursive: true, limit: WORKSPACE_FILE_LIMIT, sort_by: "mtime", extensions, }); return workspaceFileListing(response.metadata.items); } export async function readWorkspaceFile( path: string, ): Promise<{ content: string; stat: FileStat }> { const response = await callReMe("load", { path }); return { content: String(response.answer ?? ""), stat: response.metadata as unknown as FileStat, }; } export async function saveWorkspaceFile( path: string, content: string, expectedMtime?: string, ): Promise { const response = await callReMe("save", { path, content, expected_mtime: expectedMtime || null, }); return response.metadata as unknown as FileStat; } export async function streamChat( query: string, sessionId: string | undefined, signal: AbortSignal, onChunk: (chunk: StreamChunk) => void, ): Promise { const response = await fetch(`${REME_API_URL}/chat`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "text/event-stream", }, body: JSON.stringify({ query, session_id: sessionId || null }), signal, }); if (!response.ok || !response.body) throw new Error(message("agentUnavailable", response.status)); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { value, done } = await reader.read(); buffer += decoder.decode(value, { stream: !done }); const events = buffer.split(/\r?\n\r?\n/); buffer = events.pop() || ""; for (const event of events) { const chunk = decodeSseEvent(event); if (chunk) onChunk(chunk); } if (done) break; } const finalChunk = decodeSseEvent(buffer); if (finalChunk) onChunk(finalChunk); }