/** * Lite Mode API Client - Binary-Free Mode * * This provides a client for working with NomadArch in Binary-Free Mode, * using native session management instead of the OpenCode binary. */ import { CODENOMAD_API_BASE } from "./api-client" import { getLogger } from "./logger" const log = getLogger("lite-mode") export interface ModeInfo { mode: "lite" | "full" binaryFreeMode: boolean nativeSessions: boolean opencodeBinaryAvailable: boolean providers: { qwen: boolean zai: boolean zen: boolean } } export interface NativeSession { id: string workspaceId: string title?: string parentId?: string | null createdAt: number updatedAt: number messageIds: string[] model?: { providerId: string modelId: string } agent?: string } export interface NativeMessage { id: string sessionId: string role: "user" | "assistant" | "system" | "tool" content?: string createdAt: number updatedAt: number status?: "pending" | "streaming" | "completed" | "error" } let modeCache: ModeInfo | null = null /** * Get the current running mode (lite or full) */ export async function getMode(): Promise { if (modeCache) return modeCache try { const response = await fetch(`${CODENOMAD_API_BASE}/api/meta/mode`) if (!response.ok) { throw new Error(`Failed to fetch mode: ${response.status}`) } modeCache = await response.json() log.info(`Running in ${modeCache?.mode} mode`, { binaryFree: modeCache?.binaryFreeMode }) return modeCache! } catch (error) { log.warn("Failed to fetch mode, assuming lite mode", error) // Default to lite mode if we can't determine return { mode: "lite", binaryFreeMode: true, nativeSessions: true, opencodeBinaryAvailable: false, providers: { qwen: true, zai: true, zen: true } } } } /** * Check if running in Binary-Free (lite) mode */ export async function isLiteMode(): Promise { const mode = await getMode() return mode.binaryFreeMode } /** * Native Session API for Binary-Free Mode */ export const nativeSessionApi = { async listSessions(workspaceId: string): Promise { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions`) if (!response.ok) throw new Error("Failed to list sessions") const data = await response.json() return data.sessions }, async createSession(workspaceId: string, options?: { title?: string parentId?: string model?: { providerId: string; modelId: string } agent?: string }): Promise { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(options ?? {}) }) if (!response.ok) throw new Error("Failed to create session") const data = await response.json() return data.session }, async getSession(workspaceId: string, sessionId: string): Promise { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`) if (response.status === 404) return null if (!response.ok) throw new Error("Failed to get session") const data = await response.json() return data.session }, async updateSession(workspaceId: string, sessionId: string, updates: Partial): Promise { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates) }) if (response.status === 404) return null if (!response.ok) throw new Error("Failed to update session") const data = await response.json() return data.session }, async deleteSession(workspaceId: string, sessionId: string): Promise { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" }) return response.ok || response.status === 204 }, async forkSession(workspaceId: string, sessionId: string): Promise { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/fork`, { method: "POST" }) if (!response.ok) throw new Error("Failed to fork session") const data = await response.json() return data.session }, async revertSession(workspaceId: string, sessionId: string, messageId?: string): Promise { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/revert`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messageId }) }) if (!response.ok) throw new Error("Failed to revert session") const data = await response.json() return data.session }, async getMessages(workspaceId: string, sessionId: string): Promise { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/messages`) if (!response.ok) throw new Error("Failed to get messages") const data = await response.json() return data.messages }, /** * Import sessions from SDK mode to Native mode */ async importSessions(workspaceId: string, sessions: Array<{ id: string title?: string parentId?: string | null createdAt?: number updatedAt?: number model?: { providerId: string; modelId: string } agent?: string messages?: Array<{ id: string role: "user" | "assistant" | "system" | "tool" content?: string createdAt?: number }> }>): Promise<{ success: boolean; imported: number; skipped: number }> { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/import`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessions }) }) if (!response.ok) throw new Error("Failed to import sessions") return response.json() }, /** * Sync sessions from SDK (OpenCode) to Native mode * This reads sessions directly from OpenCode's storage */ async syncFromSdk(workspaceId: string, folderPath: string): Promise<{ success: boolean imported: number skipped: number total?: number message?: string }> { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sync-sdk`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ folderPath }) }) if (!response.ok) throw new Error("Failed to sync SDK sessions") return response.json() }, /** * Check if SDK sessions exist for a folder */ async checkSdkSessions(folderPath: string): Promise<{ found: boolean count: number sessions: Array<{ id: string; title: string; created: number }> }> { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/check-sdk-sessions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ folderPath }) }) if (!response.ok) throw new Error("Failed to check SDK sessions") return response.json() }, /** * Send a prompt to the session and get a streaming response */ async* streamPrompt( workspaceId: string, sessionId: string, content: string, options?: { provider?: "qwen" | "zai" | "zen" accessToken?: string resourceUrl?: string enableTools?: boolean } ): AsyncGenerator<{ type: "content" | "done" | "error"; data?: string }> { const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/prompt`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content, provider: options?.provider ?? "qwen", accessToken: options?.accessToken, resourceUrl: options?.resourceUrl, enableTools: options?.enableTools ?? true }) }) if (!response.ok) { yield { type: "error", data: `Request failed: ${response.status}` } return } const reader = response.body?.getReader() if (!reader) { yield { type: "error", data: "No response body" } return } const decoder = new TextDecoder() let buffer = "" while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split("\n") buffer = lines.pop() ?? "" for (const line of lines) { if (!line.trim()) continue if (line.startsWith("data: ")) { const data = line.slice(6) if (data === "[DONE]") { yield { type: "done" } return } try { const parsed = JSON.parse(data) if (parsed.error) { yield { type: "error", data: parsed.error } } else if (parsed.choices?.[0]?.delta?.content) { yield { type: "content", data: parsed.choices[0].delta.content } } } catch { // Skip invalid JSON } } } } yield { type: "done" } } } /** * Clear mode cache (for testing or after config changes) */ export function clearModeCache(): void { modeCache = null }