diff --git a/build/icon.icns b/build/icon.icns index 21049559..6a101799 100644 Binary files a/build/icon.icns and b/build/icon.icns differ diff --git a/build/icon.ico b/build/icon.ico index 249086b6..3b161fd4 100644 Binary files a/build/icon.ico and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png index a50dccda..122ebb2f 100644 Binary files a/build/icon.png and b/build/icon.png differ diff --git a/electron/icon.png b/electron/icon.png index a67a9cd6..122ebb2f 100644 Binary files a/electron/icon.png and b/electron/icon.png differ diff --git a/electron/main.ts b/electron/main.ts index 4c13e489..9c5b689d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -695,9 +695,7 @@ app.on('ready', async () => { }) app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() - } + app.quit() }) app.on('activate', () => { @@ -706,25 +704,33 @@ app.on('activate', () => { } }) -app.on('before-quit', async () => { +app.on('before-quit', () => { clearUpdateTimer() - await cleanupPortFile() killNitroProcess() + cleanupPortFile().catch(() => {}) }) /** Platform-aware Nitro process termination. */ function killNitroProcess(): void { if (!nitroProcess) return + const pid = nitroProcess.pid if (process.platform === 'win32') { // SIGTERM is unreliable on Windows; use taskkill for proper tree-kill try { - const pid = nitroProcess.pid if (pid) { execSync(`taskkill /pid ${pid} /T /F`, { stdio: 'ignore' }) } } catch { /* process may have already exited */ } } else { - nitroProcess.kill('SIGTERM') + // SIGTERM may not be processed before the main process exits, + // leaving orphan Nitro processes. Kill the entire process group + // with SIGKILL for reliable cleanup. + try { + if (pid) process.kill(-pid, 'SIGKILL') + } catch { /* process may have already exited */ } + try { + nitroProcess.kill('SIGKILL') + } catch { /* ignore */ } } nitroProcess = null } diff --git a/package.json b/package.json index 5238b8ad..094de531 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openpencil", - "version": "0.4.3", + "version": "0.4.4", "description": "The world's first open-source AI-native vector design tool and the first to feature concurrent Agent Teams. Design-as-Code. Turn prompts into UI directly on the live canvas. A modern alternative to Pencil.", "author": { "name": "ZSeven-W", diff --git a/server/api/ai/chat.ts b/server/api/ai/chat.ts index d291e626..48bf2819 100644 --- a/server/api/ai/chat.ts +++ b/server/api/ai/chat.ts @@ -6,6 +6,7 @@ import { resolveClaudeCli } from '../../utils/resolve-claude-cli' import { runCodexExec } from '../../utils/codex-client' import { buildClaudeAgentEnv, + buildSpawnClaudeCodeProcess, getClaudeAgentDebugFilePath, } from '../../utils/resolve-claude-agent-env' @@ -80,7 +81,7 @@ function buildClaudeExitHint(rawError: string, debugTail?: string[]): string | u export default defineEventHandler(async (event) => { const body = await readBody(event) - if (!body?.messages || !body?.system) { + if (!body?.messages || body?.system == null) { setResponseHeaders(event, { 'Content-Type': 'application/json' }) return { error: 'Missing required fields: system, messages' } } @@ -236,6 +237,7 @@ function streamViaAgentSDK(body: ChatBody, model?: string) { env, ...(debugFile ? { debugFile } : {}), ...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}), + ...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}), }, }) @@ -285,6 +287,7 @@ function streamViaAgentSDK(body: ChatBody, model?: string) { env, ...(debugFile ? { debugFile } : {}), ...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}), + ...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}), }, }) @@ -406,32 +409,8 @@ function parseOpenCodeModel(model?: string): { providerID: string; modelID: stri return { providerID: model.slice(0, idx), modelID: model.slice(idx + 1) } } -function mapOpenCodeEffort( - effort?: 'low' | 'medium' | 'high' | 'max', -): 'low' | 'medium' | 'high' | undefined { - if (!effort) return undefined - if (effort === 'max') return 'high' - return effort -} - -function buildOpenCodeReasoning( - body: ChatBody, -): Record | undefined { - const reasoning: Record = {} - const effort = mapOpenCodeEffort(body.effort) - if (effort) { - reasoning.effort = effort - } - if (body.thinkingMode === 'enabled') { - reasoning.enabled = true - } else if (body.thinkingMode === 'disabled') { - reasoning.enabled = false - } - if (typeof body.thinkingBudgetTokens === 'number' && body.thinkingBudgetTokens > 0) { - reasoning.budgetTokens = body.thinkingBudgetTokens - } - return Object.keys(reasoning).length > 0 ? reasoning : undefined -} +// Note: OpenCode SDK does not support `reasoning` in promptAsync/prompt params. +// The `reasoning` field was silently dropped by buildClientParams. Removed. /** Wrap an async generator with a timeout — yields values until timeout fires */ async function* streamWithTimeout( @@ -543,11 +522,14 @@ function streamViaOpenCode(body: ChatBody, model?: string) { } // Inject system prompt as context (no AI reply) - await ocClient.session.prompt({ + const { error: sysPromptError } = await ocClient.session.prompt({ sessionID: session.id, noReply: true, parts: [{ type: 'text', text: body.system }], - }) + }) as any + if (sysPromptError) { + console.error('[AI] OpenCode system prompt injection failed:', formatOpenCodeError(sysPromptError)) + } // Build prompt from the last user message const lastUserMsg = [...body.messages].reverse().find((m) => m.role === 'user') @@ -568,7 +550,6 @@ function streamViaOpenCode(body: ChatBody, model?: string) { { type: 'text', text: prompt || 'Analyze these images.' }, ] - console.log(`[AI] OpenCode streaming prompt: model=${model}, parsed=${JSON.stringify(parsed)}`) // Build prompt payload with optional model and reasoning const promptPayload: Record = { @@ -576,16 +557,46 @@ function streamViaOpenCode(body: ChatBody, model?: string) { ...(parsed ? { model: parsed } : {}), parts, } - const reasoning = buildOpenCodeReasoning(body) - if (reasoning) { - promptPayload.reasoning = reasoning - } - // Subscribe to event stream for real-time deltas + // Subscribe to event stream for real-time deltas. + // IMPORTANT: The SSE connection is lazy — it only connects when + // iteration starts. We must start consuming BEFORE sending the + // prompt to avoid a race where events are emitted before the + // SSE connection is established. const eventResult = await ocClient.event.subscribe() const eventStream = eventResult.stream - // Send prompt asynchronously — response comes via events + const sessionId = session.id + const STREAM_TIMEOUT_MS = 180_000 + + // Start eagerly consuming the event stream into a buffer. + // This triggers the SSE HTTP connection immediately. + const eventBuffer: unknown[] = [] + let streamDone = false + let notifyFn: (() => void) | null = null + + const notify = () => { if (notifyFn) { const fn = notifyFn; notifyFn = null; fn() } } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + void (async () => { + const timeoutPromise = new Promise<{ done: true; value: undefined }>((resolve) => + setTimeout(() => resolve({ done: true, value: undefined }), STREAM_TIMEOUT_MS), + ) + try { + for await (const event of streamWithTimeout(eventStream, timeoutPromise)) { + eventBuffer.push(event) + notify() + } + } finally { + streamDone = true + notify() + } + })() + + // Give the SSE connection a moment to establish before sending prompt + await new Promise(resolve => setTimeout(resolve, 100)) + + // Now send the prompt — SSE connection should already be active const { error: asyncError } = await ocClient.session.promptAsync(promptPayload as any) if (asyncError) { const detail = formatOpenCodeError(asyncError) @@ -593,18 +604,24 @@ function streamViaOpenCode(body: ChatBody, model?: string) { throw new Error(detail) } - // Consume event stream, forwarding text deltas to client + // Consume buffered events + wait for new ones let emittedText = false - const sessionId = session.id - const STREAM_TIMEOUT_MS = 180_000 - const timeoutPromise = new Promise<{ done: true; value: undefined }>((resolve) => - setTimeout(() => resolve({ done: true, value: undefined }), STREAM_TIMEOUT_MS), - ) + let eventCount = 0 + let shouldBreak = false - for await (const event of streamWithTimeout(eventStream, timeoutPromise)) { - if (!event || !('type' in event)) continue + while (!shouldBreak) { + // Wait for events if buffer is empty + if (eventBuffer.length === 0) { + if (streamDone) break + await new Promise(resolve => { notifyFn = resolve }) + continue + } - const eventType = event.type as string + const event = eventBuffer.shift() + if (!event || !('type' in (event as any))) continue + + const eventType = (event as any).type as string + eventCount++ // Stream text deltas for our session if (eventType === 'message.part.delta') { @@ -625,7 +642,9 @@ function streamViaOpenCode(body: ChatBody, model?: string) { // Session went idle — response complete if (eventType === 'session.idle') { const props = (event as any).properties - if (props?.sessionID === sessionId) break + if (props?.sessionID === sessionId) { + shouldBreak = true + } continue } @@ -637,7 +656,7 @@ function streamViaOpenCode(body: ChatBody, model?: string) { console.error('[AI] OpenCode session error:', errMsg) const data = JSON.stringify({ type: 'error', content: errMsg }) controller.enqueue(encoder.encode(`data: ${data}\n\n`)) - break + shouldBreak = true } continue } @@ -645,8 +664,29 @@ function streamViaOpenCode(body: ChatBody, model?: string) { clearInterval(pingTimer) + // Fallback: if no text was streamed, try reading session messages directly + if (!emittedText) { + try { + const { data: messages } = await ocClient.session.messages({ sessionID: sessionId }) as any + if (messages && Array.isArray(messages)) { + // Find the last assistant message (each item has { info, parts }) + const assistantMsg = [...messages].reverse().find((m: any) => m.info?.role === 'assistant') + if (assistantMsg?.parts) { + for (const part of assistantMsg.parts) { + if (part.type === 'text' && part.text) { + const data = JSON.stringify({ type: 'text', content: part.text }) + controller.enqueue(encoder.encode(`data: ${data}\n\n`)) + emittedText = true + } + } + } + } + } catch { + // fallback failed — will emit error below + } + } + if (!emittedText) { - console.warn('[AI] OpenCode returned no text via streaming events') const data = JSON.stringify({ type: 'error', content: 'OpenCode returned an empty response. The model may not have generated any output.' }) controller.enqueue(encoder.encode(`data: ${data}\n\n`)) } diff --git a/server/api/ai/connect-agent.ts b/server/api/ai/connect-agent.ts index a78f3903..5941dc2b 100644 --- a/server/api/ai/connect-agent.ts +++ b/server/api/ai/connect-agent.ts @@ -1,13 +1,43 @@ import { defineEventHandler, readBody, setResponseHeaders } from 'h3' import { existsSync } from 'node:fs' +import { join } from 'node:path' import type { GroupedModel } from '../../../src/types/agent-settings' import { resolveClaudeCli } from '../../utils/resolve-claude-cli' import { serverLog } from '../../utils/server-logger' import { buildClaudeAgentEnv, + buildSpawnClaudeCodeProcess, getClaudeAgentDebugFilePath, } from '../../utils/resolve-claude-agent-env' +/** Windows npm global installs may create .cmd or .ps1 wrappers — try both */ +function winNpmCandidates(dir: string, name: string): string[] { + return [join(dir, `${name}.cmd`), join(dir, `${name}.ps1`)] +} + +/** + * On Windows, `where` may return an extensionless Unix shell script (e.g. `…/npm/opencode`). + * This file exists but can't be executed. Prefer `.cmd` or `.ps1` wrapper at the same location. + */ +function resolveWinExtension(binPath: string): string { + if (process.platform !== 'win32') return binPath + // Already has a usable extension + if (/\.(cmd|ps1|exe)$/i.test(binPath)) return binPath + // Try .cmd then .ps1 + for (const ext of ['.cmd', '.ps1']) { + if (existsSync(binPath + ext)) return binPath + ext + } + return binPath +} + +/** Build a shell command to invoke a resolved binary (handles .ps1 on Windows) */ +function buildExecCmd(binPath: string, args: string): string { + if (binPath.endsWith('.ps1')) { + return `powershell -ExecutionPolicy Bypass -File "${binPath}" ${args}` + } + return `"${binPath}" ${args}` +} + interface ConnectBody { agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot' } @@ -16,6 +46,7 @@ interface ConnectResult { connected: boolean models: GroupedModel[] error?: string + warning?: string notInstalled?: boolean /** Human-readable connection status, e.g. "Connected via API key" */ connectionInfo?: string @@ -96,6 +127,7 @@ async function connectClaudeCode(): Promise { env, ...(debugFile ? { debugFile } : {}), ...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}), + ...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}), }, }) @@ -194,7 +226,8 @@ async function buildCodexConnectionInfo(): Promise<{ connectionInfo: string; hin } try { - const authPath = join(homedir(), '.codex', 'auth.json') + const codexHome = process.env.CODEX_HOME || join(homedir(), '.codex') + const authPath = join(codexHome, 'auth.json') const raw = await readFile(authPath, 'utf-8') const auth = JSON.parse(raw) as { auth_mode?: string; tokens?: { id_token?: string } } @@ -237,6 +270,41 @@ function friendlyClaudeError(raw: string): string { return raw } +/** + * Fallback: parse model IDs from Codex's bundled latest-model.md when + * models_cache.json is missing (e.g. fresh Windows install). + * Only includes text/reasoning models (skips image, audio, video, embedding, moderation). + */ +async function parseCodexLatestModelMd(codexHome: string): Promise { + const { readFile } = await import('node:fs/promises') + const { join } = await import('node:path') + const mdPath = join(codexHome, 'skills', '.system', 'openai-docs', 'references', 'latest-model.md') + try { + const content = await readFile(mdPath, 'utf-8') + const models: GroupedModel[] = [] + // Match markdown table rows: | `model-id` | description | + const rowRe = /^\|\s*`([^`]+)`\s*\|\s*(.+?)\s*\|/gm + const skipRe = /image|audio|tts|transcribe|realtime|sora|video|embedding|moderation/i + let match: RegExpExecArray | null + const seen = new Set() + while ((match = rowRe.exec(content)) !== null) { + const slug = match[1] + const desc = match[2].trim() + if (skipRe.test(slug) || skipRe.test(desc) || seen.has(slug)) continue + seen.add(slug) + models.push({ + value: slug, + displayName: slug, + description: desc, + provider: 'openai' as const, + }) + } + return models + } catch { + return [] + } +} + /** Connect to Codex CLI and fetch its supported models from the local cache */ async function connectCodexCli(): Promise { serverLog.info('[connect-agent] connecting to Codex CLI...') @@ -258,13 +326,13 @@ async function connectCodexCli(): Promise { encoding: 'utf-8', timeout: 5000, }).trim().split(/\r?\n/)[0]?.trim() ?? '' - if (result && existsSync(result)) which = result - serverLog.info(`[connect-agent] codex PATH result: "${result}" (exists=${result ? existsSync(result) : false})`) + if (result && existsSync(result)) which = resolveWinExtension(result) + serverLog.info(`[connect-agent] codex PATH result: "${result}" resolved="${which}" (exists=${result ? existsSync(result) : false})`) } catch (err) { serverLog.info(`[connect-agent] codex PATH lookup failed: ${err instanceof Error ? err.message : err}`) } - // 2. npm prefix -g (Windows: npm global creates .cmd wrappers) + // 2. npm prefix -g (Windows: npm global creates .cmd or .ps1 wrappers) if (!which && isWin) { try { serverLog.info('[connect-agent] codex: trying npm.cmd prefix -g') @@ -274,9 +342,10 @@ async function connectCodexCli(): Promise { }).trim() serverLog.info(`[connect-agent] codex npm global prefix: "${prefix}"`) if (prefix) { - const bin = join(prefix, 'codex.cmd') - serverLog.info(`[connect-agent] codex npm global bin: "${bin}" (exists=${existsSync(bin)})`) - if (existsSync(bin)) which = bin + for (const bin of winNpmCandidates(prefix, 'codex')) { + serverLog.info(`[connect-agent] codex npm global bin: "${bin}" (exists=${existsSync(bin)})`) + if (existsSync(bin)) { which = bin; break } + } } } catch (err) { serverLog.info(`[connect-agent] codex npm prefix -g failed: ${err instanceof Error ? err.message : err}`) @@ -286,9 +355,9 @@ async function connectCodexCli(): Promise { // 3. Common install locations if (!which && isWin) { const candidates = [ - join(process.env.APPDATA || '', 'npm', 'codex.cmd'), - join(process.env.NVM_SYMLINK || '', 'codex.cmd'), - join(process.env.FNM_MULTISHELL_PATH || '', 'codex.cmd'), + ...winNpmCandidates(join(process.env.APPDATA || '', 'npm'), 'codex'), + ...winNpmCandidates(join(process.env.NVM_SYMLINK || ''), 'codex'), + ...winNpmCandidates(join(process.env.FNM_MULTISHELL_PATH || ''), 'codex'), ] for (const c of candidates) { const exists = c ? existsSync(c) : false @@ -304,8 +373,8 @@ async function connectCodexCli(): Promise { serverLog.info(`[connect-agent] codex resolved: "${which}"`) - // Verify codex is responsive — on Windows, use the resolved path or .cmd wrapper - const versionCmd = isWin ? `"${which}" --version 2>&1` : 'codex --version 2>&1' + // Verify codex is responsive — always use the resolved path + const versionCmd = buildExecCmd(which, '--version') + ' 2>&1' try { const ver = execSync(versionCmd, { encoding: 'utf-8', timeout: 5000 }).trim() serverLog.info(`[connect-agent] codex version: ${ver}`) @@ -314,11 +383,10 @@ async function connectCodexCli(): Promise { return { connected: false, models: [], error: 'Codex CLI not responding' } } - // Read models from Codex CLI's local models cache + // Read models from Codex CLI's local models cache (best-effort) let models: GroupedModel[] = [] - const cachePath = join(homedir(), '.codex', 'models_cache.json') - serverLog.info(`[connect-agent] codex models cache: "${cachePath}" (exists=${existsSync(cachePath)})`) - + const codexHome = process.env.CODEX_HOME || join(homedir(), '.codex') + const cachePath = join(codexHome, 'models_cache.json') try { const raw = await readFile(cachePath, 'utf-8') const cache = JSON.parse(raw) as { @@ -330,7 +398,6 @@ async function connectCodexCli(): Promise { priority: number }> } - if (cache.models && Array.isArray(cache.models)) { models = cache.models .filter((m) => m.visibility === 'list') @@ -342,18 +409,22 @@ async function connectCodexCli(): Promise { provider: 'openai' as const, })) } - } catch (cacheErr) { - serverLog.info(`[connect-agent] codex cache read failed: ${cacheErr instanceof Error ? cacheErr.message : cacheErr}`) + } catch { + serverLog.info(`[connect-agent] codex models cache not available`) } + // Fallback: parse models from Codex's bundled latest-model.md reference if (models.length === 0) { - serverLog.info('[connect-agent] codex: no models found') - return { connected: false, models: [], error: 'No models found. Try running codex once to populate the model cache.' } + models = await parseCodexLatestModelMd(codexHome) + if (models.length > 0) { + serverLog.info(`[connect-agent] codex models loaded from latest-model.md: ${models.length}`) + } } serverLog.info(`[connect-agent] codex connected, ${models.length} models found`) const codexInfo = await buildCodexConnectionInfo() - return { connected: true, models, ...codexInfo } + const warning = models.length === 0 ? 'No models found. Try running codex once to populate the model cache.' : undefined + return { connected: true, models, warning, ...codexInfo } } catch (error) { const msg = error instanceof Error ? error.message : 'Failed to connect' serverLog.error(`[connect-agent] codex connection error: ${msg}`) @@ -377,7 +448,7 @@ async function resolveOpencodeBinary(): Promise { serverLog.info(`[resolve-opencode] PATH lookup: ${cmd}`) const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim().split(/\r?\n/)[0]?.trim() serverLog.info(`[resolve-opencode] PATH result: "${result}" (exists=${result ? existsSync(result) : false})`) - if (result && existsSync(result)) return result + if (result && existsSync(result)) return resolveWinExtension(result) } catch (err) { serverLog.info(`[resolve-opencode] PATH lookup failed: ${err instanceof Error ? err.message : err}`) } @@ -390,9 +461,16 @@ async function resolveOpencodeBinary(): Promise { const prefix = execSync(npmCmd, { encoding: 'utf-8', timeout: 5000 }).trim() serverLog.info(`[resolve-opencode] npm global prefix: "${prefix}"`) if (prefix) { - const bin = isWin ? join(prefix, 'opencode.cmd') : join(prefix, 'bin', 'opencode') - serverLog.info(`[resolve-opencode] npm global bin: "${bin}" (exists=${existsSync(bin)})`) - if (existsSync(bin)) return bin + if (isWin) { + for (const bin of winNpmCandidates(prefix, 'opencode')) { + serverLog.info(`[resolve-opencode] npm global bin: "${bin}" (exists=${existsSync(bin)})`) + if (existsSync(bin)) return bin + } + } else { + const bin = join(prefix, 'bin', 'opencode') + serverLog.info(`[resolve-opencode] npm global bin: "${bin}" (exists=${existsSync(bin)})`) + if (existsSync(bin)) return bin + } } } catch (err) { serverLog.info(`[resolve-opencode] npm prefix -g failed: ${err instanceof Error ? err.message : err}`) @@ -405,12 +483,12 @@ async function resolveOpencodeBinary(): Promise { const home = homedir() const candidates = isWin ? [ - // npm global - join(process.env.APPDATA || '', 'npm', 'opencode.cmd'), - join(process.env.ProgramFiles || '', 'nodejs', 'opencode.cmd'), + // npm global (.cmd + .ps1) + ...winNpmCandidates(join(process.env.APPDATA || '', 'npm'), 'opencode'), + ...winNpmCandidates(join(process.env.ProgramFiles || '', 'nodejs'), 'opencode'), // nvm-windows / fnm - join(process.env.NVM_SYMLINK || '', 'opencode.cmd'), - join(process.env.FNM_MULTISHELL_PATH || '', 'opencode.cmd'), + ...winNpmCandidates(join(process.env.NVM_SYMLINK || ''), 'opencode'), + ...winNpmCandidates(join(process.env.FNM_MULTISHELL_PATH || ''), 'opencode'), // Scoop join(home, 'scoop', 'shims', 'opencode.exe'), join(process.env.LOCALAPPDATA || '', 'Programs', 'opencode', 'opencode.exe'), diff --git a/server/api/ai/generate.ts b/server/api/ai/generate.ts index 97650fc0..521669b3 100644 --- a/server/api/ai/generate.ts +++ b/server/api/ai/generate.ts @@ -3,6 +3,7 @@ import { resolveClaudeCli } from '../../utils/resolve-claude-cli' import { runCodexExec } from '../../utils/codex-client' import { buildClaudeAgentEnv, + buildSpawnClaudeCodeProcess, getClaudeAgentDebugFilePath, } from '../../utils/resolve-claude-agent-env' import { formatOpenCodeError } from './chat' @@ -25,7 +26,7 @@ interface GenerateBody { export default defineEventHandler(async (event) => { const body = await readBody(event) - if (!body?.message || !body?.system) { + if (!body?.message || body?.system == null) { setResponseHeaders(event, { 'Content-Type': 'application/json' }) return { error: 'Missing required fields: system, message' } } @@ -74,6 +75,7 @@ async function generateViaAgentSDK(body: GenerateBody, model?: string): Promise< env, ...(debugFile ? { debugFile } : {}), ...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}), + ...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}), }, }) @@ -247,7 +249,6 @@ async function generateViaOpenCode(body: GenerateBody, model?: string): Promise< } if (texts.length === 0) { - console.warn('[AI] OpenCode generate returned no text parts. Response:', JSON.stringify(result).slice(0, 500)) return { error: 'OpenCode returned an empty response. The model may not have generated any output.' } } diff --git a/server/api/ai/validate.ts b/server/api/ai/validate.ts index f393cc0d..e0b8d881 100644 --- a/server/api/ai/validate.ts +++ b/server/api/ai/validate.ts @@ -2,6 +2,7 @@ import { defineEventHandler, readBody, setResponseHeaders } from 'h3' import { resolveClaudeCli } from '../../utils/resolve-claude-cli' import { buildClaudeAgentEnv, + buildSpawnClaudeCodeProcess, getClaudeAgentDebugFilePath, } from '../../utils/resolve-claude-agent-env' import { writeFile, mkdtemp, rm } from 'node:fs/promises' @@ -125,6 +126,7 @@ CRITICAL: Your ENTIRE response must be a single JSON object. No markdown, no exp env, ...(debugFile ? { debugFile } : {}), ...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}), + ...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}), }, }) diff --git a/server/opencode/client.ts b/server/opencode/client.ts new file mode 100644 index 00000000..d517a09b --- /dev/null +++ b/server/opencode/client.ts @@ -0,0 +1,39 @@ +export * from "./gen/types.gen" + +import { createClient } from "./gen/client/client.gen" +import { type Config } from "./gen/client/types.gen" +import { OpencodeClient } from "./gen/sdk.gen" +export { type Config as OpencodeClientConfig, OpencodeClient } + +export function createOpencodeClient(config?: Config & { directory?: string; experimental_workspaceID?: string }) { + if (!config?.fetch) { + const customFetch: any = (req: any) => { + // @ts-ignore + req.timeout = false + return fetch(req) + } + config = { + ...config, + fetch: customFetch, + } + } + + if (config?.directory) { + const isNonASCII = /[^\x00-\x7F]/.test(config.directory) + const encodedDirectory = isNonASCII ? encodeURIComponent(config.directory) : config.directory + config.headers = { + ...config.headers, + "x-opencode-directory": encodedDirectory, + } + } + + if (config?.experimental_workspaceID) { + config.headers = { + ...config.headers, + "x-opencode-workspace": config.experimental_workspaceID, + } + } + + const client = createClient(config) + return new OpencodeClient({ client }) +} diff --git a/server/opencode/gen/client.gen.ts b/server/opencode/gen/client.gen.ts new file mode 100644 index 00000000..17d12c76 --- /dev/null +++ b/server/opencode/gen/client.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from "./client/index" +import type { ClientOptions as ClientOptions2 } from "./types.gen" + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T> + +export const client = createClient(createConfig({ baseUrl: "http://localhost:4096" })) diff --git a/server/opencode/gen/client/client.gen.ts b/server/opencode/gen/client/client.gen.ts new file mode 100644 index 00000000..30237375 --- /dev/null +++ b/server/opencode/gen/client/client.gen.ts @@ -0,0 +1,285 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from "../core/serverSentEvents.gen" +import type { HttpMethod } from "../core/types.gen" +import { getValidRequestBody } from "../core/utils.gen" +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen" +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from "./utils.gen" + +type ReqInit = Omit & { + body?: any + headers: ReturnType +} + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config) + + const getConfig = (): Config => ({ ..._config }) + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config) + return getConfig() + } + + const interceptors = createInterceptors() + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + } + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }) + } + + if (opts.requestValidator) { + await opts.requestValidator(opts) + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === "") { + opts.headers.delete("Content-Type") + } + + const url = buildUrl(opts) + + return { opts, url } + } + + const request: Client["request"] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options) + const requestInit: ReqInit = { + redirect: "follow", + ...opts, + body: getValidRequestBody(opts), + } + + let request = new Request(url, requestInit) + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts) + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch! + let response: Response + + try { + response = await _fetch(request) + } catch (error) { + // Handle fetch exceptions (AbortError, network errors, etc.) + let finalError = error + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, undefined as any, request, opts)) as unknown + } + } + + finalError = finalError || ({} as unknown) + + if (opts.throwOnError) { + throw finalError + } + + // Return error response + return opts.responseStyle === "data" + ? undefined + : { + error: finalError, + request, + response: undefined as any, + } + } + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts) + } + } + + const result = { + request, + response, + } + + if (response.ok) { + const parseAs = + (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json" + + if (response.status === 204 || response.headers.get("Content-Length") === "0") { + let emptyData: any + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "text": + emptyData = await response[parseAs]() + break + case "formData": + emptyData = new FormData() + break + case "stream": + emptyData = response.body + break + case "json": + default: + emptyData = {} + break + } + return opts.responseStyle === "data" + ? emptyData + : { + data: emptyData, + ...result, + } + } + + let data: any + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "formData": + case "text": + data = await response[parseAs]() + break + case "json": { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text() + data = text ? JSON.parse(text) : {} + break + } + case "stream": + return opts.responseStyle === "data" + ? response.body + : { + data: response.body, + ...result, + } + } + + if (parseAs === "json") { + if (opts.responseValidator) { + await opts.responseValidator(data) + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data) + } + } + + return opts.responseStyle === "data" + ? data + : { + data, + ...result, + } + } + + const textError = await response.text() + let jsonError: unknown + + try { + jsonError = JSON.parse(textError) + } catch { + // noop + } + + const error = jsonError ?? textError + let finalError = error + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string + } + } + + finalError = finalError || ({} as string) + + if (opts.throwOnError) { + throw finalError + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === "data" + ? undefined + : { + error: finalError, + ...result, + } + } + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }) + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options) + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init) + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts) + } + } + return request + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }) + } + + return { + buildUrl, + connect: makeMethodFn("CONNECT"), + delete: makeMethodFn("DELETE"), + get: makeMethodFn("GET"), + getConfig, + head: makeMethodFn("HEAD"), + interceptors, + options: makeMethodFn("OPTIONS"), + patch: makeMethodFn("PATCH"), + post: makeMethodFn("POST"), + put: makeMethodFn("PUT"), + request, + setConfig, + sse: { + connect: makeSseFn("CONNECT"), + delete: makeSseFn("DELETE"), + get: makeSseFn("GET"), + head: makeSseFn("HEAD"), + options: makeSseFn("OPTIONS"), + patch: makeSseFn("PATCH"), + post: makeSseFn("POST"), + put: makeSseFn("PUT"), + trace: makeSseFn("TRACE"), + }, + trace: makeMethodFn("TRACE"), + } as Client +} diff --git a/server/opencode/gen/client/index.ts b/server/opencode/gen/client/index.ts new file mode 100644 index 00000000..77aabe6f --- /dev/null +++ b/server/opencode/gen/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from "../core/auth.gen" +export type { QuerySerializerOptions } from "../core/bodySerializer.gen" +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from "../core/bodySerializer.gen" +export { buildClientParams } from "../core/params.gen" +export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen" +export { createClient } from "./client.gen" +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from "./types.gen" +export { createConfig, mergeHeaders } from "./utils.gen" diff --git a/server/opencode/gen/client/types.gen.ts b/server/opencode/gen/client/types.gen.ts new file mode 100644 index 00000000..90a6ad28 --- /dev/null +++ b/server/opencode/gen/client/types.gen.ts @@ -0,0 +1,202 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from "../core/auth.gen" +import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen" +import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen" +import type { Middleware } from "./utils.gen" + +export type ResponseStyle = "data" | "fields" + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T["baseUrl"] + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text" + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T["throwOnError"] +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = "fields", + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle + throwOnError: ThrowOnError + }>, + Pick< + ServerSentEventsOptions, + "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay" + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown + path?: Record + query?: Record + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray + url: Url +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = "fields", + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = "fields", +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends "data" + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData + request: Request + response: Response + } + > + : Promise< + TResponseStyle extends "data" + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData + error: undefined + } + | { + data: undefined + error: TError extends Record ? TError[keyof TError] : TError + } + ) & { + request: Request + response: Response + } + > + +export interface ClientOptions { + baseUrl?: string + responseStyle?: ResponseStyle + throwOnError?: boolean +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method">, +) => RequestResult + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method">, +) => Promise> + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method"> & + Pick>, "method">, +) => RequestResult + +type BuildUrlFn = < + TData extends { + body?: unknown + path?: Record + query?: Record + url: string + }, +>( + options: TData & Options, +) => string + +export type Client = CoreClient & { + interceptors: Middleware +} + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T> + +export interface TDataShape { + body?: unknown + headers?: unknown + path?: unknown + query?: unknown + url: string +} + +type OmitKeys = Pick> + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = "fields", +> = OmitKeys, "body" | "path" | "query" | "url"> & + ([TData] extends [never] ? unknown : Omit) diff --git a/server/opencode/gen/client/utils.gen.ts b/server/opencode/gen/client/utils.gen.ts new file mode 100644 index 00000000..65007692 --- /dev/null +++ b/server/opencode/gen/client/utils.gen.ts @@ -0,0 +1,289 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from "../core/auth.gen" +import type { QuerySerializerOptions } from "../core/bodySerializer.gen" +import { jsonBodySerializer } from "../core/bodySerializer.gen" +import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen" +import { getUrl } from "../core/utils.gen" +import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen" + +export const createQuerySerializer = ({ parameters = {}, ...args }: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = [] + if (queryParams && typeof queryParams === "object") { + for (const name in queryParams) { + const value = queryParams[name] + + if (value === undefined || value === null) { + continue + } + + const options = parameters[name] || args + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: "form", + value, + ...options.array, + }) + if (serializedArray) search.push(serializedArray) + } else if (typeof value === "object") { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: "deepObject", + value: value as Record, + ...options.object, + }) + if (serializedObject) search.push(serializedObject) + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }) + if (serializedPrimitive) search.push(serializedPrimitive) + } + } + } + return search.join("&") + } + return querySerializer +} + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return "stream" + } + + const cleanContent = contentType.split(";")[0]?.trim() + + if (!cleanContent) { + return + } + + if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) { + return "json" + } + + if (cleanContent === "multipart/form-data") { + return "formData" + } + + if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) { + return "blob" + } + + if (cleanContent.startsWith("text/")) { + return "text" + } + + return +} + +const checkForExistence = ( + options: Pick & { + headers: Headers + }, + name?: string, +): boolean => { + if (!name) { + return false + } + if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) { + return true + } + return false +} + +export const setAuthParams = async ({ + security, + ...options +}: Pick, "security"> & + Pick & { + headers: Headers + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue + } + + const token = await getAuthToken(auth, options.auth) + + if (!token) { + continue + } + + const name = auth.name ?? "Authorization" + + switch (auth.in) { + case "query": + if (!options.query) { + options.query = {} + } + options.query[name] = token + break + case "cookie": + options.headers.append("Cookie", `${name}=${token}`) + break + case "header": + default: + options.headers.set(name, token) + break + } + } +} + +export const buildUrl: Client["buildUrl"] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === "function" + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }) + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b } + if (config.baseUrl?.endsWith("/")) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1) + } + config.headers = mergeHeaders(a.headers, b.headers) + return config +} + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = [] + headers.forEach((value, key) => { + entries.push([key, value]) + }) + return entries +} + +export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { + const mergedHeaders = new Headers() + for (const header of headers) { + if (!header) { + continue + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header) + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key) + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string) + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string)) + } + } + } + return mergedHeaders +} + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise + +type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise + +class Interceptors { + fns: Array = [] + + clear(): void { + this.fns = [] + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id) + if (this.fns[index]) { + this.fns[index] = null + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id) + return Boolean(this.fns[index]) + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === "number") { + return this.fns[id] ? id : -1 + } + return this.fns.indexOf(id) + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id) + if (this.fns[index]) { + this.fns[index] = fn + return id + } + return false + } + + use(fn: Interceptor): number { + this.fns.push(fn) + return this.fns.length - 1 + } +} + +export interface Middleware { + error: Interceptors> + request: Interceptors> + response: Interceptors> +} + +export const createInterceptors = (): Middleware => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}) + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: "form", + }, + object: { + explode: true, + style: "deepObject", + }, +}) + +const defaultHeaders = { + "Content-Type": "application/json", +} + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: "auto", + querySerializer: defaultQuerySerializer, + ...override, +}) diff --git a/server/opencode/gen/core/auth.gen.ts b/server/opencode/gen/core/auth.gen.ts new file mode 100644 index 00000000..bc7b230f --- /dev/null +++ b/server/opencode/gen/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: "header" | "query" | "cookie" + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string + scheme?: "basic" | "bearer" + type: "apiKey" | "http" +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === "function" ? await callback(auth) : callback + + if (!token) { + return + } + + if (auth.scheme === "bearer") { + return `Bearer ${token}` + } + + if (auth.scheme === "basic") { + return `Basic ${btoa(token)}` + } + + return token +} diff --git a/server/opencode/gen/core/bodySerializer.gen.ts b/server/opencode/gen/core/bodySerializer.gen.ts new file mode 100644 index 00000000..24808ed1 --- /dev/null +++ b/server/opencode/gen/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen" + +export type QuerySerializer = (query: Record) => string + +export type BodySerializer = (body: any) => any + +type QuerySerializerOptionsObject = { + allowReserved?: boolean + array?: Partial> + object?: Partial> +} + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record +} + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === "string" || value instanceof Blob) { + data.append(key, value) + } else if (value instanceof Date) { + data.append(key, value.toISOString()) + } else { + data.append(key, JSON.stringify(value)) + } +} + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === "string") { + data.append(key, value) + } else { + data.append(key, JSON.stringify(value)) + } +} + +export const formDataBodySerializer = { + bodySerializer: | Array>>(body: T): FormData => { + const data = new FormData() + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)) + } else { + serializeFormDataPair(data, key, value) + } + }) + + return data + }, +} + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)), +} + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>(body: T): string => { + const data = new URLSearchParams() + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)) + } else { + serializeUrlSearchParamsPair(data, key, value) + } + }) + + return data.toString() + }, +} diff --git a/server/opencode/gen/core/params.gen.ts b/server/opencode/gen/core/params.gen.ts new file mode 100644 index 00000000..6e9d0b9a --- /dev/null +++ b/server/opencode/gen/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = "body" | "headers" | "path" | "query" + +export type Field = + | { + in: Exclude + /** + * Field name. This is the name we want the user to see and use. + */ + key: string + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string + } + | { + in: Extract + /** + * Key isn't required for bodies. + */ + key?: string + map?: string + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot + } + +export interface Fields { + allowExtra?: Partial> + args?: ReadonlyArray +} + +export type FieldsConfig = ReadonlyArray + +const extraPrefixesMap: Record = { + $body_: "body", + $headers_: "headers", + $path_: "path", + $query_: "query", +} +const extraPrefixes = Object.entries(extraPrefixesMap) + +type KeyMap = Map< + string, + | { + in: Slot + map?: string + } + | { + in?: never + map: Slot + } +> + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map() + } + + for (const config of fields) { + if ("in" in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }) + } + } else if ("key" in config) { + map.set(config.key, { + map: config.map, + }) + } else if (config.args) { + buildKeyMap(config.args, map) + } + } + + return map +} + +interface Params { + body: unknown + headers: Record + path: Record + query: Record +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === "object" && !Object.keys(value).length) { + delete params[slot as Slot] + } + } +} + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + } + + const map = buildKeyMap(fields) + + let config: FieldsConfig[number] | undefined + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index] + } + + if (!config) { + continue + } + + if ("in" in config) { + if (config.key) { + const field = map.get(config.key)! + const name = field.map || config.key + if (field.in) { + ;(params[field.in] as Record)[name] = arg + } + } else { + params.body = arg + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key) + + if (field) { + if (field.in) { + const name = field.map || key + ;(params[field.in] as Record)[name] = value + } else { + params[field.map] = value + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)) + + if (extra) { + const [prefix, slot] = extra + ;(params[slot] as Record)[key.slice(prefix.length)] = value + } else if ("allowExtra" in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + ;(params[slot as Slot] as Record)[key] = value + break + } + } + } + } + } + } + } + + stripEmptySlots(params) + + return params +} diff --git a/server/opencode/gen/core/pathSerializer.gen.ts b/server/opencode/gen/core/pathSerializer.gen.ts new file mode 100644 index 00000000..96be3bc5 --- /dev/null +++ b/server/opencode/gen/core/pathSerializer.gen.ts @@ -0,0 +1,167 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean + name: string +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean + style: T +} + +export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited" +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle +type MatrixStyle = "label" | "matrix" | "simple" +export type ObjectStyle = "form" | "deepObject" +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case "label": + return "." + case "matrix": + return ";" + case "simple": + return "," + default: + return "&" + } +} + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case "form": + return "," + case "pipeDelimited": + return "|" + case "spaceDelimited": + return "%20" + default: + return "," + } +} + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case "label": + return "." + case "matrix": + return ";" + case "simple": + return "," + default: + return "&" + } +} + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[] +}) => { + if (!explode) { + const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join( + separatorArrayNoExplode(style), + ) + switch (style) { + case "label": + return `.${joinedValues}` + case "matrix": + return `;${name}=${joinedValues}` + case "simple": + return joinedValues + default: + return `${name}=${joinedValues}` + } + } + + const separator = separatorArrayExplode(style) + const joinedValues = value + .map((v) => { + if (style === "label" || style === "simple") { + return allowReserved ? v : encodeURIComponent(v as string) + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }) + }) + .join(separator) + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues +} + +export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return "" + } + + if (typeof value === "object") { + throw new Error( + "Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.", + ) + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}` +} + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date + valueOnly?: boolean +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}` + } + + if (style !== "deepObject" && !explode) { + let values: string[] = [] + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)] + }) + const joinedValues = values.join(",") + switch (style) { + case "form": + return `${name}=${joinedValues}` + case "label": + return `.${joinedValues}` + case "matrix": + return `;${name}=${joinedValues}` + default: + return joinedValues + } + } + + const separator = separatorObjectExplode(style) + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === "deepObject" ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator) + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues +} diff --git a/server/opencode/gen/core/queryKeySerializer.gen.ts b/server/opencode/gen/core/queryKeySerializer.gen.ts new file mode 100644 index 00000000..320204ae --- /dev/null +++ b/server/opencode/gen/core/queryKeySerializer.gen.ts @@ -0,0 +1,111 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue } + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === "function" || typeof value === "symbol") { + return undefined + } + if (typeof value === "bigint") { + return value.toString() + } + if (value instanceof Date) { + return value.toISOString() + } + return value +} + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer) + if (json === undefined) { + return undefined + } + return JSON.parse(json) as JsonValue + } catch { + return undefined + } +} + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== "object") { + return false + } + const prototype = Object.getPrototypeOf(value as object) + return prototype === Object.prototype || prototype === null +} + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)) + const result: Record = {} + + for (const [key, value] of entries) { + const existing = result[key] + if (existing === undefined) { + result[key] = value + continue + } + + if (Array.isArray(existing)) { + ;(existing as string[]).push(value) + } else { + result[key] = [existing, value] + } + } + + return result +} + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null + } + + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return value + } + + if (value === undefined || typeof value === "function" || typeof value === "symbol") { + return undefined + } + + if (typeof value === "bigint") { + return value.toString() + } + + if (value instanceof Date) { + return value.toISOString() + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value) + } + + if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) { + return serializeSearchParams(value) + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value) + } + + return undefined +} diff --git a/server/opencode/gen/core/serverSentEvents.gen.ts b/server/opencode/gen/core/serverSentEvents.gen.ts new file mode 100644 index 00000000..6b389c18 --- /dev/null +++ b/server/opencode/gen/core/serverSentEvents.gen.ts @@ -0,0 +1,239 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from "./types.gen" + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void + serializedBody?: RequestInit["body"] + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise + url: string + } + +export interface StreamEvent { + data: TData + event?: string + id?: string + retry?: number +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator ? TData[keyof TData] : TData, TReturn, TNext> +} + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))) + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000 + let attempt = 0 + const signal = options.signal ?? new AbortController().signal + + while (true) { + if (signal.aborted) break + + attempt++ + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined) + + if (lastEventId !== undefined) { + headers.set("Last-Event-ID", lastEventId) + } + + try { + const requestInit: RequestInit = { + redirect: "follow", + ...options, + body: options.serializedBody, + headers, + signal, + } + let request = new Request(url, requestInit) + if (onRequest) { + request = await onRequest(url, requestInit) + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch + const response = await _fetch(request) + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`) + + if (!response.body) throw new Error("No body in SSE response") + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader() + + let buffer = "" + + const abortHandler = () => { + try { + reader.cancel() + } catch { + // noop + } + } + + signal.addEventListener("abort", abortHandler) + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += value + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n") + + const chunks = buffer.split("\n\n") + buffer = chunks.pop() ?? "" + + for (const chunk of chunks) { + const lines = chunk.split("\n") + const dataLines: Array = [] + let eventName: string | undefined + + for (const line of lines) { + if (line.startsWith("data:")) { + dataLines.push(line.replace(/^data:\s*/, "")) + } else if (line.startsWith("event:")) { + eventName = line.replace(/^event:\s*/, "") + } else if (line.startsWith("id:")) { + lastEventId = line.replace(/^id:\s*/, "") + } else if (line.startsWith("retry:")) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10) + if (!Number.isNaN(parsed)) { + retryDelay = parsed + } + } + } + + let data: unknown + let parsedJson = false + + if (dataLines.length) { + const rawData = dataLines.join("\n") + try { + data = JSON.parse(rawData) + parsedJson = true + } catch { + data = rawData + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data) + } + + if (responseTransformer) { + data = await responseTransformer(data) + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }) + + if (dataLines.length) { + yield data as any + } + } + } + } finally { + signal.removeEventListener("abort", abortHandler) + reader.releaseLock() + } + + break // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error) + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000) + await sleep(backoff) + } + } + } + + const stream = createStream() + + return { stream } +} diff --git a/server/opencode/gen/core/types.gen.ts b/server/opencode/gen/core/types.gen.ts new file mode 100644 index 00000000..624ba56a --- /dev/null +++ b/server/opencode/gen/core/types.gen.ts @@ -0,0 +1,86 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from "./auth.gen" +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen" + +export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace" + +export type Client = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn + getConfig: () => Config + request: RequestFn + setConfig: (config: Config) => Config +} & { + [K in HttpMethod]: MethodFn +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }) + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit["headers"] + | Record + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K] +} diff --git a/server/opencode/gen/core/utils.gen.ts b/server/opencode/gen/core/utils.gen.ts new file mode 100644 index 00000000..2d9ab7b0 --- /dev/null +++ b/server/opencode/gen/core/utils.gen.ts @@ -0,0 +1,137 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen" +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from "./pathSerializer.gen" + +export interface PathSerializer { + path: Record + url: string +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url + const matches = _url.match(PATH_PARAM_RE) + if (matches) { + for (const match of matches) { + let explode = false + let name = match.substring(1, match.length - 1) + let style: ArraySeparatorStyle = "simple" + + if (name.endsWith("*")) { + explode = true + name = name.substring(0, name.length - 1) + } + + if (name.startsWith(".")) { + name = name.substring(1) + style = "label" + } else if (name.startsWith(";")) { + name = name.substring(1) + style = "matrix" + } + + const value = path[name] + + if (value === undefined || value === null) { + continue + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })) + continue + } + + if (typeof value === "object") { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ) + continue + } + + if (style === "matrix") { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ) + continue + } + + const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string)) + url = url.replace(match, replaceValue) + } + } + return url +} + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string + path?: Record + query?: Record + querySerializer: QuerySerializer + url: string +}) => { + const pathUrl = _url.startsWith("/") ? _url : `/${_url}` + let url = (baseUrl ?? "") + pathUrl + if (path) { + url = defaultPathSerializer({ path, url }) + } + let search = query ? querySerializer(query) : "" + if (search.startsWith("?")) { + search = search.substring(1) + } + if (search) { + url += `?${search}` + } + return url +} + +export function getValidRequestBody(options: { + body?: unknown + bodySerializer?: BodySerializer | null + serializedBody?: unknown +}) { + const hasBody = options.body !== undefined + const isSerializedBody = hasBody && options.bodySerializer + + if (isSerializedBody) { + if ("serializedBody" in options) { + const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== "" + + return hasSerializedBody ? options.serializedBody : null + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== "" ? options.body : null + } + + // plain/text body + if (hasBody) { + return options.body + } + + // no body was provided + return undefined +} diff --git a/server/opencode/gen/sdk.gen.ts b/server/opencode/gen/sdk.gen.ts new file mode 100644 index 00000000..b90419b5 --- /dev/null +++ b/server/opencode/gen/sdk.gen.ts @@ -0,0 +1,4033 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { client } from "./client.gen" +import { buildClientParams, type Client, type Options as Options2, type TDataShape } from "./client/index" +import type { + AgentPartInput, + AppAgentsResponses, + AppLogErrors, + AppLogResponses, + AppSkillsResponses, + Auth as Auth3, + AuthRemoveErrors, + AuthRemoveResponses, + AuthSetErrors, + AuthSetResponses, + CommandListResponses, + Config as Config3, + ConfigGetResponses, + ConfigProvidersResponses, + ConfigUpdateErrors, + ConfigUpdateResponses, + EventSubscribeResponses, + EventTuiCommandExecute, + EventTuiPromptAppend, + EventTuiSessionSelect, + EventTuiToastShow, + ExperimentalResourceListResponses, + ExperimentalSessionListResponses, + ExperimentalWorkspaceCreateErrors, + ExperimentalWorkspaceCreateResponses, + ExperimentalWorkspaceListResponses, + ExperimentalWorkspaceRemoveErrors, + ExperimentalWorkspaceRemoveResponses, + FileListResponses, + FilePartInput, + FilePartSource, + FileReadResponses, + FileStatusResponses, + FindFilesResponses, + FindSymbolsResponses, + FindTextResponses, + FormatterStatusResponses, + GlobalConfigGetResponses, + GlobalConfigUpdateErrors, + GlobalConfigUpdateResponses, + GlobalDisposeResponses, + GlobalEventResponses, + GlobalHealthResponses, + InstanceDisposeResponses, + LspStatusResponses, + McpAddErrors, + McpAddResponses, + McpAuthAuthenticateErrors, + McpAuthAuthenticateResponses, + McpAuthCallbackErrors, + McpAuthCallbackResponses, + McpAuthRemoveErrors, + McpAuthRemoveResponses, + McpAuthStartErrors, + McpAuthStartResponses, + McpConnectResponses, + McpDisconnectResponses, + McpLocalConfig, + McpRemoteConfig, + McpStatusResponses, + OutputFormat, + Part as Part2, + PartDeleteErrors, + PartDeleteResponses, + PartUpdateErrors, + PartUpdateResponses, + PathGetResponses, + PermissionListResponses, + PermissionReplyErrors, + PermissionReplyResponses, + PermissionRespondErrors, + PermissionRespondResponses, + PermissionRuleset, + ProjectCurrentResponses, + ProjectInitGitResponses, + ProjectListResponses, + ProjectUpdateErrors, + ProjectUpdateResponses, + ProviderAuthResponses, + ProviderListResponses, + ProviderOauthAuthorizeErrors, + ProviderOauthAuthorizeResponses, + ProviderOauthCallbackErrors, + ProviderOauthCallbackResponses, + PtyConnectErrors, + PtyConnectResponses, + PtyCreateErrors, + PtyCreateResponses, + PtyGetErrors, + PtyGetResponses, + PtyListResponses, + PtyRemoveErrors, + PtyRemoveResponses, + PtyUpdateErrors, + PtyUpdateResponses, + QuestionAnswer, + QuestionListResponses, + QuestionRejectErrors, + QuestionRejectResponses, + QuestionReplyErrors, + QuestionReplyResponses, + SessionAbortErrors, + SessionAbortResponses, + SessionChildrenErrors, + SessionChildrenResponses, + SessionCommandErrors, + SessionCommandResponses, + SessionCreateErrors, + SessionCreateResponses, + SessionDeleteErrors, + SessionDeleteMessageErrors, + SessionDeleteMessageResponses, + SessionDeleteResponses, + SessionDiffResponses, + SessionForkResponses, + SessionGetErrors, + SessionGetResponses, + SessionInitErrors, + SessionInitResponses, + SessionListResponses, + SessionMessageErrors, + SessionMessageResponses, + SessionMessagesErrors, + SessionMessagesResponses, + SessionPromptAsyncErrors, + SessionPromptAsyncResponses, + SessionPromptErrors, + SessionPromptResponses, + SessionRevertErrors, + SessionRevertResponses, + SessionShareErrors, + SessionShareResponses, + SessionShellErrors, + SessionShellResponses, + SessionStatusErrors, + SessionStatusResponses, + SessionSummarizeErrors, + SessionSummarizeResponses, + SessionTodoErrors, + SessionTodoResponses, + SessionUnrevertErrors, + SessionUnrevertResponses, + SessionUnshareErrors, + SessionUnshareResponses, + SessionUpdateErrors, + SessionUpdateResponses, + SubtaskPartInput, + TextPartInput, + ToolIdsErrors, + ToolIdsResponses, + ToolListErrors, + ToolListResponses, + TuiAppendPromptErrors, + TuiAppendPromptResponses, + TuiClearPromptResponses, + TuiControlNextResponses, + TuiControlResponseResponses, + TuiExecuteCommandErrors, + TuiExecuteCommandResponses, + TuiOpenHelpResponses, + TuiOpenModelsResponses, + TuiOpenSessionsResponses, + TuiOpenThemesResponses, + TuiPublishErrors, + TuiPublishResponses, + TuiSelectSessionErrors, + TuiSelectSessionResponses, + TuiShowToastResponses, + TuiSubmitPromptResponses, + VcsGetResponses, + WorktreeCreateErrors, + WorktreeCreateInput, + WorktreeCreateResponses, + WorktreeListResponses, + WorktreeRemoveErrors, + WorktreeRemoveInput, + WorktreeRemoveResponses, + WorktreeResetErrors, + WorktreeResetInput, + WorktreeResetResponses, +} from "./types.gen" + +export type Options = Options2< + TData, + ThrowOnError +> & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record +} + +class HeyApiClient { + protected client: Client + + constructor(args?: { client?: Client }) { + this.client = args?.client ?? client + } +} + +class HeyApiRegistry { + private readonly defaultKey = "default" + + private readonly instances: Map = new Map() + + get(key?: string): T { + const instance = this.instances.get(key ?? this.defaultKey) + if (!instance) { + throw new Error(`No SDK client found. Create one with "new OpencodeClient()" to fix this error.`) + } + return instance + } + + set(value: T, key?: string): void { + this.instances.set(key ?? this.defaultKey, value) + } +} + +export class Config extends HeyApiClient { + /** + * Get global configuration + * + * Retrieve the current global OpenCode configuration settings and preferences. + */ + public get(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/config", + ...options, + }) + } + + /** + * Update global configuration + * + * Update global OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }]) + return (options?.client ?? this.client).patch({ + url: "/global/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Global extends HeyApiClient { + /** + * Get health + * + * Get health information about the OpenCode server. + */ + public health(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/health", + ...options, + }) + } + + /** + * Get global events + * + * Subscribe to global events from the OpenCode system using server-sent events. + */ + public event(options?: Options) { + return (options?.client ?? this.client).sse.get({ + url: "/global/event", + ...options, + }) + } + + /** + * Dispose instance + * + * Clean up and dispose all OpenCode instances, releasing all resources. + */ + public dispose(options?: Options) { + return (options?.client ?? this.client).post({ + url: "/global/dispose", + ...options, + }) + } + + private _config?: Config + get config(): Config { + return (this._config ??= new Config({ client: this.client })) + } +} + +export class Auth extends HeyApiClient { + /** + * Remove auth credentials + * + * Remove authentication credentials + */ + public remove( + parameters: { + providerID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) + return (options?.client ?? this.client).delete({ + url: "/auth/{providerID}", + ...options, + ...params, + }) + } + + /** + * Set auth credentials + * + * Set authentication credentials + */ + public set( + parameters: { + providerID: string + auth?: Auth3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { key: "auth", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/auth/{providerID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Project extends HeyApiClient { + /** + * List all projects + * + * Get a list of projects that have been opened with OpenCode. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project", + ...options, + ...params, + }) + } + + /** + * Get current project + * + * Retrieve the currently active project that OpenCode is working with. + */ + public current( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project/current", + ...options, + ...params, + }) + } + + /** + * Initialize git repository + * + * Create a git repository for the current project and return the refreshed project info. + */ + public initGit( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/project/git/init", + ...options, + ...params, + }) + } + + /** + * Update project + * + * Update project properties such as name, icon, and commands. + */ + public update( + parameters: { + projectID: string + directory?: string + workspace?: string + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "icon" }, + { in: "body", key: "commands" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/project/{projectID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Pty extends HeyApiClient { + /** + * List PTY sessions + * + * Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty", + ...options, + ...params, + }) + } + + /** + * Create PTY session + * + * Create a new pseudo-terminal (PTY) session for running shell commands and processes. + */ + public create( + parameters?: { + directory?: string + workspace?: string + command?: string + args?: Array + cwd?: string + title?: string + env?: { + [key: string]: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "command" }, + { in: "body", key: "args" }, + { in: "body", key: "cwd" }, + { in: "body", key: "title" }, + { in: "body", key: "env" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/pty", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Remove PTY session + * + * Remove and terminate a specific pseudo-terminal (PTY) session. + */ + public remove( + parameters: { + ptyID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/pty/{ptyID}", + ...options, + ...params, + }) + } + + /** + * Get PTY session + * + * Retrieve detailed information about a specific pseudo-terminal (PTY) session. + */ + public get( + parameters: { + ptyID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty/{ptyID}", + ...options, + ...params, + }) + } + + /** + * Update PTY session + * + * Update properties of an existing pseudo-terminal (PTY) session. + */ + public update( + parameters: { + ptyID: string + directory?: string + workspace?: string + title?: string + size?: { + rows: number + cols: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "size" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/pty/{ptyID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Connect to PTY session + * + * Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time. + */ + public connect( + parameters: { + ptyID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty/{ptyID}/connect", + ...options, + ...params, + }) + } +} + +export class Config2 extends HeyApiClient { + /** + * Get configuration + * + * Retrieve the current OpenCode configuration settings and preferences. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config", + ...options, + ...params, + }) + } + + /** + * Update configuration + * + * Update OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + directory?: string + workspace?: string + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "config", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List config providers + * + * Get a list of all configured AI providers and their default models. + */ + public providers( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config/providers", + ...options, + ...params, + }) + } +} + +export class Tool extends HeyApiClient { + /** + * List tool IDs + * + * Get a list of all available tool IDs, including both built-in tools and dynamically registered tools. + */ + public ids( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/tool/ids", + ...options, + ...params, + }) + } + + /** + * List tools + * + * Get a list of available tools with their JSON schema parameters for a specific provider and model combination. + */ + public list( + parameters: { + directory?: string + workspace?: string + provider: string + model: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "provider" }, + { in: "query", key: "model" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/tool", + ...options, + ...params, + }) + } +} + +export class Workspace extends HeyApiClient { + /** + * List workspaces + * + * List all workspaces. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/workspace", + ...options, + ...params, + }) + } + + /** + * Create workspace + * + * Create a workspace for the current project. + */ + public create( + parameters?: { + directory?: string + workspace?: string + id?: string + type?: string + branch?: string | null + extra?: unknown | null + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "type" }, + { in: "body", key: "branch" }, + { in: "body", key: "extra" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceCreateResponses, + ExperimentalWorkspaceCreateErrors, + ThrowOnError + >({ + url: "/experimental/workspace", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Remove workspace + * + * Remove an existing workspace. + */ + public remove( + parameters: { + id: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "id" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceRemoveErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}", + ...options, + ...params, + }) + } +} + +export class Session extends HeyApiClient { + /** + * List sessions + * + * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default. + */ + public list( + parameters?: { + directory?: string + workspace?: string + roots?: boolean + start?: number + cursor?: number + search?: string + limit?: number + archived?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "cursor" }, + { in: "query", key: "search" }, + { in: "query", key: "limit" }, + { in: "query", key: "archived" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/session", + ...options, + ...params, + }) + } +} + +export class Resource extends HeyApiClient { + /** + * Get MCP resources + * + * Get all available MCP resources from connected servers. Optionally filter by name. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/resource", + ...options, + ...params, + }) + } +} + +export class Experimental extends HeyApiClient { + private _workspace?: Workspace + get workspace(): Workspace { + return (this._workspace ??= new Workspace({ client: this.client })) + } + + private _session?: Session + get session(): Session { + return (this._session ??= new Session({ client: this.client })) + } + + private _resource?: Resource + get resource(): Resource { + return (this._resource ??= new Resource({ client: this.client })) + } +} + +export class Worktree extends HeyApiClient { + /** + * Remove worktree + * + * Remove a git worktree and delete its branch. + */ + public remove( + parameters?: { + directory?: string + workspace?: string + worktreeRemoveInput?: WorktreeRemoveInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "worktreeRemoveInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/experimental/worktree", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List worktrees + * + * List all sandbox worktrees for the current project. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/worktree", + ...options, + ...params, + }) + } + + /** + * Create worktree + * + * Create a new git worktree for the current project and run any configured startup scripts. + */ + public create( + parameters?: { + directory?: string + workspace?: string + worktreeCreateInput?: WorktreeCreateInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "worktreeCreateInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/worktree", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Reset worktree + * + * Reset a worktree branch to the primary default branch. + */ + public reset( + parameters?: { + directory?: string + workspace?: string + worktreeResetInput?: WorktreeResetInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "worktreeResetInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/worktree/reset", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Session2 extends HeyApiClient { + /** + * List sessions + * + * Get a list of all OpenCode sessions, sorted by most recently updated. + */ + public list( + parameters?: { + directory?: string + workspace?: string + roots?: boolean + start?: number + search?: string + limit?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "search" }, + { in: "query", key: "limit" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session", + ...options, + ...params, + }) + } + + /** + * Create session + * + * Create a new OpenCode session for interacting with AI assistants and managing conversations. + */ + public create( + parameters?: { + directory?: string + workspace?: string + parentID?: string + title?: string + permission?: PermissionRuleset + workspaceID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "parentID" }, + { in: "body", key: "title" }, + { in: "body", key: "permission" }, + { in: "body", key: "workspaceID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get session status + * + * Retrieve the current status of all sessions, including active, idle, and completed states. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/status", + ...options, + ...params, + }) + } + + /** + * Delete session + * + * Delete a session and permanently remove all associated data, including messages and history. + */ + public delete( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}", + ...options, + ...params, + }) + } + + /** + * Get session + * + * Retrieve detailed information about a specific OpenCode session. + */ + public get( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}", + ...options, + ...params, + }) + } + + /** + * Update session + * + * Update properties of an existing session, such as title or other metadata. + */ + public update( + parameters: { + sessionID: string + directory?: string + workspace?: string + title?: string + time?: { + archived?: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "time" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/session/{sessionID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get session children + * + * Retrieve all child sessions that were forked from the specified parent session. + */ + public children( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/children", + ...options, + ...params, + }) + } + + /** + * Get session todos + * + * Retrieve the todo list associated with a specific session, showing tasks and action items. + */ + public todo( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/todo", + ...options, + ...params, + }) + } + + /** + * Initialize session + * + * Analyze the current application and create an AGENTS.md file with project-specific agent configurations. + */ + public init( + parameters: { + sessionID: string + directory?: string + workspace?: string + modelID?: string + providerID?: string + messageID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "modelID" }, + { in: "body", key: "providerID" }, + { in: "body", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/init", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Fork session + * + * Create a new session by forking an existing session at a specific message point. + */ + public fork( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/fork", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Abort session + * + * Abort an active session and stop any ongoing AI processing or command execution. + */ + public abort( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/abort", + ...options, + ...params, + }) + } + + /** + * Unshare session + * + * Remove the shareable link for a session, making it private again. + */ + public unshare( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}/share", + ...options, + ...params, + }) + } + + /** + * Share session + * + * Create a shareable link for a session, allowing others to view the conversation. + */ + public share( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/share", + ...options, + ...params, + }) + } + + /** + * Get message diff + * + * Get the file changes (diff) that resulted from a specific user message in the session. + */ + public diff( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/diff", + ...options, + ...params, + }) + } + + /** + * Summarize session + * + * Generate a concise summary of the session using AI compaction to preserve key information. + */ + public summarize( + parameters: { + sessionID: string + directory?: string + workspace?: string + providerID?: string + modelID?: string + auto?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "providerID" }, + { in: "body", key: "modelID" }, + { in: "body", key: "auto" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/summarize", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get session messages + * + * Retrieve all messages in a session, including user prompts and AI responses. + */ + public messages( + parameters: { + sessionID: string + directory?: string + workspace?: string + limit?: number + before?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "limit" }, + { in: "query", key: "before" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/message", + ...options, + ...params, + }) + } + + /** + * Send message + * + * Create and send a new message to a session, streaming the AI response. + */ + public prompt( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "model" }, + { in: "body", key: "agent" }, + { in: "body", key: "noReply" }, + { in: "body", key: "tools" }, + { in: "body", key: "format" }, + { in: "body", key: "system" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/message", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Delete message + * + * Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message. + */ + public deleteMessage( + parameters: { + sessionID: string + messageID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + SessionDeleteMessageResponses, + SessionDeleteMessageErrors, + ThrowOnError + >({ + url: "/session/{sessionID}/message/{messageID}", + ...options, + ...params, + }) + } + + /** + * Get message + * + * Retrieve a specific message from a session by its message ID. + */ + public message( + parameters: { + sessionID: string + messageID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/message/{messageID}", + ...options, + ...params, + }) + } + + /** + * Send async message + * + * Create and send a new message to a session asynchronously, starting the session if needed and returning immediately. + */ + public promptAsync( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "model" }, + { in: "body", key: "agent" }, + { in: "body", key: "noReply" }, + { in: "body", key: "tools" }, + { in: "body", key: "format" }, + { in: "body", key: "system" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/prompt_async", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Send command + * + * Send a new command to a session for execution by the AI assistant. + */ + public command( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + agent?: string + model?: string + arguments?: string + command?: string + variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + }> + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "arguments" }, + { in: "body", key: "command" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/command", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Run shell command + * + * Execute a shell command within the session context and return the AI's response. + */ + public shell( + parameters: { + sessionID: string + directory?: string + workspace?: string + agent?: string + model?: { + providerID: string + modelID: string + } + command?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "command" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/shell", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Revert message + * + * Revert a specific message in a session, undoing its effects and restoring the previous state. + */ + public revert( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + partID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "partID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/revert", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Restore reverted messages + * + * Restore all previously reverted messages in a session. + */ + public unrevert( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/unrevert", + ...options, + ...params, + }) + } +} + +export class Part extends HeyApiClient { + /** + * Delete a part from a message + */ + public delete( + parameters: { + sessionID: string + messageID: string + partID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "path", key: "partID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}/message/{messageID}/part/{partID}", + ...options, + ...params, + }) + } + + /** + * Update a part in a message + */ + public update( + parameters: { + sessionID: string + messageID: string + partID: string + directory?: string + workspace?: string + part?: Part2 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "path", key: "partID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "part", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/session/{sessionID}/message/{messageID}/part/{partID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Permission extends HeyApiClient { + /** + * Respond to permission + * + * Approve or deny a permission request from the AI assistant. + * + * @deprecated + */ + public respond( + parameters: { + sessionID: string + permissionID: string + directory?: string + workspace?: string + response?: "once" | "always" | "reject" + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "permissionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "response" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/permissions/{permissionID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Respond to permission request + * + * Approve or deny a permission request from the AI assistant. + */ + public reply( + parameters: { + requestID: string + directory?: string + workspace?: string + reply?: "once" | "always" | "reject" + message?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "requestID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "reply" }, + { in: "body", key: "message" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/permission/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List pending permissions + * + * Get all pending permission requests across all sessions. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/permission", + ...options, + ...params, + }) + } +} + +export class Question extends HeyApiClient { + /** + * List pending questions + * + * Get all pending question requests across all sessions. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/question", + ...options, + ...params, + }) + } + + /** + * Reply to question request + * + * Provide answers to a question request from the AI assistant. + */ + public reply( + parameters: { + requestID: string + directory?: string + workspace?: string + answers?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "requestID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "answers" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/question/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Reject question request + * + * Reject a question request from the AI assistant. + */ + public reject( + parameters: { + requestID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "requestID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/question/{requestID}/reject", + ...options, + ...params, + }) + } +} + +export class Oauth extends HeyApiClient { + /** + * OAuth authorize + * + * Initiate OAuth authorization for a specific AI provider to get an authorization URL. + */ + public authorize( + parameters: { + providerID: string + directory?: string + workspace?: string + method?: number + inputs?: { + [key: string]: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "method" }, + { in: "body", key: "inputs" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ProviderOauthAuthorizeResponses, + ProviderOauthAuthorizeErrors, + ThrowOnError + >({ + url: "/provider/{providerID}/oauth/authorize", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * OAuth callback + * + * Handle the OAuth callback from a provider after user authorization. + */ + public callback( + parameters: { + providerID: string + directory?: string + workspace?: string + method?: number + code?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "method" }, + { in: "body", key: "code" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ProviderOauthCallbackResponses, + ProviderOauthCallbackErrors, + ThrowOnError + >({ + url: "/provider/{providerID}/oauth/callback", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Provider extends HeyApiClient { + /** + * List providers + * + * Get a list of all available AI providers, including both available and connected ones. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/provider", + ...options, + ...params, + }) + } + + /** + * Get provider auth methods + * + * Retrieve available authentication methods for all AI providers. + */ + public auth( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/provider/auth", + ...options, + ...params, + }) + } + + private _oauth?: Oauth + get oauth(): Oauth { + return (this._oauth ??= new Oauth({ client: this.client })) + } +} + +export class Find extends HeyApiClient { + /** + * Find text + * + * Search for text patterns across files in the project using ripgrep. + */ + public text( + parameters: { + directory?: string + workspace?: string + pattern: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "pattern" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/find", + ...options, + ...params, + }) + } + + /** + * Find files + * + * Search for files or directories by name or pattern in the project directory. + */ + public files( + parameters: { + directory?: string + workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "query" }, + { in: "query", key: "dirs" }, + { in: "query", key: "type" }, + { in: "query", key: "limit" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/find/file", + ...options, + ...params, + }) + } + + /** + * Find symbols + * + * Search for workspace symbols like functions, classes, and variables using LSP. + */ + public symbols( + parameters: { + directory?: string + workspace?: string + query: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "query" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/find/symbol", + ...options, + ...params, + }) + } +} + +export class File extends HeyApiClient { + /** + * List files + * + * List files and directories in a specified path. + */ + public list( + parameters: { + directory?: string + workspace?: string + path: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "path" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/file", + ...options, + ...params, + }) + } + + /** + * Read file + * + * Read the content of a specified file. + */ + public read( + parameters: { + directory?: string + workspace?: string + path: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "path" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/file/content", + ...options, + ...params, + }) + } + + /** + * Get file status + * + * Get the git status of all files in the project. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/file/status", + ...options, + ...params, + }) + } +} + +export class Event extends HeyApiClient { + /** + * Subscribe to events + * + * Get events + */ + public subscribe( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).sse.get({ + url: "/event", + ...options, + ...params, + }) + } +} + +export class Auth2 extends HeyApiClient { + /** + * Remove MCP OAuth + * + * Remove OAuth credentials for an MCP server + */ + public remove( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/mcp/{name}/auth", + ...options, + ...params, + }) + } + + /** + * Start MCP OAuth + * + * Start OAuth authentication flow for a Model Context Protocol (MCP) server. + */ + public start( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth", + ...options, + ...params, + }) + } + + /** + * Complete MCP OAuth + * + * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. + */ + public callback( + parameters: { + name: string + directory?: string + workspace?: string + code?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "code" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth/callback", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Authenticate MCP OAuth + * + * Start OAuth flow and wait for callback (opens browser) + */ + public authenticate( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/mcp/{name}/auth/authenticate", + ...options, + ...params, + }, + ) + } +} + +export class Mcp extends HeyApiClient { + /** + * Get MCP status + * + * Get the status of all Model Context Protocol (MCP) servers. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/mcp", + ...options, + ...params, + }) + } + + /** + * Add MCP server + * + * Dynamically add a new Model Context Protocol (MCP) server to the system. + */ + public add( + parameters?: { + directory?: string + workspace?: string + name?: string + config?: McpLocalConfig | McpRemoteConfig + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "config" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Connect an MCP server + */ + public connect( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/connect", + ...options, + ...params, + }) + } + + /** + * Disconnect an MCP server + */ + public disconnect( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/disconnect", + ...options, + ...params, + }) + } + + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) + } +} + +export class Control extends HeyApiClient { + /** + * Get next TUI request + * + * Retrieve the next TUI (Terminal User Interface) request from the queue for processing. + */ + public next( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/tui/control/next", + ...options, + ...params, + }) + } + + /** + * Submit TUI response + * + * Submit a response to the TUI request queue to complete a pending request. + */ + public response( + parameters?: { + directory?: string + workspace?: string + body?: unknown + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "body", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/control/response", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Tui extends HeyApiClient { + /** + * Append TUI prompt + * + * Append prompt to the TUI + */ + public appendPrompt( + parameters?: { + directory?: string + workspace?: string + text?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "text" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/append-prompt", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Open help dialog + * + * Open the help dialog in the TUI to display user assistance information. + */ + public openHelp( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-help", + ...options, + ...params, + }) + } + + /** + * Open sessions dialog + * + * Open the session dialog + */ + public openSessions( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-sessions", + ...options, + ...params, + }) + } + + /** + * Open themes dialog + * + * Open the theme dialog + */ + public openThemes( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-themes", + ...options, + ...params, + }) + } + + /** + * Open models dialog + * + * Open the model dialog + */ + public openModels( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-models", + ...options, + ...params, + }) + } + + /** + * Submit TUI prompt + * + * Submit the prompt + */ + public submitPrompt( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/submit-prompt", + ...options, + ...params, + }) + } + + /** + * Clear TUI prompt + * + * Clear the prompt + */ + public clearPrompt( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/clear-prompt", + ...options, + ...params, + }) + } + + /** + * Execute TUI command + * + * Execute a TUI command (e.g. agent_cycle) + */ + public executeCommand( + parameters?: { + directory?: string + workspace?: string + command?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "command" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/execute-command", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Show TUI toast + * + * Show a toast notification in the TUI + */ + public showToast( + parameters?: { + directory?: string + workspace?: string + title?: string + message?: string + variant?: "info" | "success" | "warning" | "error" + duration?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "message" }, + { in: "body", key: "variant" }, + { in: "body", key: "duration" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/show-toast", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Publish TUI event + * + * Publish a TUI event + */ + public publish( + parameters?: { + directory?: string + workspace?: string + body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "body", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/publish", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Select session + * + * Navigate the TUI to display the specified session. + */ + public selectSession( + parameters?: { + directory?: string + workspace?: string + sessionID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/select-session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _control?: Control + get control(): Control { + return (this._control ??= new Control({ client: this.client })) + } +} + +export class Instance extends HeyApiClient { + /** + * Dispose instance + * + * Clean up and dispose the current OpenCode instance, releasing all resources. + */ + public dispose( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/instance/dispose", + ...options, + ...params, + }) + } +} + +export class Path extends HeyApiClient { + /** + * Get paths + * + * Retrieve the current working directory and related path information for the OpenCode instance. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/path", + ...options, + ...params, + }) + } +} + +export class Vcs extends HeyApiClient { + /** + * Get VCS info + * + * Retrieve version control system (VCS) information for the current project, such as git branch. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/vcs", + ...options, + ...params, + }) + } +} + +export class Command extends HeyApiClient { + /** + * List commands + * + * Get a list of all available commands in the OpenCode system. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/command", + ...options, + ...params, + }) + } +} + +export class App extends HeyApiClient { + /** + * Write log + * + * Write a log entry to the server logs with specified level and metadata. + */ + public log( + parameters?: { + directory?: string + workspace?: string + service?: string + level?: "debug" | "info" | "error" | "warn" + message?: string + extra?: { + [key: string]: unknown + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "service" }, + { in: "body", key: "level" }, + { in: "body", key: "message" }, + { in: "body", key: "extra" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/log", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List agents + * + * Get a list of all available AI agents in the OpenCode system. + */ + public agents( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/agent", + ...options, + ...params, + }) + } + + /** + * List skills + * + * Get a list of all available skills in the OpenCode system. + */ + public skills( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/skill", + ...options, + ...params, + }) + } +} + +export class Lsp extends HeyApiClient { + /** + * Get LSP status + * + * Get LSP server status + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/lsp", + ...options, + ...params, + }) + } +} + +export class Formatter extends HeyApiClient { + /** + * Get formatter status + * + * Get formatter status + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/formatter", + ...options, + ...params, + }) + } +} + +export class OpencodeClient extends HeyApiClient { + public static readonly __registry = new HeyApiRegistry() + + constructor(args?: { client?: Client; key?: string }) { + super(args) + OpencodeClient.__registry.set(this, args?.key) + } + + private _global?: Global + get global(): Global { + return (this._global ??= new Global({ client: this.client })) + } + + private _auth?: Auth + get auth(): Auth { + return (this._auth ??= new Auth({ client: this.client })) + } + + private _project?: Project + get project(): Project { + return (this._project ??= new Project({ client: this.client })) + } + + private _pty?: Pty + get pty(): Pty { + return (this._pty ??= new Pty({ client: this.client })) + } + + private _config?: Config2 + get config(): Config2 { + return (this._config ??= new Config2({ client: this.client })) + } + + private _tool?: Tool + get tool(): Tool { + return (this._tool ??= new Tool({ client: this.client })) + } + + private _experimental?: Experimental + get experimental(): Experimental { + return (this._experimental ??= new Experimental({ client: this.client })) + } + + private _worktree?: Worktree + get worktree(): Worktree { + return (this._worktree ??= new Worktree({ client: this.client })) + } + + private _session?: Session2 + get session(): Session2 { + return (this._session ??= new Session2({ client: this.client })) + } + + private _part?: Part + get part(): Part { + return (this._part ??= new Part({ client: this.client })) + } + + private _permission?: Permission + get permission(): Permission { + return (this._permission ??= new Permission({ client: this.client })) + } + + private _question?: Question + get question(): Question { + return (this._question ??= new Question({ client: this.client })) + } + + private _provider?: Provider + get provider(): Provider { + return (this._provider ??= new Provider({ client: this.client })) + } + + private _find?: Find + get find(): Find { + return (this._find ??= new Find({ client: this.client })) + } + + private _file?: File + get file(): File { + return (this._file ??= new File({ client: this.client })) + } + + private _event?: Event + get event(): Event { + return (this._event ??= new Event({ client: this.client })) + } + + private _mcp?: Mcp + get mcp(): Mcp { + return (this._mcp ??= new Mcp({ client: this.client })) + } + + private _tui?: Tui + get tui(): Tui { + return (this._tui ??= new Tui({ client: this.client })) + } + + private _instance?: Instance + get instance(): Instance { + return (this._instance ??= new Instance({ client: this.client })) + } + + private _path?: Path + get path(): Path { + return (this._path ??= new Path({ client: this.client })) + } + + private _vcs?: Vcs + get vcs(): Vcs { + return (this._vcs ??= new Vcs({ client: this.client })) + } + + private _command?: Command + get command(): Command { + return (this._command ??= new Command({ client: this.client })) + } + + private _app?: App + get app(): App { + return (this._app ??= new App({ client: this.client })) + } + + private _lsp?: Lsp + get lsp(): Lsp { + return (this._lsp ??= new Lsp({ client: this.client })) + } + + private _formatter?: Formatter + get formatter(): Formatter { + return (this._formatter ??= new Formatter({ client: this.client })) + } +} diff --git a/server/opencode/gen/types.gen.ts b/server/opencode/gen/types.gen.ts new file mode 100644 index 00000000..ec797f2b --- /dev/null +++ b/server/opencode/gen/types.gen.ts @@ -0,0 +1,5000 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}) +} + +export type EventInstallationUpdated = { + type: "installation.updated" + properties: { + version: string + } +} + +export type EventInstallationUpdateAvailable = { + type: "installation.update-available" + properties: { + version: string + } +} + +export type Project = { + id: string + worktree: string + vcs?: "git" + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + time: { + created: number + updated: number + initialized?: number + } + sandboxes: Array +} + +export type EventProjectUpdated = { + type: "project.updated" + properties: Project +} + +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string + } +} + +export type EventServerInstanceDisposed = { + type: "server.instance.disposed" + properties: { + directory: string + } +} + +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + +export type PermissionRequest = { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventPermissionAsked = { + type: "permission.asked" + properties: PermissionRequest +} + +export type EventPermissionReplied = { + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" + } +} + +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" + properties: { + branch?: string + } +} + +export type QuestionOption = { + /** + * Display text (1-5 words, concise) + */ + label: string + /** + * Explanation of choice + */ + description: string +} + +export type QuestionInfo = { + /** + * Complete question + */ + question: string + /** + * Very short label (max 30 chars) + */ + header: string + /** + * Available choices + */ + options: Array + /** + * Allow selecting multiple choices + */ + multiple?: boolean + /** + * Allow typing a custom answer (default: true) + */ + custom?: boolean +} + +export type QuestionRequest = { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventQuestionAsked = { + type: "question.asked" + properties: QuestionRequest +} + +export type QuestionAnswer = Array + +export type EventQuestionReplied = { + type: "question.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } +} + +export type EventQuestionRejected = { + type: "question.rejected" + properties: { + sessionID: string + requestID: string + } +} + +export type EventServerConnected = { + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + +export type EventLspClientDiagnostics = { + type: "lsp.client.diagnostics" + properties: { + serverID: string + path: string + } +} + +export type EventLspUpdated = { + type: "lsp.updated" + properties: { + [key: string]: unknown + } +} + +export type OutputFormatText = { + type: "text" +} + +export type JsonSchema = { + [key: string]: unknown +} + +export type OutputFormatJsonSchema = { + type: "json_schema" + schema: JsonSchema + retryCount?: number +} + +export type OutputFormat = OutputFormatText | OutputFormatJsonSchema + +export type FileDiff = { + file: string + before: string + after: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { + created: number + } + format?: OutputFormat + summary?: { + title?: string + body?: string + diffs: Array + } + agent: string + model: { + providerID: string + modelID: string + } + system?: string + tools?: { + [key: string]: boolean + } + variant?: string +} + +export type ProviderAuthError = { + name: "ProviderAuthError" + data: { + providerID: string + message: string + } +} + +export type UnknownError = { + name: "UnknownError" + data: { + message: string + } +} + +export type MessageOutputLengthError = { + name: "MessageOutputLengthError" + data: { + [key: string]: unknown + } +} + +export type MessageAbortedError = { + name: "MessageAbortedError" + data: { + message: string + } +} + +export type StructuredOutputError = { + name: "StructuredOutputError" + data: { + message: string + retries: number + } +} + +export type ContextOverflowError = { + name: "ContextOverflowError" + data: { + message: string + responseBody?: string + } +} + +export type ApiError = { + name: "APIError" + data: { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string + } + responseBody?: string + metadata?: { + [key: string]: string + } + } +} + +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { + created: number + completed?: number + } + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ApiError + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string + } + summary?: boolean + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + structured?: unknown + variant?: string + finish?: string +} + +export type Message = UserMessage | AssistantMessage + +export type EventMessageUpdated = { + type: "message.updated" + properties: { + info: Message + } +} + +export type EventMessageRemoved = { + type: "message.removed" + properties: { + sessionID: string + messageID: string + } +} + +export type TextPart = { + id: string + sessionID: string + messageID: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } +} + +export type SubtaskPart = { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { + providerID: string + modelID: string + } + command?: string +} + +export type ReasoningPart = { + id: string + sessionID: string + messageID: string + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } +} + +export type FilePartSourceText = { + value: string + start: number + end: number +} + +export type FileSource = { + text: FilePartSourceText + type: "file" + path: string +} + +export type Range = { + start: { + line: number + character: number + } + end: { + line: number + character: number + } +} + +export type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: Range + name: string + kind: number +} + +export type ResourceSource = { + text: FilePartSourceText + type: "resource" + clientName: string + uri: string +} + +export type FilePartSource = FileSource | SymbolSource | ResourceSource + +export type FilePart = { + id: string + sessionID: string + messageID: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type ToolStatePending = { + status: "pending" + input: { + [key: string]: unknown + } + raw: string +} + +export type ToolStateRunning = { + status: "running" + input: { + [key: string]: unknown + } + title?: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + } +} + +export type ToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown + } + output: string + title: string + metadata: { + [key: string]: unknown + } + time: { + start: number + end: number + compacted?: number + } + attachments?: Array +} + +export type ToolStateError = { + status: "error" + input: { + [key: string]: unknown + } + error: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end: number + } +} + +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + +export type ToolPart = { + id: string + sessionID: string + messageID: string + type: "tool" + callID: string + tool: string + state: ToolState + metadata?: { + [key: string]: unknown + } +} + +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" + snapshot?: string +} + +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + reason: string + snapshot?: string + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } +} + +export type SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string +} + +export type PatchPart = { + id: string + sessionID: string + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type RetryPart = { + id: string + sessionID: string + messageID: string + type: "retry" + attempt: number + error: ApiError + time: { + created: number + } +} + +export type CompactionPart = { + id: string + sessionID: string + messageID: string + type: "compaction" + auto: boolean + overflow?: boolean +} + +export type Part = + | TextPart + | SubtaskPart + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | RetryPart + | CompactionPart + +export type EventMessagePartUpdated = { + type: "message.part.updated" + properties: { + part: Part + } +} + +export type EventMessagePartDelta = { + type: "message.part.delta" + properties: { + sessionID: string + messageID: string + partID: string + field: string + delta: string + } +} + +export type EventMessagePartRemoved = { + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string + } +} + +export type SessionStatus = + | { + type: "idle" + } + | { + type: "retry" + attempt: number + message: string + next: number + } + | { + type: "busy" + } + +export type EventSessionStatus = { + type: "session.status" + properties: { + sessionID: string + status: SessionStatus + } +} + +export type EventSessionIdle = { + type: "session.idle" + properties: { + sessionID: string + } +} + +export type EventSessionCompacted = { + type: "session.compacted" + properties: { + sessionID: string + } +} + +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string +} + +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number + } +} + +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type EventMcpToolsChanged = { + type: "mcp.tools.changed" + properties: { + server: string + } +} + +export type EventMcpBrowserOpenFailed = { + type: "mcp.browser.open.failed" + properties: { + mcpName: string + url: string + } +} + +export type EventCommandExecuted = { + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } +} + +export type PermissionAction = "allow" | "deny" | "ask" + +export type PermissionRule = { + permission: string + pattern: string + action: PermissionAction +} + +export type PermissionRuleset = Array + +export type Session = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + share?: { + url: string + } + title: string + version: string + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } +} + +export type EventSessionCreated = { + type: "session.created" + properties: { + info: Session + } +} + +export type EventSessionUpdated = { + type: "session.updated" + properties: { + info: Session + } +} + +export type EventSessionDeleted = { + type: "session.deleted" + properties: { + info: Session + } +} + +export type EventSessionDiff = { + type: "session.diff" + properties: { + sessionID: string + diff: Array + } +} + +export type EventSessionError = { + type: "session.error" + properties: { + sessionID?: string + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ApiError + } +} + +export type EventWorkspaceReady = { + type: "workspace.ready" + properties: { + name: string + } +} + +export type EventWorkspaceFailed = { + type: "workspace.failed" + properties: { + message: string + } +} + +export type Pty = { + id: string + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number +} + +export type EventPtyCreated = { + type: "pty.created" + properties: { + info: Pty + } +} + +export type EventPtyUpdated = { + type: "pty.updated" + properties: { + info: Pty + } +} + +export type EventPtyExited = { + type: "pty.exited" + properties: { + id: string + exitCode: number + } +} + +export type EventPtyDeleted = { + type: "pty.deleted" + properties: { + id: string + } +} + +export type EventWorktreeReady = { + type: "worktree.ready" + properties: { + name: string + branch: string + } +} + +export type EventWorktreeFailed = { + type: "worktree.failed" + properties: { + message: string + } +} + +export type Event = + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventProjectUpdated + | EventFileEdited + | EventServerInstanceDisposed + | EventFileWatcherUpdated + | EventPermissionAsked + | EventPermissionReplied + | EventVcsBranchUpdated + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventServerConnected + | EventGlobalDisposed + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartDelta + | EventMessagePartRemoved + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventTodoUpdated + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionDiff + | EventSessionError + | EventWorkspaceReady + | EventWorkspaceFailed + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventWorktreeReady + | EventWorktreeFailed + +export type GlobalEvent = { + directory: string + payload: Event +} + +/** + * Log level + */ +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" + +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { + /** + * Port to listen on + */ + port?: number + /** + * Hostname to listen on + */ + hostname?: string + /** + * Enable mDNS service discovery + */ + mdns?: boolean + /** + * Custom domain name for mDNS service (default: opencode.local) + */ + mdnsDomain?: string + /** + * Additional domains to allow for CORS + */ + cors?: Array +} + +export type PermissionActionConfig = "ask" | "allow" | "deny" + +export type PermissionObjectConfig = { + [key: string]: PermissionActionConfig +} + +export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig + +export type PermissionConfig = + | { + __originalKeys?: Array + read?: PermissionRuleConfig + edit?: PermissionRuleConfig + glob?: PermissionRuleConfig + grep?: PermissionRuleConfig + list?: PermissionRuleConfig + bash?: PermissionRuleConfig + task?: PermissionRuleConfig + external_directory?: PermissionRuleConfig + todowrite?: PermissionActionConfig + todoread?: PermissionActionConfig + question?: PermissionActionConfig + webfetch?: PermissionActionConfig + websearch?: PermissionActionConfig + codesearch?: PermissionActionConfig + lsp?: PermissionRuleConfig + doom_loop?: PermissionActionConfig + skill?: PermissionRuleConfig + [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined + } + | PermissionActionConfig + +export type AgentConfig = { + model?: string + /** + * Default model variant for this agent (applies only when using the agent's configured model). + */ + variant?: string + temperature?: number + top_p?: number + prompt?: string + /** + * @deprecated Use 'permission' field instead + */ + tools?: { + [key: string]: boolean + } + disable?: boolean + /** + * Description of when to use the agent + */ + description?: string + mode?: "subagent" | "primary" | "all" + /** + * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) + */ + hidden?: boolean + options?: { + [key: string]: unknown + } + /** + * Hex color code (e.g., #FF5733) or theme color (e.g., primary) + */ + color?: string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" + /** + * Maximum number of agentic iterations before forcing text-only response + */ + steps?: number + /** + * @deprecated Use 'steps' field instead. + */ + maxSteps?: number + permission?: PermissionConfig + [key: string]: + | unknown + | string + | number + | { + [key: string]: boolean + } + | boolean + | "subagent" + | "primary" + | "all" + | { + [key: string]: unknown + } + | string + | "primary" + | "secondary" + | "accent" + | "success" + | "warning" + | "error" + | "info" + | number + | PermissionConfig + | undefined +} + +export type ProviderConfig = { + api?: string + name?: string + env?: Array + id?: string + npm?: string + models?: { + [key: string]: { + id?: string + name?: string + family?: string + release_date?: string + attachment?: boolean + reasoning?: boolean + temperature?: boolean + tool_call?: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit?: { + context: number + input?: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + options?: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + provider?: { + npm?: string + api?: string + } + /** + * Variant-specific configuration + */ + variants?: { + [key: string]: { + /** + * Disable this variant for the model + */ + disabled?: boolean + [key: string]: unknown | boolean | undefined + } + } + } + } + whitelist?: Array + blacklist?: Array + options?: { + apiKey?: string + baseURL?: string + /** + * GitHub Enterprise URL for copilot authentication + */ + enterpriseUrl?: string + /** + * Enable promptCacheKey for this provider (default false) + */ + setCacheKey?: boolean + /** + * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. + */ + timeout?: number | false + /** + * Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted. + */ + chunkTimeout?: number + [key: string]: unknown | string | boolean | number | false | number | undefined + } +} + +export type McpLocalConfig = { + /** + * Type of MCP server connection + */ + type: "local" + /** + * Command and arguments to run the MCP server + */ + command: Array + /** + * Environment variables to set when running the MCP server + */ + environment?: { + [key: string]: string + } + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number +} + +export type McpOAuthConfig = { + /** + * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. + */ + clientId?: string + /** + * OAuth client secret (if required by the authorization server) + */ + clientSecret?: string + /** + * OAuth scopes to request during authorization + */ + scope?: string +} + +export type McpRemoteConfig = { + /** + * Type of MCP server connection + */ + type: "remote" + /** + * URL of the remote MCP server + */ + url: string + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Headers to send with the request + */ + headers?: { + [key: string]: string + } + /** + * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. + */ + oauth?: McpOAuthConfig | false + /** + * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number +} + +/** + * @deprecated Always uses stretch layout. + */ +export type LayoutConfig = "auto" | "stretch" + +export type Config = { + /** + * JSON schema reference for configuration validation + */ + $schema?: string + logLevel?: LogLevel + server?: ServerConfig + /** + * Command configuration, see https://opencode.ai/docs/commands + */ + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + subtask?: boolean + } + } + /** + * Additional skill folder paths + */ + skills?: { + /** + * Additional paths to skill folders + */ + paths?: Array + /** + * URLs to fetch skills from (e.g., https://example.com/.well-known/skills/) + */ + urls?: Array + } + watcher?: { + ignore?: Array + } + plugin?: Array + /** + * Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true. + */ + snapshot?: boolean + /** + * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing + */ + share?: "manual" | "auto" | "disabled" + /** + * @deprecated Use 'share' field instead. Share newly created sessions automatically + */ + autoshare?: boolean + /** + * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications + */ + autoupdate?: boolean | "notify" + /** + * Disable providers that are loaded automatically + */ + disabled_providers?: Array + /** + * When set, ONLY these providers will be enabled. All other providers will be ignored + */ + enabled_providers?: Array + /** + * Model to use in the format of provider/model, eg anthropic/claude-2 + */ + model?: string + /** + * Small model to use for tasks like title generation in the format of provider/model + */ + small_model?: string + /** + * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. + */ + default_agent?: string + /** + * Custom username to display in conversations instead of system username + */ + username?: string + /** + * @deprecated Use `agent` field instead. + */ + mode?: { + build?: AgentConfig + plan?: AgentConfig + [key: string]: AgentConfig | undefined + } + /** + * Agent configuration, see https://opencode.ai/docs/agents + */ + agent?: { + plan?: AgentConfig + build?: AgentConfig + general?: AgentConfig + explore?: AgentConfig + title?: AgentConfig + summary?: AgentConfig + compaction?: AgentConfig + [key: string]: AgentConfig | undefined + } + /** + * Custom provider configurations and model overrides + */ + provider?: { + [key: string]: ProviderConfig + } + /** + * MCP (Model Context Protocol) server configurations + */ + mcp?: { + [key: string]: + | McpLocalConfig + | McpRemoteConfig + | { + enabled: boolean + } + } + formatter?: + | false + | { + [key: string]: { + disabled?: boolean + command?: Array + environment?: { + [key: string]: string + } + extensions?: Array + } + } + lsp?: + | false + | { + [key: string]: + | { + disabled: true + } + | { + command: Array + extensions?: Array + disabled?: boolean + env?: { + [key: string]: string + } + initialization?: { + [key: string]: unknown + } + } + } + /** + * Additional instruction files or patterns to include + */ + instructions?: Array + layout?: LayoutConfig + permission?: PermissionConfig + tools?: { + [key: string]: boolean + } + enterprise?: { + /** + * Enterprise URL + */ + url?: string + } + compaction?: { + /** + * Enable automatic compaction when context is full (default: true) + */ + auto?: boolean + /** + * Enable pruning of old tool outputs (default: true) + */ + prune?: boolean + /** + * Token buffer for compaction. Leaves enough window to avoid overflow during compaction. + */ + reserved?: number + } + experimental?: { + disable_paste_summary?: boolean + /** + * Enable the batch tool + */ + batch_tool?: boolean + /** + * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) + */ + openTelemetry?: boolean + /** + * Tools that should only be available to primary agents. + */ + primary_tools?: Array + /** + * Continue the agent loop when a tool call is denied + */ + continue_loop_on_deny?: boolean + /** + * Timeout in milliseconds for model context protocol (MCP) requests + */ + mcp_timeout?: number + } +} + +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + +export type OAuth = { + type: "oauth" + refresh: string + access: string + expires: number + accountId?: string + enterpriseUrl?: string +} + +export type ApiAuth = { + type: "api" + key: string +} + +export type WellKnownAuth = { + type: "wellknown" + key: string + token: string +} + +export type Auth = OAuth | ApiAuth | WellKnownAuth + +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string + } +} + +export type Model = { + id: string + providerID: string + api: { + id: string + url: string + npm: string + } + name: string + family?: string + capabilities: { + temperature: boolean + reasoning: boolean + attachment: boolean + toolcall: boolean + input: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + output: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + interleaved: + | boolean + | { + field: "reasoning_content" | "reasoning_details" + } + } + cost: { + input: number + output: number + cache: { + read: number + write: number + } + experimentalOver200K?: { + input: number + output: number + cache: { + read: number + write: number + } + } + } + limit: { + context: number + input?: number + output: number + } + status: "alpha" | "beta" | "deprecated" | "active" + options: { + [key: string]: unknown + } + headers: { + [key: string]: string + } + release_date: string + variants?: { + [key: string]: { + [key: string]: unknown + } + } +} + +export type Provider = { + id: string + name: string + source: "env" | "config" | "custom" | "api" + env: Array + key?: string + options: { + [key: string]: unknown + } + models: { + [key: string]: Model + } +} + +export type ToolIds = Array + +export type ToolListItem = { + id: string + description: string + parameters: unknown +} + +export type ToolList = Array + +export type Workspace = { + id: string + type: string + branch: string | null + name: string | null + directory: string | null + extra: unknown | null + projectID: string +} + +export type Worktree = { + name: string + branch: string + directory: string +} + +export type WorktreeCreateInput = { + name?: string + /** + * Additional startup script to run after the project's start command + */ + startCommand?: string +} + +export type WorktreeRemoveInput = { + directory: string +} + +export type WorktreeResetInput = { + directory: string +} + +export type ProjectSummary = { + id: string + name?: string + worktree: string +} + +export type GlobalSession = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + share?: { + url: string + } + title: string + version: string + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } + project: ProjectSummary | null +} + +export type McpResource = { + name: string + uri: string + description?: string + mimeType?: string + client: string +} + +export type TextPartInput = { + id?: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } +} + +export type FilePartInput = { + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type AgentPartInput = { + id?: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type SubtaskPartInput = { + id?: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { + providerID: string + modelID: string + } + command?: string +} + +export type ProviderAuthMethod = { + type: "oauth" | "api" + label: string + prompts?: Array< + | { + type: "text" + key: string + message: string + placeholder?: string + when?: { + key: string + op: "eq" | "neq" + value: string + } + } + | { + type: "select" + key: string + message: string + options: Array<{ + label: string + value: string + hint?: string + }> + when?: { + key: string + op: "eq" | "neq" + value: string + } + } + > +} + +export type ProviderAuthAuthorization = { + url: string + method: "auto" | "code" + instructions: string +} + +export type Symbol = { + name: string + kind: number + location: { + uri: string + range: Range + } +} + +export type FileNode = { + name: string + path: string + absolute: string + type: "file" | "directory" + ignored: boolean +} + +export type FileContent = { + type: "text" | "binary" + content: string + diff?: string + patch?: { + oldFileName: string + newFileName: string + oldHeader?: string + newHeader?: string + hunks: Array<{ + oldStart: number + oldLines: number + newStart: number + newLines: number + lines: Array + }> + index?: string + } + encoding?: "base64" + mimeType?: string +} + +export type File = { + path: string + added: number + removed: number + status: "added" | "deleted" | "modified" +} + +export type McpStatusConnected = { + status: "connected" +} + +export type McpStatusDisabled = { + status: "disabled" +} + +export type McpStatusFailed = { + status: "failed" + error: string +} + +export type McpStatusNeedsAuth = { + status: "needs_auth" +} + +export type McpStatusNeedsClientRegistration = { + status: "needs_client_registration" + error: string +} + +export type McpStatus = + | McpStatusConnected + | McpStatusDisabled + | McpStatusFailed + | McpStatusNeedsAuth + | McpStatusNeedsClientRegistration + +export type Path = { + home: string + state: string + config: string + worktree: string + directory: string +} + +export type VcsInfo = { + branch: string +} + +export type Command = { + name: string + description?: string + agent?: string + model?: string + source?: "command" | "mcp" | "skill" + template: string + subtask?: boolean + hints: Array +} + +export type Agent = { + name: string + description?: string + mode: "subagent" | "primary" | "all" + native?: boolean + hidden?: boolean + topP?: number + temperature?: number + color?: string + permission: PermissionRuleset + model?: { + modelID: string + providerID: string + } + variant?: string + prompt?: string + options: { + [key: string]: unknown + } + steps?: number +} + +export type LspStatus = { + id: string + name: string + root: string + status: "connected" | "error" +} + +export type FormatterStatus = { + name: string + extensions: Array + enabled: boolean +} + +export type GlobalHealthData = { + body?: never + path?: never + query?: never + url: "/global/health" +} + +export type GlobalHealthResponses = { + /** + * Health information + */ + 200: { + healthy: true + version: string + } +} + +export type GlobalHealthResponse = GlobalHealthResponses[keyof GlobalHealthResponses] + +export type GlobalEventData = { + body?: never + path?: never + query?: never + url: "/global/event" +} + +export type GlobalEventResponses = { + /** + * Event stream + */ + 200: GlobalEvent +} + +export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] + +export type GlobalConfigGetData = { + body?: never + path?: never + query?: never + url: "/global/config" +} + +export type GlobalConfigGetResponses = { + /** + * Get global config info + */ + 200: Config +} + +export type GlobalConfigGetResponse = GlobalConfigGetResponses[keyof GlobalConfigGetResponses] + +export type GlobalConfigUpdateData = { + body?: Config + path?: never + query?: never + url: "/global/config" +} + +export type GlobalConfigUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalConfigUpdateError = GlobalConfigUpdateErrors[keyof GlobalConfigUpdateErrors] + +export type GlobalConfigUpdateResponses = { + /** + * Successfully updated global config + */ + 200: Config +} + +export type GlobalConfigUpdateResponse = GlobalConfigUpdateResponses[keyof GlobalConfigUpdateResponses] + +export type GlobalDisposeData = { + body?: never + path?: never + query?: never + url: "/global/dispose" +} + +export type GlobalDisposeResponses = { + /** + * Global disposed + */ + 200: boolean +} + +export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses] + +export type AuthRemoveData = { + body?: never + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] + +export type AuthRemoveResponses = { + /** + * Successfully removed authentication credentials + */ + 200: boolean +} + +export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] + +export type AuthSetData = { + body?: Auth + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthSetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] + +export type AuthSetResponses = { + /** + * Successfully set authentication credentials + */ + 200: boolean +} + +export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] + +export type ProjectListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project" +} + +export type ProjectListResponses = { + /** + * List of projects + */ + 200: Array +} + +export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] + +export type ProjectCurrentData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/current" +} + +export type ProjectCurrentResponses = { + /** + * Current project information + */ + 200: Project +} + +export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] + +export type ProjectInitGitData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/git/init" +} + +export type ProjectInitGitResponses = { + /** + * Project information after git initialization + */ + 200: Project +} + +export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] + +export type ProjectUpdateData = { + body?: { + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + } + path: { + projectID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/project/{projectID}" +} + +export type ProjectUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] + +export type ProjectUpdateResponses = { + /** + * Updated project information + */ + 200: Project +} + +export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] + +export type PtyListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/pty" +} + +export type PtyListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type PtyListResponse = PtyListResponses[keyof PtyListResponses] + +export type PtyCreateData = { + body?: { + command?: string + args?: Array + cwd?: string + title?: string + env?: { + [key: string]: string + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/pty" +} + +export type PtyCreateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PtyCreateError = PtyCreateErrors[keyof PtyCreateErrors] + +export type PtyCreateResponses = { + /** + * Created session + */ + 200: Pty +} + +export type PtyCreateResponse = PtyCreateResponses[keyof PtyCreateResponses] + +export type PtyRemoveData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}" +} + +export type PtyRemoveErrors = { + /** + * Not found + */ + 404: NotFoundError +} + +export type PtyRemoveError = PtyRemoveErrors[keyof PtyRemoveErrors] + +export type PtyRemoveResponses = { + /** + * Session removed + */ + 200: boolean +} + +export type PtyRemoveResponse = PtyRemoveResponses[keyof PtyRemoveResponses] + +export type PtyGetData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}" +} + +export type PtyGetErrors = { + /** + * Not found + */ + 404: NotFoundError +} + +export type PtyGetError = PtyGetErrors[keyof PtyGetErrors] + +export type PtyGetResponses = { + /** + * Session info + */ + 200: Pty +} + +export type PtyGetResponse = PtyGetResponses[keyof PtyGetResponses] + +export type PtyUpdateData = { + body?: { + title?: string + size?: { + rows: number + cols: number + } + } + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}" +} + +export type PtyUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PtyUpdateError = PtyUpdateErrors[keyof PtyUpdateErrors] + +export type PtyUpdateResponses = { + /** + * Updated session + */ + 200: Pty +} + +export type PtyUpdateResponse = PtyUpdateResponses[keyof PtyUpdateResponses] + +export type PtyConnectData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}/connect" +} + +export type PtyConnectErrors = { + /** + * Not found + */ + 404: NotFoundError +} + +export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors] + +export type PtyConnectResponses = { + /** + * Connected session + */ + 200: boolean +} + +export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] + +export type ConfigGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigGetResponses = { + /** + * Get config info + */ + 200: Config +} + +export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] + +export type ConfigUpdateData = { + body?: Config + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] + +export type ConfigUpdateResponses = { + /** + * Successfully updated config + */ + 200: Config +} + +export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] + +export type ConfigProvidersData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config/providers" +} + +export type ConfigProvidersResponses = { + /** + * List of providers + */ + 200: { + providers: Array + default: { + [key: string]: string + } + } +} + +export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] + +export type ToolIdsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/tool/ids" +} + +export type ToolIdsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors] + +export type ToolIdsResponses = { + /** + * Tool IDs + */ + 200: ToolIds +} + +export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses] + +export type ToolListData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + provider: string + model: string + } + url: "/experimental/tool" +} + +export type ToolListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ToolListError = ToolListErrors[keyof ToolListErrors] + +export type ToolListResponses = { + /** + * Tools + */ + 200: ToolList +} + +export type ToolListResponse = ToolListResponses[keyof ToolListResponses] + +export type ExperimentalWorkspaceListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace" +} + +export type ExperimentalWorkspaceListResponses = { + /** + * Workspaces + */ + 200: Array +} + +export type ExperimentalWorkspaceListResponse = + ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] + +export type ExperimentalWorkspaceCreateData = { + body?: { + id?: string + type: string + branch: string | null + extra: unknown | null + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace" +} + +export type ExperimentalWorkspaceCreateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceCreateError = + ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] + +export type ExperimentalWorkspaceCreateResponses = { + /** + * Workspace created + */ + 200: Workspace +} + +export type ExperimentalWorkspaceCreateResponse = + ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] + +export type ExperimentalWorkspaceRemoveData = { + body?: never + path: { + id: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/{id}" +} + +export type ExperimentalWorkspaceRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceRemoveError = + ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] + +export type ExperimentalWorkspaceRemoveResponses = { + /** + * Workspace removed + */ + 200: Workspace +} + +export type ExperimentalWorkspaceRemoveResponse = + ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] + +export type WorktreeRemoveData = { + body?: WorktreeRemoveInput + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree" +} + +export type WorktreeRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors] + +export type WorktreeRemoveResponses = { + /** + * Worktree removed + */ + 200: boolean +} + +export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses] + +export type WorktreeListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree" +} + +export type WorktreeListResponses = { + /** + * List of worktree directories + */ + 200: Array +} + +export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses] + +export type WorktreeCreateData = { + body?: WorktreeCreateInput + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree" +} + +export type WorktreeCreateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors] + +export type WorktreeCreateResponses = { + /** + * Worktree created + */ + 200: Worktree +} + +export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses] + +export type WorktreeResetData = { + body?: WorktreeResetInput + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree/reset" +} + +export type WorktreeResetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type WorktreeResetError = WorktreeResetErrors[keyof WorktreeResetErrors] + +export type WorktreeResetResponses = { + /** + * Worktree reset + */ + 200: boolean +} + +export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses] + +export type ExperimentalSessionListData = { + body?: never + path?: never + query?: { + /** + * Filter sessions by project directory + */ + directory?: string + workspace?: string + /** + * Only return root sessions (no parentID) + */ + roots?: boolean + /** + * Filter sessions updated on or after this timestamp (milliseconds since epoch) + */ + start?: number + /** + * Return sessions updated before this timestamp (milliseconds since epoch) + */ + cursor?: number + /** + * Filter sessions by title (case-insensitive) + */ + search?: string + /** + * Maximum number of sessions to return + */ + limit?: number + /** + * Include archived sessions (default false) + */ + archived?: boolean + } + url: "/experimental/session" +} + +export type ExperimentalSessionListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type ExperimentalSessionListResponse = ExperimentalSessionListResponses[keyof ExperimentalSessionListResponses] + +export type ExperimentalResourceListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/resource" +} + +export type ExperimentalResourceListResponses = { + /** + * MCP resources + */ + 200: { + [key: string]: McpResource + } +} + +export type ExperimentalResourceListResponse = + ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses] + +export type SessionListData = { + body?: never + path?: never + query?: { + /** + * Filter sessions by project directory + */ + directory?: string + workspace?: string + /** + * Only return root sessions (no parentID) + */ + roots?: boolean + /** + * Filter sessions updated on or after this timestamp (milliseconds since epoch) + */ + start?: number + /** + * Filter sessions by title (case-insensitive) + */ + search?: string + /** + * Maximum number of sessions to return + */ + limit?: number + } + url: "/session" +} + +export type SessionListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type SessionListResponse = SessionListResponses[keyof SessionListResponses] + +export type SessionCreateData = { + body?: { + parentID?: string + title?: string + permission?: PermissionRuleset + workspaceID?: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/session" +} + +export type SessionCreateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors] + +export type SessionCreateResponses = { + /** + * Successfully created session + */ + 200: Session +} + +export type SessionCreateResponse = SessionCreateResponses[keyof SessionCreateResponses] + +export type SessionStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/session/status" +} + +export type SessionStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SessionStatusError = SessionStatusErrors[keyof SessionStatusErrors] + +export type SessionStatusResponses = { + /** + * Get session status + */ + 200: { + [key: string]: SessionStatus + } +} + +export type SessionStatusResponse = SessionStatusResponses[keyof SessionStatusResponses] + +export type SessionDeleteData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}" +} + +export type SessionDeleteErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionDeleteError = SessionDeleteErrors[keyof SessionDeleteErrors] + +export type SessionDeleteResponses = { + /** + * Successfully deleted session + */ + 200: boolean +} + +export type SessionDeleteResponse = SessionDeleteResponses[keyof SessionDeleteResponses] + +export type SessionGetData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}" +} + +export type SessionGetErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionGetError = SessionGetErrors[keyof SessionGetErrors] + +export type SessionGetResponses = { + /** + * Get session + */ + 200: Session +} + +export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] + +export type SessionUpdateData = { + body?: { + title?: string + time?: { + archived?: number + } + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}" +} + +export type SessionUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionUpdateError = SessionUpdateErrors[keyof SessionUpdateErrors] + +export type SessionUpdateResponses = { + /** + * Successfully updated session + */ + 200: Session +} + +export type SessionUpdateResponse = SessionUpdateResponses[keyof SessionUpdateResponses] + +export type SessionChildrenData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/children" +} + +export type SessionChildrenErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionChildrenError = SessionChildrenErrors[keyof SessionChildrenErrors] + +export type SessionChildrenResponses = { + /** + * List of children + */ + 200: Array +} + +export type SessionChildrenResponse = SessionChildrenResponses[keyof SessionChildrenResponses] + +export type SessionTodoData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/todo" +} + +export type SessionTodoErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionTodoError = SessionTodoErrors[keyof SessionTodoErrors] + +export type SessionTodoResponses = { + /** + * Todo list + */ + 200: Array +} + +export type SessionTodoResponse = SessionTodoResponses[keyof SessionTodoResponses] + +export type SessionInitData = { + body?: { + modelID: string + providerID: string + messageID: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/init" +} + +export type SessionInitErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionInitError = SessionInitErrors[keyof SessionInitErrors] + +export type SessionInitResponses = { + /** + * 200 + */ + 200: boolean +} + +export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses] + +export type SessionForkData = { + body?: { + messageID?: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/fork" +} + +export type SessionForkResponses = { + /** + * 200 + */ + 200: Session +} + +export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses] + +export type SessionAbortData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/abort" +} + +export type SessionAbortErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors] + +export type SessionAbortResponses = { + /** + * Aborted session + */ + 200: boolean +} + +export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses] + +export type SessionUnshareData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/share" +} + +export type SessionUnshareErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors] + +export type SessionUnshareResponses = { + /** + * Successfully unshared session + */ + 200: Session +} + +export type SessionUnshareResponse = SessionUnshareResponses[keyof SessionUnshareResponses] + +export type SessionShareData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/share" +} + +export type SessionShareErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionShareError = SessionShareErrors[keyof SessionShareErrors] + +export type SessionShareResponses = { + /** + * Successfully shared session + */ + 200: Session +} + +export type SessionShareResponse = SessionShareResponses[keyof SessionShareResponses] + +export type SessionDiffData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + messageID?: string + } + url: "/session/{sessionID}/diff" +} + +export type SessionDiffResponses = { + /** + * Successfully retrieved diff + */ + 200: Array +} + +export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] + +export type SessionSummarizeData = { + body?: { + providerID: string + modelID: string + auto?: boolean + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/summarize" +} + +export type SessionSummarizeErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors] + +export type SessionSummarizeResponses = { + /** + * Summarized session + */ + 200: boolean +} + +export type SessionSummarizeResponse = SessionSummarizeResponses[keyof SessionSummarizeResponses] + +export type SessionMessagesData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + /** + * Maximum number of messages to return + */ + limit?: number + before?: string + } + url: "/session/{sessionID}/message" +} + +export type SessionMessagesErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors] + +export type SessionMessagesResponses = { + /** + * List of messages + */ + 200: Array<{ + info: Message + parts: Array + }> +} + +export type SessionMessagesResponse = SessionMessagesResponses[keyof SessionMessagesResponses] + +export type SessionPromptData = { + body?: { + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + /** + * @deprecated tools and permissions have been merged, you can set permissions on the session itself now + */ + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts: Array + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message" +} + +export type SessionPromptErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors] + +export type SessionPromptResponses = { + /** + * Created message + */ + 200: { + info: AssistantMessage + parts: Array + } +} + +export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses] + +export type SessionDeleteMessageData = { + body?: never + path: { + sessionID: string + messageID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}" +} + +export type SessionDeleteMessageErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors] + +export type SessionDeleteMessageResponses = { + /** + * Successfully deleted message + */ + 200: boolean +} + +export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses] + +export type SessionMessageData = { + body?: never + path: { + sessionID: string + messageID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}" +} + +export type SessionMessageErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors] + +export type SessionMessageResponses = { + /** + * Message + */ + 200: { + info: Message + parts: Array + } +} + +export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses] + +export type PartDeleteData = { + body?: never + path: { + sessionID: string + messageID: string + partID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}/part/{partID}" +} + +export type PartDeleteErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type PartDeleteError = PartDeleteErrors[keyof PartDeleteErrors] + +export type PartDeleteResponses = { + /** + * Successfully deleted part + */ + 200: boolean +} + +export type PartDeleteResponse = PartDeleteResponses[keyof PartDeleteResponses] + +export type PartUpdateData = { + body?: Part + path: { + sessionID: string + messageID: string + partID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}/part/{partID}" +} + +export type PartUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type PartUpdateError = PartUpdateErrors[keyof PartUpdateErrors] + +export type PartUpdateResponses = { + /** + * Successfully updated part + */ + 200: Part +} + +export type PartUpdateResponse = PartUpdateResponses[keyof PartUpdateResponses] + +export type SessionPromptAsyncData = { + body?: { + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + /** + * @deprecated tools and permissions have been merged, you can set permissions on the session itself now + */ + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts: Array + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/prompt_async" +} + +export type SessionPromptAsyncErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionPromptAsyncError = SessionPromptAsyncErrors[keyof SessionPromptAsyncErrors] + +export type SessionPromptAsyncResponses = { + /** + * Prompt accepted + */ + 204: void +} + +export type SessionPromptAsyncResponse = SessionPromptAsyncResponses[keyof SessionPromptAsyncResponses] + +export type SessionCommandData = { + body?: { + messageID?: string + agent?: string + model?: string + arguments: string + command: string + variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + }> + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/command" +} + +export type SessionCommandErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionCommandError = SessionCommandErrors[keyof SessionCommandErrors] + +export type SessionCommandResponses = { + /** + * Created message + */ + 200: { + info: AssistantMessage + parts: Array + } +} + +export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses] + +export type SessionShellData = { + body?: { + agent: string + model?: { + providerID: string + modelID: string + } + command: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/shell" +} + +export type SessionShellErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionShellError = SessionShellErrors[keyof SessionShellErrors] + +export type SessionShellResponses = { + /** + * Created message + */ + 200: AssistantMessage +} + +export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses] + +export type SessionRevertData = { + body?: { + messageID: string + partID?: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/revert" +} + +export type SessionRevertErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionRevertError = SessionRevertErrors[keyof SessionRevertErrors] + +export type SessionRevertResponses = { + /** + * Updated session + */ + 200: Session +} + +export type SessionRevertResponse = SessionRevertResponses[keyof SessionRevertResponses] + +export type SessionUnrevertData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/unrevert" +} + +export type SessionUnrevertErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionUnrevertError = SessionUnrevertErrors[keyof SessionUnrevertErrors] + +export type SessionUnrevertResponses = { + /** + * Updated session + */ + 200: Session +} + +export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses] + +export type PermissionRespondData = { + body?: { + response: "once" | "always" | "reject" + } + path: { + sessionID: string + permissionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/permissions/{permissionID}" +} + +export type PermissionRespondErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type PermissionRespondError = PermissionRespondErrors[keyof PermissionRespondErrors] + +export type PermissionRespondResponses = { + /** + * Permission processed successfully + */ + 200: boolean +} + +export type PermissionRespondResponse = PermissionRespondResponses[keyof PermissionRespondResponses] + +export type PermissionReplyData = { + body?: { + reply: "once" | "always" | "reject" + message?: string + } + path: { + requestID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/permission/{requestID}/reply" +} + +export type PermissionReplyErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type PermissionReplyError = PermissionReplyErrors[keyof PermissionReplyErrors] + +export type PermissionReplyResponses = { + /** + * Permission processed successfully + */ + 200: boolean +} + +export type PermissionReplyResponse = PermissionReplyResponses[keyof PermissionReplyResponses] + +export type PermissionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/permission" +} + +export type PermissionListResponses = { + /** + * List of pending permissions + */ + 200: Array +} + +export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] + +export type QuestionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/question" +} + +export type QuestionListResponses = { + /** + * List of pending questions + */ + 200: Array +} + +export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] + +export type QuestionReplyData = { + body?: { + /** + * User answers in order of questions (each answer is an array of selected labels) + */ + answers: Array + } + path: { + requestID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/question/{requestID}/reply" +} + +export type QuestionReplyErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors] + +export type QuestionReplyResponses = { + /** + * Question answered successfully + */ + 200: boolean +} + +export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] + +export type QuestionRejectData = { + body?: never + path: { + requestID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/question/{requestID}/reject" +} + +export type QuestionRejectErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors] + +export type QuestionRejectResponses = { + /** + * Question rejected successfully + */ + 200: boolean +} + +export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses] + +export type ProviderListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/provider" +} + +export type ProviderListResponses = { + /** + * List of providers + */ + 200: { + all: Array<{ + api?: string + name: string + env: Array + id: string + npm?: string + models: { + [key: string]: { + id: string + name: string + family?: string + release_date: string + attachment: boolean + reasoning: boolean + temperature: boolean + tool_call: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit: { + context: number + input?: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + options: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + provider?: { + npm?: string + api?: string + } + variants?: { + [key: string]: { + [key: string]: unknown + } + } + } + } + }> + default: { + [key: string]: string + } + connected: Array + } +} + +export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses] + +export type ProviderAuthData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/provider/auth" +} + +export type ProviderAuthResponses = { + /** + * Provider auth methods + */ + 200: { + [key: string]: Array + } +} + +export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses] + +export type ProviderOauthAuthorizeData = { + body?: { + /** + * Auth method index + */ + method: number + /** + * Prompt inputs + */ + inputs?: { + [key: string]: string + } + } + path: { + /** + * Provider ID + */ + providerID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/provider/{providerID}/oauth/authorize" +} + +export type ProviderOauthAuthorizeErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors] + +export type ProviderOauthAuthorizeResponses = { + /** + * Authorization URL and method + */ + 200: ProviderAuthAuthorization +} + +export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses] + +export type ProviderOauthCallbackData = { + body?: { + /** + * Auth method index + */ + method: number + /** + * OAuth authorization code + */ + code?: string + } + path: { + /** + * Provider ID + */ + providerID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/provider/{providerID}/oauth/callback" +} + +export type ProviderOauthCallbackErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors] + +export type ProviderOauthCallbackResponses = { + /** + * OAuth callback processed successfully + */ + 200: boolean +} + +export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] + +export type FindTextData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + pattern: string + } + url: "/find" +} + +export type FindTextResponses = { + /** + * Matches + */ + 200: Array<{ + path: { + text: string + } + lines: { + text: string + } + line_number: number + absolute_offset: number + submatches: Array<{ + match: { + text: string + } + start: number + end: number + }> + }> +} + +export type FindTextResponse = FindTextResponses[keyof FindTextResponses] + +export type FindFilesData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number + } + url: "/find/file" +} + +export type FindFilesResponses = { + /** + * File paths + */ + 200: Array +} + +export type FindFilesResponse = FindFilesResponses[keyof FindFilesResponses] + +export type FindSymbolsData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + query: string + } + url: "/find/symbol" +} + +export type FindSymbolsResponses = { + /** + * Symbols + */ + 200: Array +} + +export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses] + +export type FileListData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + path: string + } + url: "/file" +} + +export type FileListResponses = { + /** + * Files and directories + */ + 200: Array +} + +export type FileListResponse = FileListResponses[keyof FileListResponses] + +export type FileReadData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + path: string + } + url: "/file/content" +} + +export type FileReadResponses = { + /** + * File content + */ + 200: FileContent +} + +export type FileReadResponse = FileReadResponses[keyof FileReadResponses] + +export type FileStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/file/status" +} + +export type FileStatusResponses = { + /** + * File status + */ + 200: Array +} + +export type FileStatusResponse = FileStatusResponses[keyof FileStatusResponses] + +export type EventSubscribeData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/event" +} + +export type EventSubscribeResponses = { + /** + * Event stream + */ + 200: Event +} + +export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] + +export type McpStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp" +} + +export type McpStatusResponses = { + /** + * MCP server status + */ + 200: { + [key: string]: McpStatus + } +} + +export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses] + +export type McpAddData = { + body?: { + name: string + config: McpLocalConfig | McpRemoteConfig + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp" +} + +export type McpAddErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type McpAddError = McpAddErrors[keyof McpAddErrors] + +export type McpAddResponses = { + /** + * MCP server added successfully + */ + 200: { + [key: string]: McpStatus + } +} + +export type McpAddResponse = McpAddResponses[keyof McpAddResponses] + +export type McpAuthRemoveData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth" +} + +export type McpAuthRemoveErrors = { + /** + * Not found + */ + 404: NotFoundError +} + +export type McpAuthRemoveError = McpAuthRemoveErrors[keyof McpAuthRemoveErrors] + +export type McpAuthRemoveResponses = { + /** + * OAuth credentials removed + */ + 200: { + success: true + } +} + +export type McpAuthRemoveResponse = McpAuthRemoveResponses[keyof McpAuthRemoveResponses] + +export type McpAuthStartData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth" +} + +export type McpAuthStartErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type McpAuthStartError = McpAuthStartErrors[keyof McpAuthStartErrors] + +export type McpAuthStartResponses = { + /** + * OAuth flow started + */ + 200: { + /** + * URL to open in browser for authorization + */ + authorizationUrl: string + } +} + +export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartResponses] + +export type McpAuthCallbackData = { + body?: { + /** + * Authorization code from OAuth callback + */ + code: string + } + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth/callback" +} + +export type McpAuthCallbackErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type McpAuthCallbackError = McpAuthCallbackErrors[keyof McpAuthCallbackErrors] + +export type McpAuthCallbackResponses = { + /** + * OAuth authentication completed + */ + 200: McpStatus +} + +export type McpAuthCallbackResponse = McpAuthCallbackResponses[keyof McpAuthCallbackResponses] + +export type McpAuthAuthenticateData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth/authenticate" +} + +export type McpAuthAuthenticateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type McpAuthAuthenticateError = McpAuthAuthenticateErrors[keyof McpAuthAuthenticateErrors] + +export type McpAuthAuthenticateResponses = { + /** + * OAuth authentication completed + */ + 200: McpStatus +} + +export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses] + +export type McpConnectData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/connect" +} + +export type McpConnectResponses = { + /** + * MCP server connected successfully + */ + 200: boolean +} + +export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses] + +export type McpDisconnectData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/disconnect" +} + +export type McpDisconnectResponses = { + /** + * MCP server disconnected successfully + */ + 200: boolean +} + +export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses] + +export type TuiAppendPromptData = { + body?: { + text: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/append-prompt" +} + +export type TuiAppendPromptErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiAppendPromptError = TuiAppendPromptErrors[keyof TuiAppendPromptErrors] + +export type TuiAppendPromptResponses = { + /** + * Prompt processed successfully + */ + 200: boolean +} + +export type TuiAppendPromptResponse = TuiAppendPromptResponses[keyof TuiAppendPromptResponses] + +export type TuiOpenHelpData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-help" +} + +export type TuiOpenHelpResponses = { + /** + * Help dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenHelpResponse = TuiOpenHelpResponses[keyof TuiOpenHelpResponses] + +export type TuiOpenSessionsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-sessions" +} + +export type TuiOpenSessionsResponses = { + /** + * Session dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenSessionsResponse = TuiOpenSessionsResponses[keyof TuiOpenSessionsResponses] + +export type TuiOpenThemesData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-themes" +} + +export type TuiOpenThemesResponses = { + /** + * Theme dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenThemesResponse = TuiOpenThemesResponses[keyof TuiOpenThemesResponses] + +export type TuiOpenModelsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-models" +} + +export type TuiOpenModelsResponses = { + /** + * Model dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenModelsResponse = TuiOpenModelsResponses[keyof TuiOpenModelsResponses] + +export type TuiSubmitPromptData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/submit-prompt" +} + +export type TuiSubmitPromptResponses = { + /** + * Prompt submitted successfully + */ + 200: boolean +} + +export type TuiSubmitPromptResponse = TuiSubmitPromptResponses[keyof TuiSubmitPromptResponses] + +export type TuiClearPromptData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/clear-prompt" +} + +export type TuiClearPromptResponses = { + /** + * Prompt cleared successfully + */ + 200: boolean +} + +export type TuiClearPromptResponse = TuiClearPromptResponses[keyof TuiClearPromptResponses] + +export type TuiExecuteCommandData = { + body?: { + command: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/execute-command" +} + +export type TuiExecuteCommandErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiExecuteCommandError = TuiExecuteCommandErrors[keyof TuiExecuteCommandErrors] + +export type TuiExecuteCommandResponses = { + /** + * Command executed successfully + */ + 200: boolean +} + +export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses] + +export type TuiShowToastData = { + body?: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/show-toast" +} + +export type TuiShowToastResponses = { + /** + * Toast notification shown successfully + */ + 200: boolean +} + +export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses] + +export type TuiPublishData = { + body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/publish" +} + +export type TuiPublishErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiPublishError = TuiPublishErrors[keyof TuiPublishErrors] + +export type TuiPublishResponses = { + /** + * Event published successfully + */ + 200: boolean +} + +export type TuiPublishResponse = TuiPublishResponses[keyof TuiPublishResponses] + +export type TuiSelectSessionData = { + body?: { + /** + * Session ID to navigate to + */ + sessionID: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/select-session" +} + +export type TuiSelectSessionErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type TuiSelectSessionError = TuiSelectSessionErrors[keyof TuiSelectSessionErrors] + +export type TuiSelectSessionResponses = { + /** + * Session selected successfully + */ + 200: boolean +} + +export type TuiSelectSessionResponse = TuiSelectSessionResponses[keyof TuiSelectSessionResponses] + +export type TuiControlNextData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/control/next" +} + +export type TuiControlNextResponses = { + /** + * Next TUI request + */ + 200: { + path: string + body: unknown + } +} + +export type TuiControlNextResponse = TuiControlNextResponses[keyof TuiControlNextResponses] + +export type TuiControlResponseData = { + body?: unknown + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/control/response" +} + +export type TuiControlResponseResponses = { + /** + * Response submitted successfully + */ + 200: boolean +} + +export type TuiControlResponseResponse = TuiControlResponseResponses[keyof TuiControlResponseResponses] + +export type InstanceDisposeData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/instance/dispose" +} + +export type InstanceDisposeResponses = { + /** + * Instance disposed + */ + 200: boolean +} + +export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses] + +export type PathGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/path" +} + +export type PathGetResponses = { + /** + * Path + */ + 200: Path +} + +export type PathGetResponse = PathGetResponses[keyof PathGetResponses] + +export type VcsGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/vcs" +} + +export type VcsGetResponses = { + /** + * VCS info + */ + 200: VcsInfo +} + +export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses] + +export type CommandListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/command" +} + +export type CommandListResponses = { + /** + * List of commands + */ + 200: Array +} + +export type CommandListResponse = CommandListResponses[keyof CommandListResponses] + +export type AppLogData = { + body?: { + /** + * Service name for the log entry + */ + service: string + /** + * Log level + */ + level: "debug" | "info" | "error" | "warn" + /** + * Log message + */ + message: string + /** + * Additional metadata for the log entry + */ + extra?: { + [key: string]: unknown + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/log" +} + +export type AppLogErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AppLogError = AppLogErrors[keyof AppLogErrors] + +export type AppLogResponses = { + /** + * Log entry written successfully + */ + 200: boolean +} + +export type AppLogResponse = AppLogResponses[keyof AppLogResponses] + +export type AppAgentsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/agent" +} + +export type AppAgentsResponses = { + /** + * List of agents + */ + 200: Array +} + +export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses] + +export type AppSkillsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/skill" +} + +export type AppSkillsResponses = { + /** + * List of skills + */ + 200: Array<{ + name: string + description: string + location: string + content: string + }> +} + +export type AppSkillsResponse = AppSkillsResponses[keyof AppSkillsResponses] + +export type LspStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/lsp" +} + +export type LspStatusResponses = { + /** + * LSP server status + */ + 200: Array +} + +export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] + +export type FormatterStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/formatter" +} + +export type FormatterStatusResponses = { + /** + * Formatter status + */ + 200: Array +} + +export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] diff --git a/server/opencode/index.ts b/server/opencode/index.ts new file mode 100644 index 00000000..8078b467 --- /dev/null +++ b/server/opencode/index.ts @@ -0,0 +1,21 @@ +export * from "./client" +export * from "./server" + +import { createOpencodeClient } from "./client" +import { createOpencodeServer } from "./server" +import type { ServerOptions } from "./server" + +export async function createOpencode(options?: ServerOptions) { + const server = await createOpencodeServer({ + ...options, + }) + + const client = createOpencodeClient({ + baseUrl: server.url, + }) + + return { + client, + server, + } +} diff --git a/server/opencode/server.ts b/server/opencode/server.ts new file mode 100644 index 00000000..90da7e23 --- /dev/null +++ b/server/opencode/server.ts @@ -0,0 +1,125 @@ +import { spawn } from "node:child_process" +import { type Config } from "./gen/types.gen" + +export type ServerOptions = { + hostname?: string + port?: number + signal?: AbortSignal + timeout?: number + config?: Config +} + +export type TuiOptions = { + project?: string + model?: string + session?: string + agent?: string + signal?: AbortSignal + config?: Config +} + +export async function createOpencodeServer(options?: ServerOptions) { + options = Object.assign( + { + hostname: "127.0.0.1", + port: 4096, + timeout: 5000, + }, + options ?? {}, + ) + + const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`] + if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`) + + const proc = spawn(`opencode`, args, { + shell: process.platform === 'win32', + signal: options.signal, + env: { + ...process.env, + OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}), + }, + }) + + const url = await new Promise((resolve, reject) => { + const id = setTimeout(() => { + reject(new Error(`Timeout waiting for server to start after ${options.timeout}ms`)) + }, options.timeout) + let output = "" + proc.stdout?.on("data", (chunk) => { + output += chunk.toString() + const lines = output.split("\n") + for (const line of lines) { + if (line.startsWith("opencode server listening")) { + const match = line.match(/on\s+(https?:\/\/[^\s]+)/) + if (!match) { + throw new Error(`Failed to parse server url from output: ${line}`) + } + clearTimeout(id) + resolve(match[1]!) + return + } + } + }) + proc.stderr?.on("data", (chunk) => { + output += chunk.toString() + }) + proc.on("exit", (code) => { + clearTimeout(id) + let msg = `Server exited with code ${code}` + if (output.trim()) { + msg += `\nServer output: ${output}` + } + reject(new Error(msg)) + }) + proc.on("error", (error) => { + clearTimeout(id) + reject(error) + }) + if (options.signal) { + options.signal.addEventListener("abort", () => { + clearTimeout(id) + reject(new Error("Aborted")) + }) + } + }) + + return { + url, + close() { + proc.kill() + }, + } +} + +export function createOpencodeTui(options?: TuiOptions) { + const args = [] + + if (options?.project) { + args.push(`--project=${options.project}`) + } + if (options?.model) { + args.push(`--model=${options.model}`) + } + if (options?.session) { + args.push(`--session=${options.session}`) + } + if (options?.agent) { + args.push(`--agent=${options.agent}`) + } + + const proc = spawn(`opencode`, args, { + shell: process.platform === 'win32', + signal: options?.signal, + stdio: "inherit", + env: { + ...process.env, + OPENCODE_CONFIG_CONTENT: JSON.stringify(options?.config ?? {}), + }, + }) + + return { + close() { + proc.kill() + }, + } +} diff --git a/server/opencode/v2/client.ts b/server/opencode/v2/client.ts new file mode 100644 index 00000000..ad956dd4 --- /dev/null +++ b/server/opencode/v2/client.ts @@ -0,0 +1,39 @@ +export * from "./gen/types.gen.js" + +import { createClient } from "./gen/client/client.gen.js" +import { type Config } from "./gen/client/types.gen.js" +import { OpencodeClient } from "./gen/sdk.gen.js" +export { type Config as OpencodeClientConfig, OpencodeClient } + +export function createOpencodeClient(config?: Config & { directory?: string; experimental_workspaceID?: string }) { + if (!config?.fetch) { + const customFetch: any = (req: any) => { + // @ts-ignore + req.timeout = false + return fetch(req) + } + config = { + ...config, + fetch: customFetch, + } + } + + if (config?.directory) { + const isNonASCII = /[^\x00-\x7F]/.test(config.directory) + const encodedDirectory = isNonASCII ? encodeURIComponent(config.directory) : config.directory + config.headers = { + ...config.headers, + "x-opencode-directory": encodedDirectory, + } + } + + if (config?.experimental_workspaceID) { + config.headers = { + ...config.headers, + "x-opencode-workspace": config.experimental_workspaceID, + } + } + + const client = createClient(config) + return new OpencodeClient({ client }) +} diff --git a/server/opencode/v2/gen/client.gen.ts b/server/opencode/v2/gen/client.gen.ts new file mode 100644 index 00000000..0c110eca --- /dev/null +++ b/server/opencode/v2/gen/client.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from "./client/index.js" +import type { ClientOptions as ClientOptions2 } from "./types.gen.js" + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T> + +export const client = createClient(createConfig({ baseUrl: "http://localhost:4096" })) diff --git a/server/opencode/v2/gen/client/client.gen.ts b/server/opencode/v2/gen/client/client.gen.ts new file mode 100644 index 00000000..627e98ec --- /dev/null +++ b/server/opencode/v2/gen/client/client.gen.ts @@ -0,0 +1,285 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from "../core/serverSentEvents.gen.js" +import type { HttpMethod } from "../core/types.gen.js" +import { getValidRequestBody } from "../core/utils.gen.js" +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen.js" +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from "./utils.gen.js" + +type ReqInit = Omit & { + body?: any + headers: ReturnType +} + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config) + + const getConfig = (): Config => ({ ..._config }) + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config) + return getConfig() + } + + const interceptors = createInterceptors() + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + } + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }) + } + + if (opts.requestValidator) { + await opts.requestValidator(opts) + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === "") { + opts.headers.delete("Content-Type") + } + + const url = buildUrl(opts) + + return { opts, url } + } + + const request: Client["request"] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options) + const requestInit: ReqInit = { + redirect: "follow", + ...opts, + body: getValidRequestBody(opts), + } + + let request = new Request(url, requestInit) + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts) + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch! + let response: Response + + try { + response = await _fetch(request) + } catch (error) { + // Handle fetch exceptions (AbortError, network errors, etc.) + let finalError = error + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, undefined as any, request, opts)) as unknown + } + } + + finalError = finalError || ({} as unknown) + + if (opts.throwOnError) { + throw finalError + } + + // Return error response + return opts.responseStyle === "data" + ? undefined + : { + error: finalError, + request, + response: undefined as any, + } + } + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts) + } + } + + const result = { + request, + response, + } + + if (response.ok) { + const parseAs = + (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json" + + if (response.status === 204 || response.headers.get("Content-Length") === "0") { + let emptyData: any + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "text": + emptyData = await response[parseAs]() + break + case "formData": + emptyData = new FormData() + break + case "stream": + emptyData = response.body + break + case "json": + default: + emptyData = {} + break + } + return opts.responseStyle === "data" + ? emptyData + : { + data: emptyData, + ...result, + } + } + + let data: any + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "formData": + case "text": + data = await response[parseAs]() + break + case "json": { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text() + data = text ? JSON.parse(text) : {} + break + } + case "stream": + return opts.responseStyle === "data" + ? response.body + : { + data: response.body, + ...result, + } + } + + if (parseAs === "json") { + if (opts.responseValidator) { + await opts.responseValidator(data) + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data) + } + } + + return opts.responseStyle === "data" + ? data + : { + data, + ...result, + } + } + + const textError = await response.text() + let jsonError: unknown + + try { + jsonError = JSON.parse(textError) + } catch { + // noop + } + + const error = jsonError ?? textError + let finalError = error + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string + } + } + + finalError = finalError || ({} as string) + + if (opts.throwOnError) { + throw finalError + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === "data" + ? undefined + : { + error: finalError, + ...result, + } + } + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }) + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options) + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init) + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts) + } + } + return request + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }) + } + + return { + buildUrl, + connect: makeMethodFn("CONNECT"), + delete: makeMethodFn("DELETE"), + get: makeMethodFn("GET"), + getConfig, + head: makeMethodFn("HEAD"), + interceptors, + options: makeMethodFn("OPTIONS"), + patch: makeMethodFn("PATCH"), + post: makeMethodFn("POST"), + put: makeMethodFn("PUT"), + request, + setConfig, + sse: { + connect: makeSseFn("CONNECT"), + delete: makeSseFn("DELETE"), + get: makeSseFn("GET"), + head: makeSseFn("HEAD"), + options: makeSseFn("OPTIONS"), + patch: makeSseFn("PATCH"), + post: makeSseFn("POST"), + put: makeSseFn("PUT"), + trace: makeSseFn("TRACE"), + }, + trace: makeMethodFn("TRACE"), + } as Client +} diff --git a/server/opencode/v2/gen/client/index.ts b/server/opencode/v2/gen/client/index.ts new file mode 100644 index 00000000..0af63f33 --- /dev/null +++ b/server/opencode/v2/gen/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from "../core/auth.gen.js" +export type { QuerySerializerOptions } from "../core/bodySerializer.gen.js" +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from "../core/bodySerializer.gen.js" +export { buildClientParams } from "../core/params.gen.js" +export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen.js" +export { createClient } from "./client.gen.js" +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from "./types.gen.js" +export { createConfig, mergeHeaders } from "./utils.gen.js" diff --git a/server/opencode/v2/gen/client/types.gen.ts b/server/opencode/v2/gen/client/types.gen.ts new file mode 100644 index 00000000..e053aa40 --- /dev/null +++ b/server/opencode/v2/gen/client/types.gen.ts @@ -0,0 +1,202 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from "../core/auth.gen.js" +import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen.js" +import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen.js" +import type { Middleware } from "./utils.gen.js" + +export type ResponseStyle = "data" | "fields" + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T["baseUrl"] + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text" + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T["throwOnError"] +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = "fields", + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle + throwOnError: ThrowOnError + }>, + Pick< + ServerSentEventsOptions, + "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay" + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown + path?: Record + query?: Record + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray + url: Url +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = "fields", + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = "fields", +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends "data" + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData + request: Request + response: Response + } + > + : Promise< + TResponseStyle extends "data" + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData + error: undefined + } + | { + data: undefined + error: TError extends Record ? TError[keyof TError] : TError + } + ) & { + request: Request + response: Response + } + > + +export interface ClientOptions { + baseUrl?: string + responseStyle?: ResponseStyle + throwOnError?: boolean +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method">, +) => RequestResult + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method">, +) => Promise> + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method"> & + Pick>, "method">, +) => RequestResult + +type BuildUrlFn = < + TData extends { + body?: unknown + path?: Record + query?: Record + url: string + }, +>( + options: TData & Options, +) => string + +export type Client = CoreClient & { + interceptors: Middleware +} + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T> + +export interface TDataShape { + body?: unknown + headers?: unknown + path?: unknown + query?: unknown + url: string +} + +type OmitKeys = Pick> + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = "fields", +> = OmitKeys, "body" | "path" | "query" | "url"> & + ([TData] extends [never] ? unknown : Omit) diff --git a/server/opencode/v2/gen/client/utils.gen.ts b/server/opencode/v2/gen/client/utils.gen.ts new file mode 100644 index 00000000..3b1dfb78 --- /dev/null +++ b/server/opencode/v2/gen/client/utils.gen.ts @@ -0,0 +1,289 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from "../core/auth.gen.js" +import type { QuerySerializerOptions } from "../core/bodySerializer.gen.js" +import { jsonBodySerializer } from "../core/bodySerializer.gen.js" +import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen.js" +import { getUrl } from "../core/utils.gen.js" +import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen.js" + +export const createQuerySerializer = ({ parameters = {}, ...args }: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = [] + if (queryParams && typeof queryParams === "object") { + for (const name in queryParams) { + const value = queryParams[name] + + if (value === undefined || value === null) { + continue + } + + const options = parameters[name] || args + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: "form", + value, + ...options.array, + }) + if (serializedArray) search.push(serializedArray) + } else if (typeof value === "object") { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: "deepObject", + value: value as Record, + ...options.object, + }) + if (serializedObject) search.push(serializedObject) + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }) + if (serializedPrimitive) search.push(serializedPrimitive) + } + } + } + return search.join("&") + } + return querySerializer +} + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return "stream" + } + + const cleanContent = contentType.split(";")[0]?.trim() + + if (!cleanContent) { + return + } + + if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) { + return "json" + } + + if (cleanContent === "multipart/form-data") { + return "formData" + } + + if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) { + return "blob" + } + + if (cleanContent.startsWith("text/")) { + return "text" + } + + return +} + +const checkForExistence = ( + options: Pick & { + headers: Headers + }, + name?: string, +): boolean => { + if (!name) { + return false + } + if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) { + return true + } + return false +} + +export const setAuthParams = async ({ + security, + ...options +}: Pick, "security"> & + Pick & { + headers: Headers + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue + } + + const token = await getAuthToken(auth, options.auth) + + if (!token) { + continue + } + + const name = auth.name ?? "Authorization" + + switch (auth.in) { + case "query": + if (!options.query) { + options.query = {} + } + options.query[name] = token + break + case "cookie": + options.headers.append("Cookie", `${name}=${token}`) + break + case "header": + default: + options.headers.set(name, token) + break + } + } +} + +export const buildUrl: Client["buildUrl"] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === "function" + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }) + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b } + if (config.baseUrl?.endsWith("/")) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1) + } + config.headers = mergeHeaders(a.headers, b.headers) + return config +} + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = [] + headers.forEach((value, key) => { + entries.push([key, value]) + }) + return entries +} + +export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { + const mergedHeaders = new Headers() + for (const header of headers) { + if (!header) { + continue + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header) + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key) + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string) + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string)) + } + } + } + return mergedHeaders +} + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise + +type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise + +class Interceptors { + fns: Array = [] + + clear(): void { + this.fns = [] + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id) + if (this.fns[index]) { + this.fns[index] = null + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id) + return Boolean(this.fns[index]) + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === "number") { + return this.fns[id] ? id : -1 + } + return this.fns.indexOf(id) + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id) + if (this.fns[index]) { + this.fns[index] = fn + return id + } + return false + } + + use(fn: Interceptor): number { + this.fns.push(fn) + return this.fns.length - 1 + } +} + +export interface Middleware { + error: Interceptors> + request: Interceptors> + response: Interceptors> +} + +export const createInterceptors = (): Middleware => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}) + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: "form", + }, + object: { + explode: true, + style: "deepObject", + }, +}) + +const defaultHeaders = { + "Content-Type": "application/json", +} + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: "auto", + querySerializer: defaultQuerySerializer, + ...override, +}) diff --git a/server/opencode/v2/gen/core/auth.gen.ts b/server/opencode/v2/gen/core/auth.gen.ts new file mode 100644 index 00000000..bc7b230f --- /dev/null +++ b/server/opencode/v2/gen/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: "header" | "query" | "cookie" + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string + scheme?: "basic" | "bearer" + type: "apiKey" | "http" +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === "function" ? await callback(auth) : callback + + if (!token) { + return + } + + if (auth.scheme === "bearer") { + return `Bearer ${token}` + } + + if (auth.scheme === "basic") { + return `Basic ${btoa(token)}` + } + + return token +} diff --git a/server/opencode/v2/gen/core/bodySerializer.gen.ts b/server/opencode/v2/gen/core/bodySerializer.gen.ts new file mode 100644 index 00000000..9678fb08 --- /dev/null +++ b/server/opencode/v2/gen/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen.js" + +export type QuerySerializer = (query: Record) => string + +export type BodySerializer = (body: any) => any + +type QuerySerializerOptionsObject = { + allowReserved?: boolean + array?: Partial> + object?: Partial> +} + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record +} + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === "string" || value instanceof Blob) { + data.append(key, value) + } else if (value instanceof Date) { + data.append(key, value.toISOString()) + } else { + data.append(key, JSON.stringify(value)) + } +} + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === "string") { + data.append(key, value) + } else { + data.append(key, JSON.stringify(value)) + } +} + +export const formDataBodySerializer = { + bodySerializer: | Array>>(body: T): FormData => { + const data = new FormData() + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)) + } else { + serializeFormDataPair(data, key, value) + } + }) + + return data + }, +} + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)), +} + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>(body: T): string => { + const data = new URLSearchParams() + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)) + } else { + serializeUrlSearchParamsPair(data, key, value) + } + }) + + return data.toString() + }, +} diff --git a/server/opencode/v2/gen/core/params.gen.ts b/server/opencode/v2/gen/core/params.gen.ts new file mode 100644 index 00000000..6e9d0b9a --- /dev/null +++ b/server/opencode/v2/gen/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = "body" | "headers" | "path" | "query" + +export type Field = + | { + in: Exclude + /** + * Field name. This is the name we want the user to see and use. + */ + key: string + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string + } + | { + in: Extract + /** + * Key isn't required for bodies. + */ + key?: string + map?: string + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot + } + +export interface Fields { + allowExtra?: Partial> + args?: ReadonlyArray +} + +export type FieldsConfig = ReadonlyArray + +const extraPrefixesMap: Record = { + $body_: "body", + $headers_: "headers", + $path_: "path", + $query_: "query", +} +const extraPrefixes = Object.entries(extraPrefixesMap) + +type KeyMap = Map< + string, + | { + in: Slot + map?: string + } + | { + in?: never + map: Slot + } +> + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map() + } + + for (const config of fields) { + if ("in" in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }) + } + } else if ("key" in config) { + map.set(config.key, { + map: config.map, + }) + } else if (config.args) { + buildKeyMap(config.args, map) + } + } + + return map +} + +interface Params { + body: unknown + headers: Record + path: Record + query: Record +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === "object" && !Object.keys(value).length) { + delete params[slot as Slot] + } + } +} + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + } + + const map = buildKeyMap(fields) + + let config: FieldsConfig[number] | undefined + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index] + } + + if (!config) { + continue + } + + if ("in" in config) { + if (config.key) { + const field = map.get(config.key)! + const name = field.map || config.key + if (field.in) { + ;(params[field.in] as Record)[name] = arg + } + } else { + params.body = arg + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key) + + if (field) { + if (field.in) { + const name = field.map || key + ;(params[field.in] as Record)[name] = value + } else { + params[field.map] = value + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)) + + if (extra) { + const [prefix, slot] = extra + ;(params[slot] as Record)[key.slice(prefix.length)] = value + } else if ("allowExtra" in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + ;(params[slot as Slot] as Record)[key] = value + break + } + } + } + } + } + } + } + + stripEmptySlots(params) + + return params +} diff --git a/server/opencode/v2/gen/core/pathSerializer.gen.ts b/server/opencode/v2/gen/core/pathSerializer.gen.ts new file mode 100644 index 00000000..96be3bc5 --- /dev/null +++ b/server/opencode/v2/gen/core/pathSerializer.gen.ts @@ -0,0 +1,167 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean + name: string +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean + style: T +} + +export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited" +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle +type MatrixStyle = "label" | "matrix" | "simple" +export type ObjectStyle = "form" | "deepObject" +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case "label": + return "." + case "matrix": + return ";" + case "simple": + return "," + default: + return "&" + } +} + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case "form": + return "," + case "pipeDelimited": + return "|" + case "spaceDelimited": + return "%20" + default: + return "," + } +} + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case "label": + return "." + case "matrix": + return ";" + case "simple": + return "," + default: + return "&" + } +} + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[] +}) => { + if (!explode) { + const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join( + separatorArrayNoExplode(style), + ) + switch (style) { + case "label": + return `.${joinedValues}` + case "matrix": + return `;${name}=${joinedValues}` + case "simple": + return joinedValues + default: + return `${name}=${joinedValues}` + } + } + + const separator = separatorArrayExplode(style) + const joinedValues = value + .map((v) => { + if (style === "label" || style === "simple") { + return allowReserved ? v : encodeURIComponent(v as string) + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }) + }) + .join(separator) + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues +} + +export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return "" + } + + if (typeof value === "object") { + throw new Error( + "Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.", + ) + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}` +} + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date + valueOnly?: boolean +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}` + } + + if (style !== "deepObject" && !explode) { + let values: string[] = [] + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)] + }) + const joinedValues = values.join(",") + switch (style) { + case "form": + return `${name}=${joinedValues}` + case "label": + return `.${joinedValues}` + case "matrix": + return `;${name}=${joinedValues}` + default: + return joinedValues + } + } + + const separator = separatorObjectExplode(style) + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === "deepObject" ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator) + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues +} diff --git a/server/opencode/v2/gen/core/queryKeySerializer.gen.ts b/server/opencode/v2/gen/core/queryKeySerializer.gen.ts new file mode 100644 index 00000000..320204ae --- /dev/null +++ b/server/opencode/v2/gen/core/queryKeySerializer.gen.ts @@ -0,0 +1,111 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue } + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === "function" || typeof value === "symbol") { + return undefined + } + if (typeof value === "bigint") { + return value.toString() + } + if (value instanceof Date) { + return value.toISOString() + } + return value +} + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer) + if (json === undefined) { + return undefined + } + return JSON.parse(json) as JsonValue + } catch { + return undefined + } +} + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== "object") { + return false + } + const prototype = Object.getPrototypeOf(value as object) + return prototype === Object.prototype || prototype === null +} + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)) + const result: Record = {} + + for (const [key, value] of entries) { + const existing = result[key] + if (existing === undefined) { + result[key] = value + continue + } + + if (Array.isArray(existing)) { + ;(existing as string[]).push(value) + } else { + result[key] = [existing, value] + } + } + + return result +} + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null + } + + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return value + } + + if (value === undefined || typeof value === "function" || typeof value === "symbol") { + return undefined + } + + if (typeof value === "bigint") { + return value.toString() + } + + if (value instanceof Date) { + return value.toISOString() + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value) + } + + if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) { + return serializeSearchParams(value) + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value) + } + + return undefined +} diff --git a/server/opencode/v2/gen/core/serverSentEvents.gen.ts b/server/opencode/v2/gen/core/serverSentEvents.gen.ts new file mode 100644 index 00000000..056a8125 --- /dev/null +++ b/server/opencode/v2/gen/core/serverSentEvents.gen.ts @@ -0,0 +1,239 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from "./types.gen.js" + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void + serializedBody?: RequestInit["body"] + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise + url: string + } + +export interface StreamEvent { + data: TData + event?: string + id?: string + retry?: number +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator ? TData[keyof TData] : TData, TReturn, TNext> +} + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))) + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000 + let attempt = 0 + const signal = options.signal ?? new AbortController().signal + + while (true) { + if (signal.aborted) break + + attempt++ + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined) + + if (lastEventId !== undefined) { + headers.set("Last-Event-ID", lastEventId) + } + + try { + const requestInit: RequestInit = { + redirect: "follow", + ...options, + body: options.serializedBody, + headers, + signal, + } + let request = new Request(url, requestInit) + if (onRequest) { + request = await onRequest(url, requestInit) + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch + const response = await _fetch(request) + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`) + + if (!response.body) throw new Error("No body in SSE response") + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader() + + let buffer = "" + + const abortHandler = () => { + try { + reader.cancel() + } catch { + // noop + } + } + + signal.addEventListener("abort", abortHandler) + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += value + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n") + + const chunks = buffer.split("\n\n") + buffer = chunks.pop() ?? "" + + for (const chunk of chunks) { + const lines = chunk.split("\n") + const dataLines: Array = [] + let eventName: string | undefined + + for (const line of lines) { + if (line.startsWith("data:")) { + dataLines.push(line.replace(/^data:\s*/, "")) + } else if (line.startsWith("event:")) { + eventName = line.replace(/^event:\s*/, "") + } else if (line.startsWith("id:")) { + lastEventId = line.replace(/^id:\s*/, "") + } else if (line.startsWith("retry:")) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10) + if (!Number.isNaN(parsed)) { + retryDelay = parsed + } + } + } + + let data: unknown + let parsedJson = false + + if (dataLines.length) { + const rawData = dataLines.join("\n") + try { + data = JSON.parse(rawData) + parsedJson = true + } catch { + data = rawData + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data) + } + + if (responseTransformer) { + data = await responseTransformer(data) + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }) + + if (dataLines.length) { + yield data as any + } + } + } + } finally { + signal.removeEventListener("abort", abortHandler) + reader.releaseLock() + } + + break // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error) + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000) + await sleep(backoff) + } + } + } + + const stream = createStream() + + return { stream } +} diff --git a/server/opencode/v2/gen/core/types.gen.ts b/server/opencode/v2/gen/core/types.gen.ts new file mode 100644 index 00000000..bfa77b8a --- /dev/null +++ b/server/opencode/v2/gen/core/types.gen.ts @@ -0,0 +1,86 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from "./auth.gen.js" +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js" + +export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace" + +export type Client = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn + getConfig: () => Config + request: RequestFn + setConfig: (config: Config) => Config +} & { + [K in HttpMethod]: MethodFn +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }) + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit["headers"] + | Record + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K] +} diff --git a/server/opencode/v2/gen/core/utils.gen.ts b/server/opencode/v2/gen/core/utils.gen.ts new file mode 100644 index 00000000..8a45f726 --- /dev/null +++ b/server/opencode/v2/gen/core/utils.gen.ts @@ -0,0 +1,137 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen.js" +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from "./pathSerializer.gen.js" + +export interface PathSerializer { + path: Record + url: string +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url + const matches = _url.match(PATH_PARAM_RE) + if (matches) { + for (const match of matches) { + let explode = false + let name = match.substring(1, match.length - 1) + let style: ArraySeparatorStyle = "simple" + + if (name.endsWith("*")) { + explode = true + name = name.substring(0, name.length - 1) + } + + if (name.startsWith(".")) { + name = name.substring(1) + style = "label" + } else if (name.startsWith(";")) { + name = name.substring(1) + style = "matrix" + } + + const value = path[name] + + if (value === undefined || value === null) { + continue + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })) + continue + } + + if (typeof value === "object") { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ) + continue + } + + if (style === "matrix") { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ) + continue + } + + const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string)) + url = url.replace(match, replaceValue) + } + } + return url +} + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string + path?: Record + query?: Record + querySerializer: QuerySerializer + url: string +}) => { + const pathUrl = _url.startsWith("/") ? _url : `/${_url}` + let url = (baseUrl ?? "") + pathUrl + if (path) { + url = defaultPathSerializer({ path, url }) + } + let search = query ? querySerializer(query) : "" + if (search.startsWith("?")) { + search = search.substring(1) + } + if (search) { + url += `?${search}` + } + return url +} + +export function getValidRequestBody(options: { + body?: unknown + bodySerializer?: BodySerializer | null + serializedBody?: unknown +}) { + const hasBody = options.body !== undefined + const isSerializedBody = hasBody && options.bodySerializer + + if (isSerializedBody) { + if ("serializedBody" in options) { + const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== "" + + return hasSerializedBody ? options.serializedBody : null + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== "" ? options.body : null + } + + // plain/text body + if (hasBody) { + return options.body + } + + // no body was provided + return undefined +} diff --git a/server/opencode/v2/gen/sdk.gen.ts b/server/opencode/v2/gen/sdk.gen.ts new file mode 100644 index 00000000..b6821322 --- /dev/null +++ b/server/opencode/v2/gen/sdk.gen.ts @@ -0,0 +1,4033 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { client } from "./client.gen.js" +import { buildClientParams, type Client, type Options as Options2, type TDataShape } from "./client/index.js" +import type { + AgentPartInput, + AppAgentsResponses, + AppLogErrors, + AppLogResponses, + AppSkillsResponses, + Auth as Auth3, + AuthRemoveErrors, + AuthRemoveResponses, + AuthSetErrors, + AuthSetResponses, + CommandListResponses, + Config as Config3, + ConfigGetResponses, + ConfigProvidersResponses, + ConfigUpdateErrors, + ConfigUpdateResponses, + EventSubscribeResponses, + EventTuiCommandExecute, + EventTuiPromptAppend, + EventTuiSessionSelect, + EventTuiToastShow, + ExperimentalResourceListResponses, + ExperimentalSessionListResponses, + ExperimentalWorkspaceCreateErrors, + ExperimentalWorkspaceCreateResponses, + ExperimentalWorkspaceListResponses, + ExperimentalWorkspaceRemoveErrors, + ExperimentalWorkspaceRemoveResponses, + FileListResponses, + FilePartInput, + FilePartSource, + FileReadResponses, + FileStatusResponses, + FindFilesResponses, + FindSymbolsResponses, + FindTextResponses, + FormatterStatusResponses, + GlobalConfigGetResponses, + GlobalConfigUpdateErrors, + GlobalConfigUpdateResponses, + GlobalDisposeResponses, + GlobalEventResponses, + GlobalHealthResponses, + InstanceDisposeResponses, + LspStatusResponses, + McpAddErrors, + McpAddResponses, + McpAuthAuthenticateErrors, + McpAuthAuthenticateResponses, + McpAuthCallbackErrors, + McpAuthCallbackResponses, + McpAuthRemoveErrors, + McpAuthRemoveResponses, + McpAuthStartErrors, + McpAuthStartResponses, + McpConnectResponses, + McpDisconnectResponses, + McpLocalConfig, + McpRemoteConfig, + McpStatusResponses, + OutputFormat, + Part as Part2, + PartDeleteErrors, + PartDeleteResponses, + PartUpdateErrors, + PartUpdateResponses, + PathGetResponses, + PermissionListResponses, + PermissionReplyErrors, + PermissionReplyResponses, + PermissionRespondErrors, + PermissionRespondResponses, + PermissionRuleset, + ProjectCurrentResponses, + ProjectInitGitResponses, + ProjectListResponses, + ProjectUpdateErrors, + ProjectUpdateResponses, + ProviderAuthResponses, + ProviderListResponses, + ProviderOauthAuthorizeErrors, + ProviderOauthAuthorizeResponses, + ProviderOauthCallbackErrors, + ProviderOauthCallbackResponses, + PtyConnectErrors, + PtyConnectResponses, + PtyCreateErrors, + PtyCreateResponses, + PtyGetErrors, + PtyGetResponses, + PtyListResponses, + PtyRemoveErrors, + PtyRemoveResponses, + PtyUpdateErrors, + PtyUpdateResponses, + QuestionAnswer, + QuestionListResponses, + QuestionRejectErrors, + QuestionRejectResponses, + QuestionReplyErrors, + QuestionReplyResponses, + SessionAbortErrors, + SessionAbortResponses, + SessionChildrenErrors, + SessionChildrenResponses, + SessionCommandErrors, + SessionCommandResponses, + SessionCreateErrors, + SessionCreateResponses, + SessionDeleteErrors, + SessionDeleteMessageErrors, + SessionDeleteMessageResponses, + SessionDeleteResponses, + SessionDiffResponses, + SessionForkResponses, + SessionGetErrors, + SessionGetResponses, + SessionInitErrors, + SessionInitResponses, + SessionListResponses, + SessionMessageErrors, + SessionMessageResponses, + SessionMessagesErrors, + SessionMessagesResponses, + SessionPromptAsyncErrors, + SessionPromptAsyncResponses, + SessionPromptErrors, + SessionPromptResponses, + SessionRevertErrors, + SessionRevertResponses, + SessionShareErrors, + SessionShareResponses, + SessionShellErrors, + SessionShellResponses, + SessionStatusErrors, + SessionStatusResponses, + SessionSummarizeErrors, + SessionSummarizeResponses, + SessionTodoErrors, + SessionTodoResponses, + SessionUnrevertErrors, + SessionUnrevertResponses, + SessionUnshareErrors, + SessionUnshareResponses, + SessionUpdateErrors, + SessionUpdateResponses, + SubtaskPartInput, + TextPartInput, + ToolIdsErrors, + ToolIdsResponses, + ToolListErrors, + ToolListResponses, + TuiAppendPromptErrors, + TuiAppendPromptResponses, + TuiClearPromptResponses, + TuiControlNextResponses, + TuiControlResponseResponses, + TuiExecuteCommandErrors, + TuiExecuteCommandResponses, + TuiOpenHelpResponses, + TuiOpenModelsResponses, + TuiOpenSessionsResponses, + TuiOpenThemesResponses, + TuiPublishErrors, + TuiPublishResponses, + TuiSelectSessionErrors, + TuiSelectSessionResponses, + TuiShowToastResponses, + TuiSubmitPromptResponses, + VcsGetResponses, + WorktreeCreateErrors, + WorktreeCreateInput, + WorktreeCreateResponses, + WorktreeListResponses, + WorktreeRemoveErrors, + WorktreeRemoveInput, + WorktreeRemoveResponses, + WorktreeResetErrors, + WorktreeResetInput, + WorktreeResetResponses, +} from "./types.gen.js" + +export type Options = Options2< + TData, + ThrowOnError +> & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record +} + +class HeyApiClient { + protected client: Client + + constructor(args?: { client?: Client }) { + this.client = args?.client ?? client + } +} + +class HeyApiRegistry { + private readonly defaultKey = "default" + + private readonly instances: Map = new Map() + + get(key?: string): T { + const instance = this.instances.get(key ?? this.defaultKey) + if (!instance) { + throw new Error(`No SDK client found. Create one with "new OpencodeClient()" to fix this error.`) + } + return instance + } + + set(value: T, key?: string): void { + this.instances.set(key ?? this.defaultKey, value) + } +} + +export class Config extends HeyApiClient { + /** + * Get global configuration + * + * Retrieve the current global OpenCode configuration settings and preferences. + */ + public get(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/config", + ...options, + }) + } + + /** + * Update global configuration + * + * Update global OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }]) + return (options?.client ?? this.client).patch({ + url: "/global/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Global extends HeyApiClient { + /** + * Get health + * + * Get health information about the OpenCode server. + */ + public health(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/health", + ...options, + }) + } + + /** + * Get global events + * + * Subscribe to global events from the OpenCode system using server-sent events. + */ + public event(options?: Options) { + return (options?.client ?? this.client).sse.get({ + url: "/global/event", + ...options, + }) + } + + /** + * Dispose instance + * + * Clean up and dispose all OpenCode instances, releasing all resources. + */ + public dispose(options?: Options) { + return (options?.client ?? this.client).post({ + url: "/global/dispose", + ...options, + }) + } + + private _config?: Config + get config(): Config { + return (this._config ??= new Config({ client: this.client })) + } +} + +export class Auth extends HeyApiClient { + /** + * Remove auth credentials + * + * Remove authentication credentials + */ + public remove( + parameters: { + providerID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) + return (options?.client ?? this.client).delete({ + url: "/auth/{providerID}", + ...options, + ...params, + }) + } + + /** + * Set auth credentials + * + * Set authentication credentials + */ + public set( + parameters: { + providerID: string + auth?: Auth3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { key: "auth", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/auth/{providerID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Project extends HeyApiClient { + /** + * List all projects + * + * Get a list of projects that have been opened with OpenCode. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project", + ...options, + ...params, + }) + } + + /** + * Get current project + * + * Retrieve the currently active project that OpenCode is working with. + */ + public current( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project/current", + ...options, + ...params, + }) + } + + /** + * Initialize git repository + * + * Create a git repository for the current project and return the refreshed project info. + */ + public initGit( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/project/git/init", + ...options, + ...params, + }) + } + + /** + * Update project + * + * Update project properties such as name, icon, and commands. + */ + public update( + parameters: { + projectID: string + directory?: string + workspace?: string + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "icon" }, + { in: "body", key: "commands" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/project/{projectID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Pty extends HeyApiClient { + /** + * List PTY sessions + * + * Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty", + ...options, + ...params, + }) + } + + /** + * Create PTY session + * + * Create a new pseudo-terminal (PTY) session for running shell commands and processes. + */ + public create( + parameters?: { + directory?: string + workspace?: string + command?: string + args?: Array + cwd?: string + title?: string + env?: { + [key: string]: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "command" }, + { in: "body", key: "args" }, + { in: "body", key: "cwd" }, + { in: "body", key: "title" }, + { in: "body", key: "env" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/pty", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Remove PTY session + * + * Remove and terminate a specific pseudo-terminal (PTY) session. + */ + public remove( + parameters: { + ptyID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/pty/{ptyID}", + ...options, + ...params, + }) + } + + /** + * Get PTY session + * + * Retrieve detailed information about a specific pseudo-terminal (PTY) session. + */ + public get( + parameters: { + ptyID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty/{ptyID}", + ...options, + ...params, + }) + } + + /** + * Update PTY session + * + * Update properties of an existing pseudo-terminal (PTY) session. + */ + public update( + parameters: { + ptyID: string + directory?: string + workspace?: string + title?: string + size?: { + rows: number + cols: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "size" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/pty/{ptyID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Connect to PTY session + * + * Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time. + */ + public connect( + parameters: { + ptyID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty/{ptyID}/connect", + ...options, + ...params, + }) + } +} + +export class Config2 extends HeyApiClient { + /** + * Get configuration + * + * Retrieve the current OpenCode configuration settings and preferences. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config", + ...options, + ...params, + }) + } + + /** + * Update configuration + * + * Update OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + directory?: string + workspace?: string + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "config", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List config providers + * + * Get a list of all configured AI providers and their default models. + */ + public providers( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config/providers", + ...options, + ...params, + }) + } +} + +export class Tool extends HeyApiClient { + /** + * List tool IDs + * + * Get a list of all available tool IDs, including both built-in tools and dynamically registered tools. + */ + public ids( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/tool/ids", + ...options, + ...params, + }) + } + + /** + * List tools + * + * Get a list of available tools with their JSON schema parameters for a specific provider and model combination. + */ + public list( + parameters: { + directory?: string + workspace?: string + provider: string + model: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "provider" }, + { in: "query", key: "model" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/tool", + ...options, + ...params, + }) + } +} + +export class Workspace extends HeyApiClient { + /** + * List workspaces + * + * List all workspaces. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/workspace", + ...options, + ...params, + }) + } + + /** + * Create workspace + * + * Create a workspace for the current project. + */ + public create( + parameters?: { + directory?: string + workspace?: string + id?: string + type?: string + branch?: string | null + extra?: unknown | null + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "type" }, + { in: "body", key: "branch" }, + { in: "body", key: "extra" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceCreateResponses, + ExperimentalWorkspaceCreateErrors, + ThrowOnError + >({ + url: "/experimental/workspace", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Remove workspace + * + * Remove an existing workspace. + */ + public remove( + parameters: { + id: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "id" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceRemoveErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}", + ...options, + ...params, + }) + } +} + +export class Session extends HeyApiClient { + /** + * List sessions + * + * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default. + */ + public list( + parameters?: { + directory?: string + workspace?: string + roots?: boolean + start?: number + cursor?: number + search?: string + limit?: number + archived?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "cursor" }, + { in: "query", key: "search" }, + { in: "query", key: "limit" }, + { in: "query", key: "archived" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/session", + ...options, + ...params, + }) + } +} + +export class Resource extends HeyApiClient { + /** + * Get MCP resources + * + * Get all available MCP resources from connected servers. Optionally filter by name. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/resource", + ...options, + ...params, + }) + } +} + +export class Experimental extends HeyApiClient { + private _workspace?: Workspace + get workspace(): Workspace { + return (this._workspace ??= new Workspace({ client: this.client })) + } + + private _session?: Session + get session(): Session { + return (this._session ??= new Session({ client: this.client })) + } + + private _resource?: Resource + get resource(): Resource { + return (this._resource ??= new Resource({ client: this.client })) + } +} + +export class Worktree extends HeyApiClient { + /** + * Remove worktree + * + * Remove a git worktree and delete its branch. + */ + public remove( + parameters?: { + directory?: string + workspace?: string + worktreeRemoveInput?: WorktreeRemoveInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "worktreeRemoveInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/experimental/worktree", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List worktrees + * + * List all sandbox worktrees for the current project. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/worktree", + ...options, + ...params, + }) + } + + /** + * Create worktree + * + * Create a new git worktree for the current project and run any configured startup scripts. + */ + public create( + parameters?: { + directory?: string + workspace?: string + worktreeCreateInput?: WorktreeCreateInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "worktreeCreateInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/worktree", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Reset worktree + * + * Reset a worktree branch to the primary default branch. + */ + public reset( + parameters?: { + directory?: string + workspace?: string + worktreeResetInput?: WorktreeResetInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "worktreeResetInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/worktree/reset", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Session2 extends HeyApiClient { + /** + * List sessions + * + * Get a list of all OpenCode sessions, sorted by most recently updated. + */ + public list( + parameters?: { + directory?: string + workspace?: string + roots?: boolean + start?: number + search?: string + limit?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "search" }, + { in: "query", key: "limit" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session", + ...options, + ...params, + }) + } + + /** + * Create session + * + * Create a new OpenCode session for interacting with AI assistants and managing conversations. + */ + public create( + parameters?: { + directory?: string + workspace?: string + parentID?: string + title?: string + permission?: PermissionRuleset + workspaceID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "parentID" }, + { in: "body", key: "title" }, + { in: "body", key: "permission" }, + { in: "body", key: "workspaceID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get session status + * + * Retrieve the current status of all sessions, including active, idle, and completed states. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/status", + ...options, + ...params, + }) + } + + /** + * Delete session + * + * Delete a session and permanently remove all associated data, including messages and history. + */ + public delete( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}", + ...options, + ...params, + }) + } + + /** + * Get session + * + * Retrieve detailed information about a specific OpenCode session. + */ + public get( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}", + ...options, + ...params, + }) + } + + /** + * Update session + * + * Update properties of an existing session, such as title or other metadata. + */ + public update( + parameters: { + sessionID: string + directory?: string + workspace?: string + title?: string + time?: { + archived?: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "time" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/session/{sessionID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get session children + * + * Retrieve all child sessions that were forked from the specified parent session. + */ + public children( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/children", + ...options, + ...params, + }) + } + + /** + * Get session todos + * + * Retrieve the todo list associated with a specific session, showing tasks and action items. + */ + public todo( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/todo", + ...options, + ...params, + }) + } + + /** + * Initialize session + * + * Analyze the current application and create an AGENTS.md file with project-specific agent configurations. + */ + public init( + parameters: { + sessionID: string + directory?: string + workspace?: string + modelID?: string + providerID?: string + messageID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "modelID" }, + { in: "body", key: "providerID" }, + { in: "body", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/init", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Fork session + * + * Create a new session by forking an existing session at a specific message point. + */ + public fork( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/fork", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Abort session + * + * Abort an active session and stop any ongoing AI processing or command execution. + */ + public abort( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/abort", + ...options, + ...params, + }) + } + + /** + * Unshare session + * + * Remove the shareable link for a session, making it private again. + */ + public unshare( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}/share", + ...options, + ...params, + }) + } + + /** + * Share session + * + * Create a shareable link for a session, allowing others to view the conversation. + */ + public share( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/share", + ...options, + ...params, + }) + } + + /** + * Get message diff + * + * Get the file changes (diff) that resulted from a specific user message in the session. + */ + public diff( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/diff", + ...options, + ...params, + }) + } + + /** + * Summarize session + * + * Generate a concise summary of the session using AI compaction to preserve key information. + */ + public summarize( + parameters: { + sessionID: string + directory?: string + workspace?: string + providerID?: string + modelID?: string + auto?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "providerID" }, + { in: "body", key: "modelID" }, + { in: "body", key: "auto" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/summarize", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get session messages + * + * Retrieve all messages in a session, including user prompts and AI responses. + */ + public messages( + parameters: { + sessionID: string + directory?: string + workspace?: string + limit?: number + before?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "limit" }, + { in: "query", key: "before" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/message", + ...options, + ...params, + }) + } + + /** + * Send message + * + * Create and send a new message to a session, streaming the AI response. + */ + public prompt( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "model" }, + { in: "body", key: "agent" }, + { in: "body", key: "noReply" }, + { in: "body", key: "tools" }, + { in: "body", key: "format" }, + { in: "body", key: "system" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/message", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Delete message + * + * Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message. + */ + public deleteMessage( + parameters: { + sessionID: string + messageID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + SessionDeleteMessageResponses, + SessionDeleteMessageErrors, + ThrowOnError + >({ + url: "/session/{sessionID}/message/{messageID}", + ...options, + ...params, + }) + } + + /** + * Get message + * + * Retrieve a specific message from a session by its message ID. + */ + public message( + parameters: { + sessionID: string + messageID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/message/{messageID}", + ...options, + ...params, + }) + } + + /** + * Send async message + * + * Create and send a new message to a session asynchronously, starting the session if needed and returning immediately. + */ + public promptAsync( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "model" }, + { in: "body", key: "agent" }, + { in: "body", key: "noReply" }, + { in: "body", key: "tools" }, + { in: "body", key: "format" }, + { in: "body", key: "system" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/prompt_async", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Send command + * + * Send a new command to a session for execution by the AI assistant. + */ + public command( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + agent?: string + model?: string + arguments?: string + command?: string + variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + }> + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "arguments" }, + { in: "body", key: "command" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/command", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Run shell command + * + * Execute a shell command within the session context and return the AI's response. + */ + public shell( + parameters: { + sessionID: string + directory?: string + workspace?: string + agent?: string + model?: { + providerID: string + modelID: string + } + command?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "command" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/shell", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Revert message + * + * Revert a specific message in a session, undoing its effects and restoring the previous state. + */ + public revert( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + partID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "partID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/revert", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Restore reverted messages + * + * Restore all previously reverted messages in a session. + */ + public unrevert( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/unrevert", + ...options, + ...params, + }) + } +} + +export class Part extends HeyApiClient { + /** + * Delete a part from a message + */ + public delete( + parameters: { + sessionID: string + messageID: string + partID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "path", key: "partID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}/message/{messageID}/part/{partID}", + ...options, + ...params, + }) + } + + /** + * Update a part in a message + */ + public update( + parameters: { + sessionID: string + messageID: string + partID: string + directory?: string + workspace?: string + part?: Part2 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "path", key: "partID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "part", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/session/{sessionID}/message/{messageID}/part/{partID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Permission extends HeyApiClient { + /** + * Respond to permission + * + * Approve or deny a permission request from the AI assistant. + * + * @deprecated + */ + public respond( + parameters: { + sessionID: string + permissionID: string + directory?: string + workspace?: string + response?: "once" | "always" | "reject" + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "permissionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "response" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/permissions/{permissionID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Respond to permission request + * + * Approve or deny a permission request from the AI assistant. + */ + public reply( + parameters: { + requestID: string + directory?: string + workspace?: string + reply?: "once" | "always" | "reject" + message?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "requestID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "reply" }, + { in: "body", key: "message" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/permission/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List pending permissions + * + * Get all pending permission requests across all sessions. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/permission", + ...options, + ...params, + }) + } +} + +export class Question extends HeyApiClient { + /** + * List pending questions + * + * Get all pending question requests across all sessions. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/question", + ...options, + ...params, + }) + } + + /** + * Reply to question request + * + * Provide answers to a question request from the AI assistant. + */ + public reply( + parameters: { + requestID: string + directory?: string + workspace?: string + answers?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "requestID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "answers" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/question/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Reject question request + * + * Reject a question request from the AI assistant. + */ + public reject( + parameters: { + requestID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "requestID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/question/{requestID}/reject", + ...options, + ...params, + }) + } +} + +export class Oauth extends HeyApiClient { + /** + * OAuth authorize + * + * Initiate OAuth authorization for a specific AI provider to get an authorization URL. + */ + public authorize( + parameters: { + providerID: string + directory?: string + workspace?: string + method?: number + inputs?: { + [key: string]: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "method" }, + { in: "body", key: "inputs" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ProviderOauthAuthorizeResponses, + ProviderOauthAuthorizeErrors, + ThrowOnError + >({ + url: "/provider/{providerID}/oauth/authorize", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * OAuth callback + * + * Handle the OAuth callback from a provider after user authorization. + */ + public callback( + parameters: { + providerID: string + directory?: string + workspace?: string + method?: number + code?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "method" }, + { in: "body", key: "code" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ProviderOauthCallbackResponses, + ProviderOauthCallbackErrors, + ThrowOnError + >({ + url: "/provider/{providerID}/oauth/callback", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Provider extends HeyApiClient { + /** + * List providers + * + * Get a list of all available AI providers, including both available and connected ones. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/provider", + ...options, + ...params, + }) + } + + /** + * Get provider auth methods + * + * Retrieve available authentication methods for all AI providers. + */ + public auth( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/provider/auth", + ...options, + ...params, + }) + } + + private _oauth?: Oauth + get oauth(): Oauth { + return (this._oauth ??= new Oauth({ client: this.client })) + } +} + +export class Find extends HeyApiClient { + /** + * Find text + * + * Search for text patterns across files in the project using ripgrep. + */ + public text( + parameters: { + directory?: string + workspace?: string + pattern: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "pattern" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/find", + ...options, + ...params, + }) + } + + /** + * Find files + * + * Search for files or directories by name or pattern in the project directory. + */ + public files( + parameters: { + directory?: string + workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "query" }, + { in: "query", key: "dirs" }, + { in: "query", key: "type" }, + { in: "query", key: "limit" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/find/file", + ...options, + ...params, + }) + } + + /** + * Find symbols + * + * Search for workspace symbols like functions, classes, and variables using LSP. + */ + public symbols( + parameters: { + directory?: string + workspace?: string + query: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "query" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/find/symbol", + ...options, + ...params, + }) + } +} + +export class File extends HeyApiClient { + /** + * List files + * + * List files and directories in a specified path. + */ + public list( + parameters: { + directory?: string + workspace?: string + path: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "path" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/file", + ...options, + ...params, + }) + } + + /** + * Read file + * + * Read the content of a specified file. + */ + public read( + parameters: { + directory?: string + workspace?: string + path: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "path" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/file/content", + ...options, + ...params, + }) + } + + /** + * Get file status + * + * Get the git status of all files in the project. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/file/status", + ...options, + ...params, + }) + } +} + +export class Event extends HeyApiClient { + /** + * Subscribe to events + * + * Get events + */ + public subscribe( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).sse.get({ + url: "/event", + ...options, + ...params, + }) + } +} + +export class Auth2 extends HeyApiClient { + /** + * Remove MCP OAuth + * + * Remove OAuth credentials for an MCP server + */ + public remove( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/mcp/{name}/auth", + ...options, + ...params, + }) + } + + /** + * Start MCP OAuth + * + * Start OAuth authentication flow for a Model Context Protocol (MCP) server. + */ + public start( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth", + ...options, + ...params, + }) + } + + /** + * Complete MCP OAuth + * + * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. + */ + public callback( + parameters: { + name: string + directory?: string + workspace?: string + code?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "code" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth/callback", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Authenticate MCP OAuth + * + * Start OAuth flow and wait for callback (opens browser) + */ + public authenticate( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/mcp/{name}/auth/authenticate", + ...options, + ...params, + }, + ) + } +} + +export class Mcp extends HeyApiClient { + /** + * Get MCP status + * + * Get the status of all Model Context Protocol (MCP) servers. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/mcp", + ...options, + ...params, + }) + } + + /** + * Add MCP server + * + * Dynamically add a new Model Context Protocol (MCP) server to the system. + */ + public add( + parameters?: { + directory?: string + workspace?: string + name?: string + config?: McpLocalConfig | McpRemoteConfig + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "config" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Connect an MCP server + */ + public connect( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/connect", + ...options, + ...params, + }) + } + + /** + * Disconnect an MCP server + */ + public disconnect( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/disconnect", + ...options, + ...params, + }) + } + + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) + } +} + +export class Control extends HeyApiClient { + /** + * Get next TUI request + * + * Retrieve the next TUI (Terminal User Interface) request from the queue for processing. + */ + public next( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/tui/control/next", + ...options, + ...params, + }) + } + + /** + * Submit TUI response + * + * Submit a response to the TUI request queue to complete a pending request. + */ + public response( + parameters?: { + directory?: string + workspace?: string + body?: unknown + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "body", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/control/response", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Tui extends HeyApiClient { + /** + * Append TUI prompt + * + * Append prompt to the TUI + */ + public appendPrompt( + parameters?: { + directory?: string + workspace?: string + text?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "text" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/append-prompt", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Open help dialog + * + * Open the help dialog in the TUI to display user assistance information. + */ + public openHelp( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-help", + ...options, + ...params, + }) + } + + /** + * Open sessions dialog + * + * Open the session dialog + */ + public openSessions( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-sessions", + ...options, + ...params, + }) + } + + /** + * Open themes dialog + * + * Open the theme dialog + */ + public openThemes( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-themes", + ...options, + ...params, + }) + } + + /** + * Open models dialog + * + * Open the model dialog + */ + public openModels( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-models", + ...options, + ...params, + }) + } + + /** + * Submit TUI prompt + * + * Submit the prompt + */ + public submitPrompt( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/submit-prompt", + ...options, + ...params, + }) + } + + /** + * Clear TUI prompt + * + * Clear the prompt + */ + public clearPrompt( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/clear-prompt", + ...options, + ...params, + }) + } + + /** + * Execute TUI command + * + * Execute a TUI command (e.g. agent_cycle) + */ + public executeCommand( + parameters?: { + directory?: string + workspace?: string + command?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "command" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/execute-command", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Show TUI toast + * + * Show a toast notification in the TUI + */ + public showToast( + parameters?: { + directory?: string + workspace?: string + title?: string + message?: string + variant?: "info" | "success" | "warning" | "error" + duration?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "message" }, + { in: "body", key: "variant" }, + { in: "body", key: "duration" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/show-toast", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Publish TUI event + * + * Publish a TUI event + */ + public publish( + parameters?: { + directory?: string + workspace?: string + body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "body", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/publish", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Select session + * + * Navigate the TUI to display the specified session. + */ + public selectSession( + parameters?: { + directory?: string + workspace?: string + sessionID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/select-session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _control?: Control + get control(): Control { + return (this._control ??= new Control({ client: this.client })) + } +} + +export class Instance extends HeyApiClient { + /** + * Dispose instance + * + * Clean up and dispose the current OpenCode instance, releasing all resources. + */ + public dispose( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/instance/dispose", + ...options, + ...params, + }) + } +} + +export class Path extends HeyApiClient { + /** + * Get paths + * + * Retrieve the current working directory and related path information for the OpenCode instance. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/path", + ...options, + ...params, + }) + } +} + +export class Vcs extends HeyApiClient { + /** + * Get VCS info + * + * Retrieve version control system (VCS) information for the current project, such as git branch. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/vcs", + ...options, + ...params, + }) + } +} + +export class Command extends HeyApiClient { + /** + * List commands + * + * Get a list of all available commands in the OpenCode system. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/command", + ...options, + ...params, + }) + } +} + +export class App extends HeyApiClient { + /** + * Write log + * + * Write a log entry to the server logs with specified level and metadata. + */ + public log( + parameters?: { + directory?: string + workspace?: string + service?: string + level?: "debug" | "info" | "error" | "warn" + message?: string + extra?: { + [key: string]: unknown + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "service" }, + { in: "body", key: "level" }, + { in: "body", key: "message" }, + { in: "body", key: "extra" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/log", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List agents + * + * Get a list of all available AI agents in the OpenCode system. + */ + public agents( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/agent", + ...options, + ...params, + }) + } + + /** + * List skills + * + * Get a list of all available skills in the OpenCode system. + */ + public skills( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/skill", + ...options, + ...params, + }) + } +} + +export class Lsp extends HeyApiClient { + /** + * Get LSP status + * + * Get LSP server status + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/lsp", + ...options, + ...params, + }) + } +} + +export class Formatter extends HeyApiClient { + /** + * Get formatter status + * + * Get formatter status + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/formatter", + ...options, + ...params, + }) + } +} + +export class OpencodeClient extends HeyApiClient { + public static readonly __registry = new HeyApiRegistry() + + constructor(args?: { client?: Client; key?: string }) { + super(args) + OpencodeClient.__registry.set(this, args?.key) + } + + private _global?: Global + get global(): Global { + return (this._global ??= new Global({ client: this.client })) + } + + private _auth?: Auth + get auth(): Auth { + return (this._auth ??= new Auth({ client: this.client })) + } + + private _project?: Project + get project(): Project { + return (this._project ??= new Project({ client: this.client })) + } + + private _pty?: Pty + get pty(): Pty { + return (this._pty ??= new Pty({ client: this.client })) + } + + private _config?: Config2 + get config(): Config2 { + return (this._config ??= new Config2({ client: this.client })) + } + + private _tool?: Tool + get tool(): Tool { + return (this._tool ??= new Tool({ client: this.client })) + } + + private _experimental?: Experimental + get experimental(): Experimental { + return (this._experimental ??= new Experimental({ client: this.client })) + } + + private _worktree?: Worktree + get worktree(): Worktree { + return (this._worktree ??= new Worktree({ client: this.client })) + } + + private _session?: Session2 + get session(): Session2 { + return (this._session ??= new Session2({ client: this.client })) + } + + private _part?: Part + get part(): Part { + return (this._part ??= new Part({ client: this.client })) + } + + private _permission?: Permission + get permission(): Permission { + return (this._permission ??= new Permission({ client: this.client })) + } + + private _question?: Question + get question(): Question { + return (this._question ??= new Question({ client: this.client })) + } + + private _provider?: Provider + get provider(): Provider { + return (this._provider ??= new Provider({ client: this.client })) + } + + private _find?: Find + get find(): Find { + return (this._find ??= new Find({ client: this.client })) + } + + private _file?: File + get file(): File { + return (this._file ??= new File({ client: this.client })) + } + + private _event?: Event + get event(): Event { + return (this._event ??= new Event({ client: this.client })) + } + + private _mcp?: Mcp + get mcp(): Mcp { + return (this._mcp ??= new Mcp({ client: this.client })) + } + + private _tui?: Tui + get tui(): Tui { + return (this._tui ??= new Tui({ client: this.client })) + } + + private _instance?: Instance + get instance(): Instance { + return (this._instance ??= new Instance({ client: this.client })) + } + + private _path?: Path + get path(): Path { + return (this._path ??= new Path({ client: this.client })) + } + + private _vcs?: Vcs + get vcs(): Vcs { + return (this._vcs ??= new Vcs({ client: this.client })) + } + + private _command?: Command + get command(): Command { + return (this._command ??= new Command({ client: this.client })) + } + + private _app?: App + get app(): App { + return (this._app ??= new App({ client: this.client })) + } + + private _lsp?: Lsp + get lsp(): Lsp { + return (this._lsp ??= new Lsp({ client: this.client })) + } + + private _formatter?: Formatter + get formatter(): Formatter { + return (this._formatter ??= new Formatter({ client: this.client })) + } +} diff --git a/server/opencode/v2/gen/types.gen.ts b/server/opencode/v2/gen/types.gen.ts new file mode 100644 index 00000000..ec797f2b --- /dev/null +++ b/server/opencode/v2/gen/types.gen.ts @@ -0,0 +1,5000 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}) +} + +export type EventInstallationUpdated = { + type: "installation.updated" + properties: { + version: string + } +} + +export type EventInstallationUpdateAvailable = { + type: "installation.update-available" + properties: { + version: string + } +} + +export type Project = { + id: string + worktree: string + vcs?: "git" + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + time: { + created: number + updated: number + initialized?: number + } + sandboxes: Array +} + +export type EventProjectUpdated = { + type: "project.updated" + properties: Project +} + +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string + } +} + +export type EventServerInstanceDisposed = { + type: "server.instance.disposed" + properties: { + directory: string + } +} + +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + +export type PermissionRequest = { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventPermissionAsked = { + type: "permission.asked" + properties: PermissionRequest +} + +export type EventPermissionReplied = { + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" + } +} + +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" + properties: { + branch?: string + } +} + +export type QuestionOption = { + /** + * Display text (1-5 words, concise) + */ + label: string + /** + * Explanation of choice + */ + description: string +} + +export type QuestionInfo = { + /** + * Complete question + */ + question: string + /** + * Very short label (max 30 chars) + */ + header: string + /** + * Available choices + */ + options: Array + /** + * Allow selecting multiple choices + */ + multiple?: boolean + /** + * Allow typing a custom answer (default: true) + */ + custom?: boolean +} + +export type QuestionRequest = { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventQuestionAsked = { + type: "question.asked" + properties: QuestionRequest +} + +export type QuestionAnswer = Array + +export type EventQuestionReplied = { + type: "question.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } +} + +export type EventQuestionRejected = { + type: "question.rejected" + properties: { + sessionID: string + requestID: string + } +} + +export type EventServerConnected = { + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + +export type EventLspClientDiagnostics = { + type: "lsp.client.diagnostics" + properties: { + serverID: string + path: string + } +} + +export type EventLspUpdated = { + type: "lsp.updated" + properties: { + [key: string]: unknown + } +} + +export type OutputFormatText = { + type: "text" +} + +export type JsonSchema = { + [key: string]: unknown +} + +export type OutputFormatJsonSchema = { + type: "json_schema" + schema: JsonSchema + retryCount?: number +} + +export type OutputFormat = OutputFormatText | OutputFormatJsonSchema + +export type FileDiff = { + file: string + before: string + after: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { + created: number + } + format?: OutputFormat + summary?: { + title?: string + body?: string + diffs: Array + } + agent: string + model: { + providerID: string + modelID: string + } + system?: string + tools?: { + [key: string]: boolean + } + variant?: string +} + +export type ProviderAuthError = { + name: "ProviderAuthError" + data: { + providerID: string + message: string + } +} + +export type UnknownError = { + name: "UnknownError" + data: { + message: string + } +} + +export type MessageOutputLengthError = { + name: "MessageOutputLengthError" + data: { + [key: string]: unknown + } +} + +export type MessageAbortedError = { + name: "MessageAbortedError" + data: { + message: string + } +} + +export type StructuredOutputError = { + name: "StructuredOutputError" + data: { + message: string + retries: number + } +} + +export type ContextOverflowError = { + name: "ContextOverflowError" + data: { + message: string + responseBody?: string + } +} + +export type ApiError = { + name: "APIError" + data: { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string + } + responseBody?: string + metadata?: { + [key: string]: string + } + } +} + +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { + created: number + completed?: number + } + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ApiError + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string + } + summary?: boolean + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + structured?: unknown + variant?: string + finish?: string +} + +export type Message = UserMessage | AssistantMessage + +export type EventMessageUpdated = { + type: "message.updated" + properties: { + info: Message + } +} + +export type EventMessageRemoved = { + type: "message.removed" + properties: { + sessionID: string + messageID: string + } +} + +export type TextPart = { + id: string + sessionID: string + messageID: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } +} + +export type SubtaskPart = { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { + providerID: string + modelID: string + } + command?: string +} + +export type ReasoningPart = { + id: string + sessionID: string + messageID: string + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } +} + +export type FilePartSourceText = { + value: string + start: number + end: number +} + +export type FileSource = { + text: FilePartSourceText + type: "file" + path: string +} + +export type Range = { + start: { + line: number + character: number + } + end: { + line: number + character: number + } +} + +export type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: Range + name: string + kind: number +} + +export type ResourceSource = { + text: FilePartSourceText + type: "resource" + clientName: string + uri: string +} + +export type FilePartSource = FileSource | SymbolSource | ResourceSource + +export type FilePart = { + id: string + sessionID: string + messageID: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type ToolStatePending = { + status: "pending" + input: { + [key: string]: unknown + } + raw: string +} + +export type ToolStateRunning = { + status: "running" + input: { + [key: string]: unknown + } + title?: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + } +} + +export type ToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown + } + output: string + title: string + metadata: { + [key: string]: unknown + } + time: { + start: number + end: number + compacted?: number + } + attachments?: Array +} + +export type ToolStateError = { + status: "error" + input: { + [key: string]: unknown + } + error: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end: number + } +} + +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + +export type ToolPart = { + id: string + sessionID: string + messageID: string + type: "tool" + callID: string + tool: string + state: ToolState + metadata?: { + [key: string]: unknown + } +} + +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" + snapshot?: string +} + +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + reason: string + snapshot?: string + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } +} + +export type SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string +} + +export type PatchPart = { + id: string + sessionID: string + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type RetryPart = { + id: string + sessionID: string + messageID: string + type: "retry" + attempt: number + error: ApiError + time: { + created: number + } +} + +export type CompactionPart = { + id: string + sessionID: string + messageID: string + type: "compaction" + auto: boolean + overflow?: boolean +} + +export type Part = + | TextPart + | SubtaskPart + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | RetryPart + | CompactionPart + +export type EventMessagePartUpdated = { + type: "message.part.updated" + properties: { + part: Part + } +} + +export type EventMessagePartDelta = { + type: "message.part.delta" + properties: { + sessionID: string + messageID: string + partID: string + field: string + delta: string + } +} + +export type EventMessagePartRemoved = { + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string + } +} + +export type SessionStatus = + | { + type: "idle" + } + | { + type: "retry" + attempt: number + message: string + next: number + } + | { + type: "busy" + } + +export type EventSessionStatus = { + type: "session.status" + properties: { + sessionID: string + status: SessionStatus + } +} + +export type EventSessionIdle = { + type: "session.idle" + properties: { + sessionID: string + } +} + +export type EventSessionCompacted = { + type: "session.compacted" + properties: { + sessionID: string + } +} + +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string +} + +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number + } +} + +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type EventMcpToolsChanged = { + type: "mcp.tools.changed" + properties: { + server: string + } +} + +export type EventMcpBrowserOpenFailed = { + type: "mcp.browser.open.failed" + properties: { + mcpName: string + url: string + } +} + +export type EventCommandExecuted = { + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } +} + +export type PermissionAction = "allow" | "deny" | "ask" + +export type PermissionRule = { + permission: string + pattern: string + action: PermissionAction +} + +export type PermissionRuleset = Array + +export type Session = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + share?: { + url: string + } + title: string + version: string + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } +} + +export type EventSessionCreated = { + type: "session.created" + properties: { + info: Session + } +} + +export type EventSessionUpdated = { + type: "session.updated" + properties: { + info: Session + } +} + +export type EventSessionDeleted = { + type: "session.deleted" + properties: { + info: Session + } +} + +export type EventSessionDiff = { + type: "session.diff" + properties: { + sessionID: string + diff: Array + } +} + +export type EventSessionError = { + type: "session.error" + properties: { + sessionID?: string + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ApiError + } +} + +export type EventWorkspaceReady = { + type: "workspace.ready" + properties: { + name: string + } +} + +export type EventWorkspaceFailed = { + type: "workspace.failed" + properties: { + message: string + } +} + +export type Pty = { + id: string + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number +} + +export type EventPtyCreated = { + type: "pty.created" + properties: { + info: Pty + } +} + +export type EventPtyUpdated = { + type: "pty.updated" + properties: { + info: Pty + } +} + +export type EventPtyExited = { + type: "pty.exited" + properties: { + id: string + exitCode: number + } +} + +export type EventPtyDeleted = { + type: "pty.deleted" + properties: { + id: string + } +} + +export type EventWorktreeReady = { + type: "worktree.ready" + properties: { + name: string + branch: string + } +} + +export type EventWorktreeFailed = { + type: "worktree.failed" + properties: { + message: string + } +} + +export type Event = + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventProjectUpdated + | EventFileEdited + | EventServerInstanceDisposed + | EventFileWatcherUpdated + | EventPermissionAsked + | EventPermissionReplied + | EventVcsBranchUpdated + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventServerConnected + | EventGlobalDisposed + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartDelta + | EventMessagePartRemoved + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventTodoUpdated + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionDiff + | EventSessionError + | EventWorkspaceReady + | EventWorkspaceFailed + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventWorktreeReady + | EventWorktreeFailed + +export type GlobalEvent = { + directory: string + payload: Event +} + +/** + * Log level + */ +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" + +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { + /** + * Port to listen on + */ + port?: number + /** + * Hostname to listen on + */ + hostname?: string + /** + * Enable mDNS service discovery + */ + mdns?: boolean + /** + * Custom domain name for mDNS service (default: opencode.local) + */ + mdnsDomain?: string + /** + * Additional domains to allow for CORS + */ + cors?: Array +} + +export type PermissionActionConfig = "ask" | "allow" | "deny" + +export type PermissionObjectConfig = { + [key: string]: PermissionActionConfig +} + +export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig + +export type PermissionConfig = + | { + __originalKeys?: Array + read?: PermissionRuleConfig + edit?: PermissionRuleConfig + glob?: PermissionRuleConfig + grep?: PermissionRuleConfig + list?: PermissionRuleConfig + bash?: PermissionRuleConfig + task?: PermissionRuleConfig + external_directory?: PermissionRuleConfig + todowrite?: PermissionActionConfig + todoread?: PermissionActionConfig + question?: PermissionActionConfig + webfetch?: PermissionActionConfig + websearch?: PermissionActionConfig + codesearch?: PermissionActionConfig + lsp?: PermissionRuleConfig + doom_loop?: PermissionActionConfig + skill?: PermissionRuleConfig + [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined + } + | PermissionActionConfig + +export type AgentConfig = { + model?: string + /** + * Default model variant for this agent (applies only when using the agent's configured model). + */ + variant?: string + temperature?: number + top_p?: number + prompt?: string + /** + * @deprecated Use 'permission' field instead + */ + tools?: { + [key: string]: boolean + } + disable?: boolean + /** + * Description of when to use the agent + */ + description?: string + mode?: "subagent" | "primary" | "all" + /** + * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) + */ + hidden?: boolean + options?: { + [key: string]: unknown + } + /** + * Hex color code (e.g., #FF5733) or theme color (e.g., primary) + */ + color?: string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" + /** + * Maximum number of agentic iterations before forcing text-only response + */ + steps?: number + /** + * @deprecated Use 'steps' field instead. + */ + maxSteps?: number + permission?: PermissionConfig + [key: string]: + | unknown + | string + | number + | { + [key: string]: boolean + } + | boolean + | "subagent" + | "primary" + | "all" + | { + [key: string]: unknown + } + | string + | "primary" + | "secondary" + | "accent" + | "success" + | "warning" + | "error" + | "info" + | number + | PermissionConfig + | undefined +} + +export type ProviderConfig = { + api?: string + name?: string + env?: Array + id?: string + npm?: string + models?: { + [key: string]: { + id?: string + name?: string + family?: string + release_date?: string + attachment?: boolean + reasoning?: boolean + temperature?: boolean + tool_call?: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit?: { + context: number + input?: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + options?: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + provider?: { + npm?: string + api?: string + } + /** + * Variant-specific configuration + */ + variants?: { + [key: string]: { + /** + * Disable this variant for the model + */ + disabled?: boolean + [key: string]: unknown | boolean | undefined + } + } + } + } + whitelist?: Array + blacklist?: Array + options?: { + apiKey?: string + baseURL?: string + /** + * GitHub Enterprise URL for copilot authentication + */ + enterpriseUrl?: string + /** + * Enable promptCacheKey for this provider (default false) + */ + setCacheKey?: boolean + /** + * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. + */ + timeout?: number | false + /** + * Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted. + */ + chunkTimeout?: number + [key: string]: unknown | string | boolean | number | false | number | undefined + } +} + +export type McpLocalConfig = { + /** + * Type of MCP server connection + */ + type: "local" + /** + * Command and arguments to run the MCP server + */ + command: Array + /** + * Environment variables to set when running the MCP server + */ + environment?: { + [key: string]: string + } + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number +} + +export type McpOAuthConfig = { + /** + * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. + */ + clientId?: string + /** + * OAuth client secret (if required by the authorization server) + */ + clientSecret?: string + /** + * OAuth scopes to request during authorization + */ + scope?: string +} + +export type McpRemoteConfig = { + /** + * Type of MCP server connection + */ + type: "remote" + /** + * URL of the remote MCP server + */ + url: string + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Headers to send with the request + */ + headers?: { + [key: string]: string + } + /** + * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. + */ + oauth?: McpOAuthConfig | false + /** + * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number +} + +/** + * @deprecated Always uses stretch layout. + */ +export type LayoutConfig = "auto" | "stretch" + +export type Config = { + /** + * JSON schema reference for configuration validation + */ + $schema?: string + logLevel?: LogLevel + server?: ServerConfig + /** + * Command configuration, see https://opencode.ai/docs/commands + */ + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + subtask?: boolean + } + } + /** + * Additional skill folder paths + */ + skills?: { + /** + * Additional paths to skill folders + */ + paths?: Array + /** + * URLs to fetch skills from (e.g., https://example.com/.well-known/skills/) + */ + urls?: Array + } + watcher?: { + ignore?: Array + } + plugin?: Array + /** + * Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true. + */ + snapshot?: boolean + /** + * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing + */ + share?: "manual" | "auto" | "disabled" + /** + * @deprecated Use 'share' field instead. Share newly created sessions automatically + */ + autoshare?: boolean + /** + * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications + */ + autoupdate?: boolean | "notify" + /** + * Disable providers that are loaded automatically + */ + disabled_providers?: Array + /** + * When set, ONLY these providers will be enabled. All other providers will be ignored + */ + enabled_providers?: Array + /** + * Model to use in the format of provider/model, eg anthropic/claude-2 + */ + model?: string + /** + * Small model to use for tasks like title generation in the format of provider/model + */ + small_model?: string + /** + * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. + */ + default_agent?: string + /** + * Custom username to display in conversations instead of system username + */ + username?: string + /** + * @deprecated Use `agent` field instead. + */ + mode?: { + build?: AgentConfig + plan?: AgentConfig + [key: string]: AgentConfig | undefined + } + /** + * Agent configuration, see https://opencode.ai/docs/agents + */ + agent?: { + plan?: AgentConfig + build?: AgentConfig + general?: AgentConfig + explore?: AgentConfig + title?: AgentConfig + summary?: AgentConfig + compaction?: AgentConfig + [key: string]: AgentConfig | undefined + } + /** + * Custom provider configurations and model overrides + */ + provider?: { + [key: string]: ProviderConfig + } + /** + * MCP (Model Context Protocol) server configurations + */ + mcp?: { + [key: string]: + | McpLocalConfig + | McpRemoteConfig + | { + enabled: boolean + } + } + formatter?: + | false + | { + [key: string]: { + disabled?: boolean + command?: Array + environment?: { + [key: string]: string + } + extensions?: Array + } + } + lsp?: + | false + | { + [key: string]: + | { + disabled: true + } + | { + command: Array + extensions?: Array + disabled?: boolean + env?: { + [key: string]: string + } + initialization?: { + [key: string]: unknown + } + } + } + /** + * Additional instruction files or patterns to include + */ + instructions?: Array + layout?: LayoutConfig + permission?: PermissionConfig + tools?: { + [key: string]: boolean + } + enterprise?: { + /** + * Enterprise URL + */ + url?: string + } + compaction?: { + /** + * Enable automatic compaction when context is full (default: true) + */ + auto?: boolean + /** + * Enable pruning of old tool outputs (default: true) + */ + prune?: boolean + /** + * Token buffer for compaction. Leaves enough window to avoid overflow during compaction. + */ + reserved?: number + } + experimental?: { + disable_paste_summary?: boolean + /** + * Enable the batch tool + */ + batch_tool?: boolean + /** + * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) + */ + openTelemetry?: boolean + /** + * Tools that should only be available to primary agents. + */ + primary_tools?: Array + /** + * Continue the agent loop when a tool call is denied + */ + continue_loop_on_deny?: boolean + /** + * Timeout in milliseconds for model context protocol (MCP) requests + */ + mcp_timeout?: number + } +} + +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + +export type OAuth = { + type: "oauth" + refresh: string + access: string + expires: number + accountId?: string + enterpriseUrl?: string +} + +export type ApiAuth = { + type: "api" + key: string +} + +export type WellKnownAuth = { + type: "wellknown" + key: string + token: string +} + +export type Auth = OAuth | ApiAuth | WellKnownAuth + +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string + } +} + +export type Model = { + id: string + providerID: string + api: { + id: string + url: string + npm: string + } + name: string + family?: string + capabilities: { + temperature: boolean + reasoning: boolean + attachment: boolean + toolcall: boolean + input: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + output: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + interleaved: + | boolean + | { + field: "reasoning_content" | "reasoning_details" + } + } + cost: { + input: number + output: number + cache: { + read: number + write: number + } + experimentalOver200K?: { + input: number + output: number + cache: { + read: number + write: number + } + } + } + limit: { + context: number + input?: number + output: number + } + status: "alpha" | "beta" | "deprecated" | "active" + options: { + [key: string]: unknown + } + headers: { + [key: string]: string + } + release_date: string + variants?: { + [key: string]: { + [key: string]: unknown + } + } +} + +export type Provider = { + id: string + name: string + source: "env" | "config" | "custom" | "api" + env: Array + key?: string + options: { + [key: string]: unknown + } + models: { + [key: string]: Model + } +} + +export type ToolIds = Array + +export type ToolListItem = { + id: string + description: string + parameters: unknown +} + +export type ToolList = Array + +export type Workspace = { + id: string + type: string + branch: string | null + name: string | null + directory: string | null + extra: unknown | null + projectID: string +} + +export type Worktree = { + name: string + branch: string + directory: string +} + +export type WorktreeCreateInput = { + name?: string + /** + * Additional startup script to run after the project's start command + */ + startCommand?: string +} + +export type WorktreeRemoveInput = { + directory: string +} + +export type WorktreeResetInput = { + directory: string +} + +export type ProjectSummary = { + id: string + name?: string + worktree: string +} + +export type GlobalSession = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + share?: { + url: string + } + title: string + version: string + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } + project: ProjectSummary | null +} + +export type McpResource = { + name: string + uri: string + description?: string + mimeType?: string + client: string +} + +export type TextPartInput = { + id?: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } +} + +export type FilePartInput = { + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type AgentPartInput = { + id?: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type SubtaskPartInput = { + id?: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { + providerID: string + modelID: string + } + command?: string +} + +export type ProviderAuthMethod = { + type: "oauth" | "api" + label: string + prompts?: Array< + | { + type: "text" + key: string + message: string + placeholder?: string + when?: { + key: string + op: "eq" | "neq" + value: string + } + } + | { + type: "select" + key: string + message: string + options: Array<{ + label: string + value: string + hint?: string + }> + when?: { + key: string + op: "eq" | "neq" + value: string + } + } + > +} + +export type ProviderAuthAuthorization = { + url: string + method: "auto" | "code" + instructions: string +} + +export type Symbol = { + name: string + kind: number + location: { + uri: string + range: Range + } +} + +export type FileNode = { + name: string + path: string + absolute: string + type: "file" | "directory" + ignored: boolean +} + +export type FileContent = { + type: "text" | "binary" + content: string + diff?: string + patch?: { + oldFileName: string + newFileName: string + oldHeader?: string + newHeader?: string + hunks: Array<{ + oldStart: number + oldLines: number + newStart: number + newLines: number + lines: Array + }> + index?: string + } + encoding?: "base64" + mimeType?: string +} + +export type File = { + path: string + added: number + removed: number + status: "added" | "deleted" | "modified" +} + +export type McpStatusConnected = { + status: "connected" +} + +export type McpStatusDisabled = { + status: "disabled" +} + +export type McpStatusFailed = { + status: "failed" + error: string +} + +export type McpStatusNeedsAuth = { + status: "needs_auth" +} + +export type McpStatusNeedsClientRegistration = { + status: "needs_client_registration" + error: string +} + +export type McpStatus = + | McpStatusConnected + | McpStatusDisabled + | McpStatusFailed + | McpStatusNeedsAuth + | McpStatusNeedsClientRegistration + +export type Path = { + home: string + state: string + config: string + worktree: string + directory: string +} + +export type VcsInfo = { + branch: string +} + +export type Command = { + name: string + description?: string + agent?: string + model?: string + source?: "command" | "mcp" | "skill" + template: string + subtask?: boolean + hints: Array +} + +export type Agent = { + name: string + description?: string + mode: "subagent" | "primary" | "all" + native?: boolean + hidden?: boolean + topP?: number + temperature?: number + color?: string + permission: PermissionRuleset + model?: { + modelID: string + providerID: string + } + variant?: string + prompt?: string + options: { + [key: string]: unknown + } + steps?: number +} + +export type LspStatus = { + id: string + name: string + root: string + status: "connected" | "error" +} + +export type FormatterStatus = { + name: string + extensions: Array + enabled: boolean +} + +export type GlobalHealthData = { + body?: never + path?: never + query?: never + url: "/global/health" +} + +export type GlobalHealthResponses = { + /** + * Health information + */ + 200: { + healthy: true + version: string + } +} + +export type GlobalHealthResponse = GlobalHealthResponses[keyof GlobalHealthResponses] + +export type GlobalEventData = { + body?: never + path?: never + query?: never + url: "/global/event" +} + +export type GlobalEventResponses = { + /** + * Event stream + */ + 200: GlobalEvent +} + +export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] + +export type GlobalConfigGetData = { + body?: never + path?: never + query?: never + url: "/global/config" +} + +export type GlobalConfigGetResponses = { + /** + * Get global config info + */ + 200: Config +} + +export type GlobalConfigGetResponse = GlobalConfigGetResponses[keyof GlobalConfigGetResponses] + +export type GlobalConfigUpdateData = { + body?: Config + path?: never + query?: never + url: "/global/config" +} + +export type GlobalConfigUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalConfigUpdateError = GlobalConfigUpdateErrors[keyof GlobalConfigUpdateErrors] + +export type GlobalConfigUpdateResponses = { + /** + * Successfully updated global config + */ + 200: Config +} + +export type GlobalConfigUpdateResponse = GlobalConfigUpdateResponses[keyof GlobalConfigUpdateResponses] + +export type GlobalDisposeData = { + body?: never + path?: never + query?: never + url: "/global/dispose" +} + +export type GlobalDisposeResponses = { + /** + * Global disposed + */ + 200: boolean +} + +export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses] + +export type AuthRemoveData = { + body?: never + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] + +export type AuthRemoveResponses = { + /** + * Successfully removed authentication credentials + */ + 200: boolean +} + +export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] + +export type AuthSetData = { + body?: Auth + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthSetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] + +export type AuthSetResponses = { + /** + * Successfully set authentication credentials + */ + 200: boolean +} + +export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] + +export type ProjectListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project" +} + +export type ProjectListResponses = { + /** + * List of projects + */ + 200: Array +} + +export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] + +export type ProjectCurrentData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/current" +} + +export type ProjectCurrentResponses = { + /** + * Current project information + */ + 200: Project +} + +export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] + +export type ProjectInitGitData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/git/init" +} + +export type ProjectInitGitResponses = { + /** + * Project information after git initialization + */ + 200: Project +} + +export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] + +export type ProjectUpdateData = { + body?: { + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + } + path: { + projectID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/project/{projectID}" +} + +export type ProjectUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] + +export type ProjectUpdateResponses = { + /** + * Updated project information + */ + 200: Project +} + +export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] + +export type PtyListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/pty" +} + +export type PtyListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type PtyListResponse = PtyListResponses[keyof PtyListResponses] + +export type PtyCreateData = { + body?: { + command?: string + args?: Array + cwd?: string + title?: string + env?: { + [key: string]: string + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/pty" +} + +export type PtyCreateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PtyCreateError = PtyCreateErrors[keyof PtyCreateErrors] + +export type PtyCreateResponses = { + /** + * Created session + */ + 200: Pty +} + +export type PtyCreateResponse = PtyCreateResponses[keyof PtyCreateResponses] + +export type PtyRemoveData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}" +} + +export type PtyRemoveErrors = { + /** + * Not found + */ + 404: NotFoundError +} + +export type PtyRemoveError = PtyRemoveErrors[keyof PtyRemoveErrors] + +export type PtyRemoveResponses = { + /** + * Session removed + */ + 200: boolean +} + +export type PtyRemoveResponse = PtyRemoveResponses[keyof PtyRemoveResponses] + +export type PtyGetData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}" +} + +export type PtyGetErrors = { + /** + * Not found + */ + 404: NotFoundError +} + +export type PtyGetError = PtyGetErrors[keyof PtyGetErrors] + +export type PtyGetResponses = { + /** + * Session info + */ + 200: Pty +} + +export type PtyGetResponse = PtyGetResponses[keyof PtyGetResponses] + +export type PtyUpdateData = { + body?: { + title?: string + size?: { + rows: number + cols: number + } + } + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}" +} + +export type PtyUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PtyUpdateError = PtyUpdateErrors[keyof PtyUpdateErrors] + +export type PtyUpdateResponses = { + /** + * Updated session + */ + 200: Pty +} + +export type PtyUpdateResponse = PtyUpdateResponses[keyof PtyUpdateResponses] + +export type PtyConnectData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}/connect" +} + +export type PtyConnectErrors = { + /** + * Not found + */ + 404: NotFoundError +} + +export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors] + +export type PtyConnectResponses = { + /** + * Connected session + */ + 200: boolean +} + +export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] + +export type ConfigGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigGetResponses = { + /** + * Get config info + */ + 200: Config +} + +export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] + +export type ConfigUpdateData = { + body?: Config + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] + +export type ConfigUpdateResponses = { + /** + * Successfully updated config + */ + 200: Config +} + +export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] + +export type ConfigProvidersData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config/providers" +} + +export type ConfigProvidersResponses = { + /** + * List of providers + */ + 200: { + providers: Array + default: { + [key: string]: string + } + } +} + +export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] + +export type ToolIdsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/tool/ids" +} + +export type ToolIdsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors] + +export type ToolIdsResponses = { + /** + * Tool IDs + */ + 200: ToolIds +} + +export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses] + +export type ToolListData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + provider: string + model: string + } + url: "/experimental/tool" +} + +export type ToolListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ToolListError = ToolListErrors[keyof ToolListErrors] + +export type ToolListResponses = { + /** + * Tools + */ + 200: ToolList +} + +export type ToolListResponse = ToolListResponses[keyof ToolListResponses] + +export type ExperimentalWorkspaceListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace" +} + +export type ExperimentalWorkspaceListResponses = { + /** + * Workspaces + */ + 200: Array +} + +export type ExperimentalWorkspaceListResponse = + ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] + +export type ExperimentalWorkspaceCreateData = { + body?: { + id?: string + type: string + branch: string | null + extra: unknown | null + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace" +} + +export type ExperimentalWorkspaceCreateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceCreateError = + ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] + +export type ExperimentalWorkspaceCreateResponses = { + /** + * Workspace created + */ + 200: Workspace +} + +export type ExperimentalWorkspaceCreateResponse = + ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] + +export type ExperimentalWorkspaceRemoveData = { + body?: never + path: { + id: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/{id}" +} + +export type ExperimentalWorkspaceRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceRemoveError = + ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] + +export type ExperimentalWorkspaceRemoveResponses = { + /** + * Workspace removed + */ + 200: Workspace +} + +export type ExperimentalWorkspaceRemoveResponse = + ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] + +export type WorktreeRemoveData = { + body?: WorktreeRemoveInput + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree" +} + +export type WorktreeRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors] + +export type WorktreeRemoveResponses = { + /** + * Worktree removed + */ + 200: boolean +} + +export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses] + +export type WorktreeListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree" +} + +export type WorktreeListResponses = { + /** + * List of worktree directories + */ + 200: Array +} + +export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses] + +export type WorktreeCreateData = { + body?: WorktreeCreateInput + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree" +} + +export type WorktreeCreateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors] + +export type WorktreeCreateResponses = { + /** + * Worktree created + */ + 200: Worktree +} + +export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses] + +export type WorktreeResetData = { + body?: WorktreeResetInput + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree/reset" +} + +export type WorktreeResetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type WorktreeResetError = WorktreeResetErrors[keyof WorktreeResetErrors] + +export type WorktreeResetResponses = { + /** + * Worktree reset + */ + 200: boolean +} + +export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses] + +export type ExperimentalSessionListData = { + body?: never + path?: never + query?: { + /** + * Filter sessions by project directory + */ + directory?: string + workspace?: string + /** + * Only return root sessions (no parentID) + */ + roots?: boolean + /** + * Filter sessions updated on or after this timestamp (milliseconds since epoch) + */ + start?: number + /** + * Return sessions updated before this timestamp (milliseconds since epoch) + */ + cursor?: number + /** + * Filter sessions by title (case-insensitive) + */ + search?: string + /** + * Maximum number of sessions to return + */ + limit?: number + /** + * Include archived sessions (default false) + */ + archived?: boolean + } + url: "/experimental/session" +} + +export type ExperimentalSessionListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type ExperimentalSessionListResponse = ExperimentalSessionListResponses[keyof ExperimentalSessionListResponses] + +export type ExperimentalResourceListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/resource" +} + +export type ExperimentalResourceListResponses = { + /** + * MCP resources + */ + 200: { + [key: string]: McpResource + } +} + +export type ExperimentalResourceListResponse = + ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses] + +export type SessionListData = { + body?: never + path?: never + query?: { + /** + * Filter sessions by project directory + */ + directory?: string + workspace?: string + /** + * Only return root sessions (no parentID) + */ + roots?: boolean + /** + * Filter sessions updated on or after this timestamp (milliseconds since epoch) + */ + start?: number + /** + * Filter sessions by title (case-insensitive) + */ + search?: string + /** + * Maximum number of sessions to return + */ + limit?: number + } + url: "/session" +} + +export type SessionListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type SessionListResponse = SessionListResponses[keyof SessionListResponses] + +export type SessionCreateData = { + body?: { + parentID?: string + title?: string + permission?: PermissionRuleset + workspaceID?: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/session" +} + +export type SessionCreateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors] + +export type SessionCreateResponses = { + /** + * Successfully created session + */ + 200: Session +} + +export type SessionCreateResponse = SessionCreateResponses[keyof SessionCreateResponses] + +export type SessionStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/session/status" +} + +export type SessionStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SessionStatusError = SessionStatusErrors[keyof SessionStatusErrors] + +export type SessionStatusResponses = { + /** + * Get session status + */ + 200: { + [key: string]: SessionStatus + } +} + +export type SessionStatusResponse = SessionStatusResponses[keyof SessionStatusResponses] + +export type SessionDeleteData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}" +} + +export type SessionDeleteErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionDeleteError = SessionDeleteErrors[keyof SessionDeleteErrors] + +export type SessionDeleteResponses = { + /** + * Successfully deleted session + */ + 200: boolean +} + +export type SessionDeleteResponse = SessionDeleteResponses[keyof SessionDeleteResponses] + +export type SessionGetData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}" +} + +export type SessionGetErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionGetError = SessionGetErrors[keyof SessionGetErrors] + +export type SessionGetResponses = { + /** + * Get session + */ + 200: Session +} + +export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] + +export type SessionUpdateData = { + body?: { + title?: string + time?: { + archived?: number + } + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}" +} + +export type SessionUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionUpdateError = SessionUpdateErrors[keyof SessionUpdateErrors] + +export type SessionUpdateResponses = { + /** + * Successfully updated session + */ + 200: Session +} + +export type SessionUpdateResponse = SessionUpdateResponses[keyof SessionUpdateResponses] + +export type SessionChildrenData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/children" +} + +export type SessionChildrenErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionChildrenError = SessionChildrenErrors[keyof SessionChildrenErrors] + +export type SessionChildrenResponses = { + /** + * List of children + */ + 200: Array +} + +export type SessionChildrenResponse = SessionChildrenResponses[keyof SessionChildrenResponses] + +export type SessionTodoData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/todo" +} + +export type SessionTodoErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionTodoError = SessionTodoErrors[keyof SessionTodoErrors] + +export type SessionTodoResponses = { + /** + * Todo list + */ + 200: Array +} + +export type SessionTodoResponse = SessionTodoResponses[keyof SessionTodoResponses] + +export type SessionInitData = { + body?: { + modelID: string + providerID: string + messageID: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/init" +} + +export type SessionInitErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionInitError = SessionInitErrors[keyof SessionInitErrors] + +export type SessionInitResponses = { + /** + * 200 + */ + 200: boolean +} + +export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses] + +export type SessionForkData = { + body?: { + messageID?: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/fork" +} + +export type SessionForkResponses = { + /** + * 200 + */ + 200: Session +} + +export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses] + +export type SessionAbortData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/abort" +} + +export type SessionAbortErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors] + +export type SessionAbortResponses = { + /** + * Aborted session + */ + 200: boolean +} + +export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses] + +export type SessionUnshareData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/share" +} + +export type SessionUnshareErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors] + +export type SessionUnshareResponses = { + /** + * Successfully unshared session + */ + 200: Session +} + +export type SessionUnshareResponse = SessionUnshareResponses[keyof SessionUnshareResponses] + +export type SessionShareData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/share" +} + +export type SessionShareErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionShareError = SessionShareErrors[keyof SessionShareErrors] + +export type SessionShareResponses = { + /** + * Successfully shared session + */ + 200: Session +} + +export type SessionShareResponse = SessionShareResponses[keyof SessionShareResponses] + +export type SessionDiffData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + messageID?: string + } + url: "/session/{sessionID}/diff" +} + +export type SessionDiffResponses = { + /** + * Successfully retrieved diff + */ + 200: Array +} + +export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] + +export type SessionSummarizeData = { + body?: { + providerID: string + modelID: string + auto?: boolean + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/summarize" +} + +export type SessionSummarizeErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors] + +export type SessionSummarizeResponses = { + /** + * Summarized session + */ + 200: boolean +} + +export type SessionSummarizeResponse = SessionSummarizeResponses[keyof SessionSummarizeResponses] + +export type SessionMessagesData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + /** + * Maximum number of messages to return + */ + limit?: number + before?: string + } + url: "/session/{sessionID}/message" +} + +export type SessionMessagesErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors] + +export type SessionMessagesResponses = { + /** + * List of messages + */ + 200: Array<{ + info: Message + parts: Array + }> +} + +export type SessionMessagesResponse = SessionMessagesResponses[keyof SessionMessagesResponses] + +export type SessionPromptData = { + body?: { + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + /** + * @deprecated tools and permissions have been merged, you can set permissions on the session itself now + */ + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts: Array + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message" +} + +export type SessionPromptErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors] + +export type SessionPromptResponses = { + /** + * Created message + */ + 200: { + info: AssistantMessage + parts: Array + } +} + +export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses] + +export type SessionDeleteMessageData = { + body?: never + path: { + sessionID: string + messageID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}" +} + +export type SessionDeleteMessageErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors] + +export type SessionDeleteMessageResponses = { + /** + * Successfully deleted message + */ + 200: boolean +} + +export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses] + +export type SessionMessageData = { + body?: never + path: { + sessionID: string + messageID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}" +} + +export type SessionMessageErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors] + +export type SessionMessageResponses = { + /** + * Message + */ + 200: { + info: Message + parts: Array + } +} + +export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses] + +export type PartDeleteData = { + body?: never + path: { + sessionID: string + messageID: string + partID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}/part/{partID}" +} + +export type PartDeleteErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type PartDeleteError = PartDeleteErrors[keyof PartDeleteErrors] + +export type PartDeleteResponses = { + /** + * Successfully deleted part + */ + 200: boolean +} + +export type PartDeleteResponse = PartDeleteResponses[keyof PartDeleteResponses] + +export type PartUpdateData = { + body?: Part + path: { + sessionID: string + messageID: string + partID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}/part/{partID}" +} + +export type PartUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type PartUpdateError = PartUpdateErrors[keyof PartUpdateErrors] + +export type PartUpdateResponses = { + /** + * Successfully updated part + */ + 200: Part +} + +export type PartUpdateResponse = PartUpdateResponses[keyof PartUpdateResponses] + +export type SessionPromptAsyncData = { + body?: { + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + /** + * @deprecated tools and permissions have been merged, you can set permissions on the session itself now + */ + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts: Array + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/prompt_async" +} + +export type SessionPromptAsyncErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionPromptAsyncError = SessionPromptAsyncErrors[keyof SessionPromptAsyncErrors] + +export type SessionPromptAsyncResponses = { + /** + * Prompt accepted + */ + 204: void +} + +export type SessionPromptAsyncResponse = SessionPromptAsyncResponses[keyof SessionPromptAsyncResponses] + +export type SessionCommandData = { + body?: { + messageID?: string + agent?: string + model?: string + arguments: string + command: string + variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + }> + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/command" +} + +export type SessionCommandErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionCommandError = SessionCommandErrors[keyof SessionCommandErrors] + +export type SessionCommandResponses = { + /** + * Created message + */ + 200: { + info: AssistantMessage + parts: Array + } +} + +export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses] + +export type SessionShellData = { + body?: { + agent: string + model?: { + providerID: string + modelID: string + } + command: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/shell" +} + +export type SessionShellErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionShellError = SessionShellErrors[keyof SessionShellErrors] + +export type SessionShellResponses = { + /** + * Created message + */ + 200: AssistantMessage +} + +export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses] + +export type SessionRevertData = { + body?: { + messageID: string + partID?: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/revert" +} + +export type SessionRevertErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionRevertError = SessionRevertErrors[keyof SessionRevertErrors] + +export type SessionRevertResponses = { + /** + * Updated session + */ + 200: Session +} + +export type SessionRevertResponse = SessionRevertResponses[keyof SessionRevertResponses] + +export type SessionUnrevertData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/unrevert" +} + +export type SessionUnrevertErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionUnrevertError = SessionUnrevertErrors[keyof SessionUnrevertErrors] + +export type SessionUnrevertResponses = { + /** + * Updated session + */ + 200: Session +} + +export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses] + +export type PermissionRespondData = { + body?: { + response: "once" | "always" | "reject" + } + path: { + sessionID: string + permissionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/permissions/{permissionID}" +} + +export type PermissionRespondErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type PermissionRespondError = PermissionRespondErrors[keyof PermissionRespondErrors] + +export type PermissionRespondResponses = { + /** + * Permission processed successfully + */ + 200: boolean +} + +export type PermissionRespondResponse = PermissionRespondResponses[keyof PermissionRespondResponses] + +export type PermissionReplyData = { + body?: { + reply: "once" | "always" | "reject" + message?: string + } + path: { + requestID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/permission/{requestID}/reply" +} + +export type PermissionReplyErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type PermissionReplyError = PermissionReplyErrors[keyof PermissionReplyErrors] + +export type PermissionReplyResponses = { + /** + * Permission processed successfully + */ + 200: boolean +} + +export type PermissionReplyResponse = PermissionReplyResponses[keyof PermissionReplyResponses] + +export type PermissionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/permission" +} + +export type PermissionListResponses = { + /** + * List of pending permissions + */ + 200: Array +} + +export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] + +export type QuestionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/question" +} + +export type QuestionListResponses = { + /** + * List of pending questions + */ + 200: Array +} + +export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] + +export type QuestionReplyData = { + body?: { + /** + * User answers in order of questions (each answer is an array of selected labels) + */ + answers: Array + } + path: { + requestID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/question/{requestID}/reply" +} + +export type QuestionReplyErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors] + +export type QuestionReplyResponses = { + /** + * Question answered successfully + */ + 200: boolean +} + +export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] + +export type QuestionRejectData = { + body?: never + path: { + requestID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/question/{requestID}/reject" +} + +export type QuestionRejectErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors] + +export type QuestionRejectResponses = { + /** + * Question rejected successfully + */ + 200: boolean +} + +export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses] + +export type ProviderListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/provider" +} + +export type ProviderListResponses = { + /** + * List of providers + */ + 200: { + all: Array<{ + api?: string + name: string + env: Array + id: string + npm?: string + models: { + [key: string]: { + id: string + name: string + family?: string + release_date: string + attachment: boolean + reasoning: boolean + temperature: boolean + tool_call: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit: { + context: number + input?: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + options: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + provider?: { + npm?: string + api?: string + } + variants?: { + [key: string]: { + [key: string]: unknown + } + } + } + } + }> + default: { + [key: string]: string + } + connected: Array + } +} + +export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses] + +export type ProviderAuthData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/provider/auth" +} + +export type ProviderAuthResponses = { + /** + * Provider auth methods + */ + 200: { + [key: string]: Array + } +} + +export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses] + +export type ProviderOauthAuthorizeData = { + body?: { + /** + * Auth method index + */ + method: number + /** + * Prompt inputs + */ + inputs?: { + [key: string]: string + } + } + path: { + /** + * Provider ID + */ + providerID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/provider/{providerID}/oauth/authorize" +} + +export type ProviderOauthAuthorizeErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors] + +export type ProviderOauthAuthorizeResponses = { + /** + * Authorization URL and method + */ + 200: ProviderAuthAuthorization +} + +export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses] + +export type ProviderOauthCallbackData = { + body?: { + /** + * Auth method index + */ + method: number + /** + * OAuth authorization code + */ + code?: string + } + path: { + /** + * Provider ID + */ + providerID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/provider/{providerID}/oauth/callback" +} + +export type ProviderOauthCallbackErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors] + +export type ProviderOauthCallbackResponses = { + /** + * OAuth callback processed successfully + */ + 200: boolean +} + +export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] + +export type FindTextData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + pattern: string + } + url: "/find" +} + +export type FindTextResponses = { + /** + * Matches + */ + 200: Array<{ + path: { + text: string + } + lines: { + text: string + } + line_number: number + absolute_offset: number + submatches: Array<{ + match: { + text: string + } + start: number + end: number + }> + }> +} + +export type FindTextResponse = FindTextResponses[keyof FindTextResponses] + +export type FindFilesData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number + } + url: "/find/file" +} + +export type FindFilesResponses = { + /** + * File paths + */ + 200: Array +} + +export type FindFilesResponse = FindFilesResponses[keyof FindFilesResponses] + +export type FindSymbolsData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + query: string + } + url: "/find/symbol" +} + +export type FindSymbolsResponses = { + /** + * Symbols + */ + 200: Array +} + +export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses] + +export type FileListData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + path: string + } + url: "/file" +} + +export type FileListResponses = { + /** + * Files and directories + */ + 200: Array +} + +export type FileListResponse = FileListResponses[keyof FileListResponses] + +export type FileReadData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + path: string + } + url: "/file/content" +} + +export type FileReadResponses = { + /** + * File content + */ + 200: FileContent +} + +export type FileReadResponse = FileReadResponses[keyof FileReadResponses] + +export type FileStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/file/status" +} + +export type FileStatusResponses = { + /** + * File status + */ + 200: Array +} + +export type FileStatusResponse = FileStatusResponses[keyof FileStatusResponses] + +export type EventSubscribeData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/event" +} + +export type EventSubscribeResponses = { + /** + * Event stream + */ + 200: Event +} + +export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] + +export type McpStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp" +} + +export type McpStatusResponses = { + /** + * MCP server status + */ + 200: { + [key: string]: McpStatus + } +} + +export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses] + +export type McpAddData = { + body?: { + name: string + config: McpLocalConfig | McpRemoteConfig + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp" +} + +export type McpAddErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type McpAddError = McpAddErrors[keyof McpAddErrors] + +export type McpAddResponses = { + /** + * MCP server added successfully + */ + 200: { + [key: string]: McpStatus + } +} + +export type McpAddResponse = McpAddResponses[keyof McpAddResponses] + +export type McpAuthRemoveData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth" +} + +export type McpAuthRemoveErrors = { + /** + * Not found + */ + 404: NotFoundError +} + +export type McpAuthRemoveError = McpAuthRemoveErrors[keyof McpAuthRemoveErrors] + +export type McpAuthRemoveResponses = { + /** + * OAuth credentials removed + */ + 200: { + success: true + } +} + +export type McpAuthRemoveResponse = McpAuthRemoveResponses[keyof McpAuthRemoveResponses] + +export type McpAuthStartData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth" +} + +export type McpAuthStartErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type McpAuthStartError = McpAuthStartErrors[keyof McpAuthStartErrors] + +export type McpAuthStartResponses = { + /** + * OAuth flow started + */ + 200: { + /** + * URL to open in browser for authorization + */ + authorizationUrl: string + } +} + +export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartResponses] + +export type McpAuthCallbackData = { + body?: { + /** + * Authorization code from OAuth callback + */ + code: string + } + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth/callback" +} + +export type McpAuthCallbackErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type McpAuthCallbackError = McpAuthCallbackErrors[keyof McpAuthCallbackErrors] + +export type McpAuthCallbackResponses = { + /** + * OAuth authentication completed + */ + 200: McpStatus +} + +export type McpAuthCallbackResponse = McpAuthCallbackResponses[keyof McpAuthCallbackResponses] + +export type McpAuthAuthenticateData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth/authenticate" +} + +export type McpAuthAuthenticateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type McpAuthAuthenticateError = McpAuthAuthenticateErrors[keyof McpAuthAuthenticateErrors] + +export type McpAuthAuthenticateResponses = { + /** + * OAuth authentication completed + */ + 200: McpStatus +} + +export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses] + +export type McpConnectData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/connect" +} + +export type McpConnectResponses = { + /** + * MCP server connected successfully + */ + 200: boolean +} + +export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses] + +export type McpDisconnectData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/disconnect" +} + +export type McpDisconnectResponses = { + /** + * MCP server disconnected successfully + */ + 200: boolean +} + +export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses] + +export type TuiAppendPromptData = { + body?: { + text: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/append-prompt" +} + +export type TuiAppendPromptErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiAppendPromptError = TuiAppendPromptErrors[keyof TuiAppendPromptErrors] + +export type TuiAppendPromptResponses = { + /** + * Prompt processed successfully + */ + 200: boolean +} + +export type TuiAppendPromptResponse = TuiAppendPromptResponses[keyof TuiAppendPromptResponses] + +export type TuiOpenHelpData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-help" +} + +export type TuiOpenHelpResponses = { + /** + * Help dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenHelpResponse = TuiOpenHelpResponses[keyof TuiOpenHelpResponses] + +export type TuiOpenSessionsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-sessions" +} + +export type TuiOpenSessionsResponses = { + /** + * Session dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenSessionsResponse = TuiOpenSessionsResponses[keyof TuiOpenSessionsResponses] + +export type TuiOpenThemesData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-themes" +} + +export type TuiOpenThemesResponses = { + /** + * Theme dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenThemesResponse = TuiOpenThemesResponses[keyof TuiOpenThemesResponses] + +export type TuiOpenModelsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-models" +} + +export type TuiOpenModelsResponses = { + /** + * Model dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenModelsResponse = TuiOpenModelsResponses[keyof TuiOpenModelsResponses] + +export type TuiSubmitPromptData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/submit-prompt" +} + +export type TuiSubmitPromptResponses = { + /** + * Prompt submitted successfully + */ + 200: boolean +} + +export type TuiSubmitPromptResponse = TuiSubmitPromptResponses[keyof TuiSubmitPromptResponses] + +export type TuiClearPromptData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/clear-prompt" +} + +export type TuiClearPromptResponses = { + /** + * Prompt cleared successfully + */ + 200: boolean +} + +export type TuiClearPromptResponse = TuiClearPromptResponses[keyof TuiClearPromptResponses] + +export type TuiExecuteCommandData = { + body?: { + command: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/execute-command" +} + +export type TuiExecuteCommandErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiExecuteCommandError = TuiExecuteCommandErrors[keyof TuiExecuteCommandErrors] + +export type TuiExecuteCommandResponses = { + /** + * Command executed successfully + */ + 200: boolean +} + +export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses] + +export type TuiShowToastData = { + body?: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/show-toast" +} + +export type TuiShowToastResponses = { + /** + * Toast notification shown successfully + */ + 200: boolean +} + +export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses] + +export type TuiPublishData = { + body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/publish" +} + +export type TuiPublishErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiPublishError = TuiPublishErrors[keyof TuiPublishErrors] + +export type TuiPublishResponses = { + /** + * Event published successfully + */ + 200: boolean +} + +export type TuiPublishResponse = TuiPublishResponses[keyof TuiPublishResponses] + +export type TuiSelectSessionData = { + body?: { + /** + * Session ID to navigate to + */ + sessionID: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/select-session" +} + +export type TuiSelectSessionErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type TuiSelectSessionError = TuiSelectSessionErrors[keyof TuiSelectSessionErrors] + +export type TuiSelectSessionResponses = { + /** + * Session selected successfully + */ + 200: boolean +} + +export type TuiSelectSessionResponse = TuiSelectSessionResponses[keyof TuiSelectSessionResponses] + +export type TuiControlNextData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/control/next" +} + +export type TuiControlNextResponses = { + /** + * Next TUI request + */ + 200: { + path: string + body: unknown + } +} + +export type TuiControlNextResponse = TuiControlNextResponses[keyof TuiControlNextResponses] + +export type TuiControlResponseData = { + body?: unknown + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/control/response" +} + +export type TuiControlResponseResponses = { + /** + * Response submitted successfully + */ + 200: boolean +} + +export type TuiControlResponseResponse = TuiControlResponseResponses[keyof TuiControlResponseResponses] + +export type InstanceDisposeData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/instance/dispose" +} + +export type InstanceDisposeResponses = { + /** + * Instance disposed + */ + 200: boolean +} + +export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses] + +export type PathGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/path" +} + +export type PathGetResponses = { + /** + * Path + */ + 200: Path +} + +export type PathGetResponse = PathGetResponses[keyof PathGetResponses] + +export type VcsGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/vcs" +} + +export type VcsGetResponses = { + /** + * VCS info + */ + 200: VcsInfo +} + +export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses] + +export type CommandListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/command" +} + +export type CommandListResponses = { + /** + * List of commands + */ + 200: Array +} + +export type CommandListResponse = CommandListResponses[keyof CommandListResponses] + +export type AppLogData = { + body?: { + /** + * Service name for the log entry + */ + service: string + /** + * Log level + */ + level: "debug" | "info" | "error" | "warn" + /** + * Log message + */ + message: string + /** + * Additional metadata for the log entry + */ + extra?: { + [key: string]: unknown + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/log" +} + +export type AppLogErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AppLogError = AppLogErrors[keyof AppLogErrors] + +export type AppLogResponses = { + /** + * Log entry written successfully + */ + 200: boolean +} + +export type AppLogResponse = AppLogResponses[keyof AppLogResponses] + +export type AppAgentsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/agent" +} + +export type AppAgentsResponses = { + /** + * List of agents + */ + 200: Array +} + +export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses] + +export type AppSkillsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/skill" +} + +export type AppSkillsResponses = { + /** + * List of skills + */ + 200: Array<{ + name: string + description: string + location: string + content: string + }> +} + +export type AppSkillsResponse = AppSkillsResponses[keyof AppSkillsResponses] + +export type LspStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/lsp" +} + +export type LspStatusResponses = { + /** + * LSP server status + */ + 200: Array +} + +export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] + +export type FormatterStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/formatter" +} + +export type FormatterStatusResponses = { + /** + * Formatter status + */ + 200: Array +} + +export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] diff --git a/server/opencode/v2/index.ts b/server/opencode/v2/index.ts new file mode 100644 index 00000000..d044f5ad --- /dev/null +++ b/server/opencode/v2/index.ts @@ -0,0 +1,21 @@ +export * from "./client.js" +export * from "./server.js" + +import { createOpencodeClient } from "./client.js" +import { createOpencodeServer } from "./server.js" +import type { ServerOptions } from "./server.js" + +export async function createOpencode(options?: ServerOptions) { + const server = await createOpencodeServer({ + ...options, + }) + + const client = createOpencodeClient({ + baseUrl: server.url, + }) + + return { + client, + server, + } +} diff --git a/server/opencode/v2/server.ts b/server/opencode/v2/server.ts new file mode 100644 index 00000000..a76b0ce4 --- /dev/null +++ b/server/opencode/v2/server.ts @@ -0,0 +1,125 @@ +import { spawn } from "node:child_process" +import { type Config } from "./gen/types.gen.js" + +export type ServerOptions = { + hostname?: string + port?: number + signal?: AbortSignal + timeout?: number + config?: Config +} + +export type TuiOptions = { + project?: string + model?: string + session?: string + agent?: string + signal?: AbortSignal + config?: Config +} + +export async function createOpencodeServer(options?: ServerOptions) { + options = Object.assign( + { + hostname: "127.0.0.1", + port: 4096, + timeout: 5000, + }, + options ?? {}, + ) + + const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`] + if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`) + + const proc = spawn(`opencode`, args, { + signal: options.signal, + shell: process.platform === 'win32', + env: { + ...process.env, + OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}), + }, + }) + + const url = await new Promise((resolve, reject) => { + const id = setTimeout(() => { + reject(new Error(`Timeout waiting for server to start after ${options.timeout}ms`)) + }, options.timeout) + let output = "" + proc.stdout?.on("data", (chunk) => { + output += chunk.toString() + const lines = output.split("\n") + for (const line of lines) { + if (line.startsWith("opencode server listening")) { + const match = line.match(/on\s+(https?:\/\/[^\s]+)/) + if (!match) { + throw new Error(`Failed to parse server url from output: ${line}`) + } + clearTimeout(id) + resolve(match[1]!) + return + } + } + }) + proc.stderr?.on("data", (chunk) => { + output += chunk.toString() + }) + proc.on("exit", (code) => { + clearTimeout(id) + let msg = `Server exited with code ${code}` + if (output.trim()) { + msg += `\nServer output: ${output}` + } + reject(new Error(msg)) + }) + proc.on("error", (error) => { + clearTimeout(id) + reject(error) + }) + if (options.signal) { + options.signal.addEventListener("abort", () => { + clearTimeout(id) + reject(new Error("Aborted")) + }) + } + }) + + return { + url, + close() { + proc.kill() + }, + } +} + +export function createOpencodeTui(options?: TuiOptions) { + const args = [] + + if (options?.project) { + args.push(`--project=${options.project}`) + } + if (options?.model) { + args.push(`--model=${options.model}`) + } + if (options?.session) { + args.push(`--session=${options.session}`) + } + if (options?.agent) { + args.push(`--agent=${options.agent}`) + } + + const proc = spawn(`opencode`, args, { + signal: options?.signal, + stdio: "inherit", + shell: process.platform === 'win32', + env: { + ...process.env, + OPENCODE_CONFIG_CONTENT: JSON.stringify(options?.config ?? {}), + }, + }) + + return { + close() { + proc.kill() + }, + } +} diff --git a/server/utils/codex-client.ts b/server/utils/codex-client.ts index 766bdd82..82b6c175 100644 --- a/server/utils/codex-client.ts +++ b/server/utils/codex-client.ts @@ -72,15 +72,20 @@ export async function runCodexExec( } if (codexEffort) { - args.push('--config', `model_reasoning_effort="${codexEffort}"`) + args.push('--config', `model_reasoning_effort=${codexEffort}`) } - args.push(prompt) + // On Windows, passing long prompts as command-line arguments causes + // shell escaping issues (PowerShell MissingExpression, special chars). + // Use codex's stdin mode (`-` as prompt arg) on all platforms — simpler + // and avoids command-line length limits. + args.push('-') try { const runResult = await executeCodexCommand( args, options.timeoutMs ?? DEFAULT_CODEX_TIMEOUT_MS, + prompt, ) const finalText = await readFile(outputPath, 'utf-8').catch(() => '') const normalizedText = finalText.trim() || runResult.text.trim() @@ -112,10 +117,12 @@ function buildPrompt(systemPrompt: string | undefined, userPrompt: string, image } return [ - 'SYSTEM INSTRUCTIONS:', + 'You are a design generation assistant. Follow the guidelines below to produce the requested output.', + '', + '--- GUIDELINES ---', systemPrompt.trim(), '', - 'USER REQUEST:', + '--- TASK ---', userText + imageSection, ].join('\n') } @@ -146,15 +153,22 @@ function resolveCodexEffort( async function executeCodexCommand( args: string[], timeoutMs: number, + stdinText?: string, ): Promise<{ text: string; errors: string[] }> { return await new Promise((resolve, reject) => { const child = spawn('codex', args, { env: filterCodexEnv(process.env as Record), - stdio: ['ignore', 'pipe', 'pipe'], - // On Windows, npm-installed CLIs are .cmd scripts — need shell to resolve them + stdio: [stdinText ? 'pipe' : 'ignore', 'pipe', 'pipe'], + // On Windows, npm-installed CLIs are .cmd scripts — need shell to resolve. ...(process.platform === 'win32' && { shell: true }), }) + // Pipe prompt via stdin (codex reads from stdin when `-` is the prompt arg) + if (stdinText && child.stdin) { + child.stdin.write(stdinText) + child.stdin.end() + } + let stdoutBuffer = '' let stderrBuffer = '' let textAccumulator = '' @@ -176,7 +190,7 @@ async function executeCodexCommand( reject(new Error(`Codex request timed out after ${Math.round(timeoutMs / 1000)}s.`)) }, timeoutMs) - child.stdout.on('data', (chunk: Buffer) => { + child.stdout!.on('data', (chunk: Buffer) => { stdoutBuffer += chunk.toString('utf-8') let idx = stdoutBuffer.indexOf('\n') while (idx >= 0) { @@ -187,7 +201,7 @@ async function executeCodexCommand( } }) - child.stderr.on('data', (chunk: Buffer) => { + child.stderr!.on('data', (chunk: Buffer) => { stderrBuffer += chunk.toString('utf-8') }) @@ -266,6 +280,8 @@ function extractCodexCliError(stderr: string): string | null { if (!trimmed) return null const lines = trimmed.split('\n').map((line) => line.trim()).filter(Boolean) + + // 1. Look for "error: ..." lines (simple CLI errors) for (let i = lines.length - 1; i >= 0; i--) { const line = lines[i] if (line.toLowerCase().startsWith('error:')) { @@ -273,5 +289,25 @@ function extractCodexCliError(stderr: string): string | null { } } - return lines[lines.length - 1] ?? null + // 2. Look for Codex structured log errors: " ERROR : " + // These contain the real error (auth failures, API errors, etc.) + for (let i = lines.length - 1; i >= 0; i--) { + const match = lines[i].match(/\bERROR\s+\S+:\s*(.+)/) + if (match) { + const msg = match[1].trim() + // For auth errors, provide actionable guidance + if (/refresh token|sign in again|token.*expired|401 Unauthorized/i.test(msg)) { + return 'Codex authentication expired. Run "codex logout && codex login" to re-authenticate.' + } + return msg + } + } + + // 3. Skip unhelpful "Warning: no last agent message" — surface it only as fallback + const lastLine = lines[lines.length - 1] ?? null + if (lastLine && /^warning:\s*no last agent message/i.test(lastLine)) { + return 'Codex returned no output. Check "codex login" status or try a different model.' + } + + return lastLine } diff --git a/server/utils/copilot-client.ts b/server/utils/copilot-client.ts index 25c770db..be3a6556 100644 --- a/server/utils/copilot-client.ts +++ b/server/utils/copilot-client.ts @@ -5,6 +5,21 @@ import { serverLog } from './server-logger' const isWindows = process.platform === 'win32' +/** Windows npm global installs may create .cmd or .ps1 wrappers — try both */ +function winNpmCandidates(dir: string, name: string): string[] { + return [join(dir, `${name}.cmd`), join(dir, `${name}.ps1`)] +} + +/** On Windows, `where` may return an extensionless shell script — prefer .cmd/.ps1 */ +function resolveWinExtension(binPath: string): string { + if (!isWindows) return binPath + if (/\.(cmd|ps1|exe)$/i.test(binPath)) return binPath + for (const ext of ['.cmd', '.ps1']) { + if (existsSync(binPath + ext)) return binPath + ext + } + return binPath +} + /** Resolve the standalone copilot CLI binary path to avoid Bun's node:sqlite issue */ export function resolveCopilotCli(): string | undefined { serverLog.info(`[resolve-copilot] platform=${process.platform}, isWindows=${isWindows}`) @@ -17,7 +32,7 @@ export function resolveCopilotCli(): string | undefined { // `where` on Windows may return multiple lines const path = result.split(/\r?\n/)[0]?.trim() serverLog.info(`[resolve-copilot] PATH result: "${path}" (exists=${path ? existsSync(path) : false})`) - if (path && existsSync(path)) return path + if (path && existsSync(path)) return resolveWinExtension(path) } catch (err) { serverLog.info(`[resolve-copilot] PATH lookup failed: ${err instanceof Error ? err.message : err}`) } @@ -32,9 +47,10 @@ export function resolveCopilotCli(): string | undefined { }).trim() serverLog.info(`[resolve-copilot] npm global prefix: "${prefix}"`) if (prefix) { - const bin = join(prefix, 'copilot.cmd') - serverLog.info(`[resolve-copilot] npm global bin: "${bin}" (exists=${existsSync(bin)})`) - if (existsSync(bin)) return bin + for (const bin of winNpmCandidates(prefix, 'copilot')) { + serverLog.info(`[resolve-copilot] npm global bin: "${bin}" (exists=${existsSync(bin)})`) + if (existsSync(bin)) return bin + } } } catch (err) { serverLog.info(`[resolve-copilot] npm prefix -g failed: ${err instanceof Error ? err.message : err}`) @@ -44,11 +60,11 @@ export function resolveCopilotCli(): string | undefined { // 3. Common install locations if (isWindows) { const candidates = [ - // npm global - join(process.env.APPDATA || '', 'npm', 'copilot.cmd'), + // npm global (.cmd + .ps1) + ...winNpmCandidates(join(process.env.APPDATA || '', 'npm'), 'copilot'), // nvm-windows / fnm - join(process.env.NVM_SYMLINK || '', 'copilot.cmd'), - join(process.env.FNM_MULTISHELL_PATH || '', 'copilot.cmd'), + ...winNpmCandidates(join(process.env.NVM_SYMLINK || ''), 'copilot'), + ...winNpmCandidates(join(process.env.FNM_MULTISHELL_PATH || ''), 'copilot'), // winget / native join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'copilot.exe'), ] diff --git a/server/utils/opencode-client.ts b/server/utils/opencode-client.ts index ce579e12..f0850115 100644 --- a/server/utils/opencode-client.ts +++ b/server/utils/opencode-client.ts @@ -19,7 +19,7 @@ process.on('SIGTERM', cleanup) process.on('SIGINT', cleanup) export async function getOpencodeClient() { - const { createOpencodeClient, createOpencode } = await import('@opencode-ai/sdk/v2') + const { createOpencodeClient, createOpencode } = await import('../opencode/index') // Try connecting to an existing server first try { diff --git a/server/utils/resolve-claude-agent-env.ts b/server/utils/resolve-claude-agent-env.ts index a241dc75..8c86fdc4 100644 --- a/server/utils/resolve-claude-agent-env.ts +++ b/server/utils/resolve-claude-agent-env.ts @@ -1,3 +1,4 @@ +import { spawn } from 'node:child_process' import { mkdirSync, readFileSync } from 'node:fs' import { homedir, tmpdir } from 'node:os' import { join } from 'node:path' @@ -117,3 +118,21 @@ export function getClaudeAgentDebugFilePath(): string | undefined { return undefined } } + +/** + * Custom spawnClaudeCodeProcess for Windows. + * On Windows, npm-installed CLIs are .cmd/.ps1 scripts that can't be spawned + * directly without a shell. Uses PowerShell to avoid cmd.exe's 8191-char limit. + */ +export function buildSpawnClaudeCodeProcess() { + if (process.platform !== 'win32') return undefined + return (options: { command: string; args: string[]; cwd?: string; env: Record; signal: AbortSignal }) => { + return spawn(options.command, options.args, { + cwd: options.cwd, + env: options.env as NodeJS.ProcessEnv, + signal: options.signal, + stdio: ['pipe', 'pipe', 'pipe'], + shell: 'powershell.exe', + }) + } +} diff --git a/server/utils/resolve-claude-cli.ts b/server/utils/resolve-claude-cli.ts index 592089e9..23a388b4 100644 --- a/server/utils/resolve-claude-cli.ts +++ b/server/utils/resolve-claude-cli.ts @@ -6,6 +6,21 @@ import { serverLog } from './server-logger' const isWindows = platform() === 'win32' +/** Windows npm global installs may create .cmd or .ps1 wrappers — try both */ +function winNpmCandidates(dir: string, name: string): string[] { + return [join(dir, `${name}.cmd`), join(dir, `${name}.ps1`)] +} + +/** On Windows, `where` may return an extensionless shell script — prefer .cmd/.ps1/.exe */ +function resolveWinExtension(binPath: string): string { + if (!isWindows) return binPath + if (/\.(cmd|ps1|exe)$/i.test(binPath)) return binPath + for (const ext of ['.cmd', '.ps1', '.exe']) { + if (existsSync(binPath + ext)) return binPath + ext + } + return binPath +} + /** * Resolve the absolute path to the standalone `claude` binary. * @@ -28,7 +43,7 @@ export function resolveClaudeCli(): string | undefined { }).trim() const p = raw.split(/\r?\n/)[0] // `where` on Windows may return multiple lines serverLog.info(`[resolve-claude-cli] PATH lookup result: "${p}" (exists=${p ? existsSync(p) : false})`) - if (p && existsSync(p)) return p + if (p && existsSync(p)) return resolveWinExtension(p) } catch (err) { serverLog.info(`[resolve-claude-cli] PATH lookup failed: ${err instanceof Error ? err.message : err}`) } @@ -44,9 +59,10 @@ export function resolveClaudeCli(): string | undefined { }).trim() serverLog.info(`[resolve-claude-cli] npm global prefix: "${prefix}"`) if (prefix) { - const bin = join(prefix, 'claude.cmd') - serverLog.info(`[resolve-claude-cli] checking npm global bin: "${bin}" (exists=${existsSync(bin)})`) - if (existsSync(bin)) return bin + for (const bin of winNpmCandidates(prefix, 'claude')) { + serverLog.info(`[resolve-claude-cli] checking npm global bin: "${bin}" (exists=${existsSync(bin)})`) + if (existsSync(bin)) return bin + } } } catch (err) { serverLog.info(`[resolve-claude-cli] npm prefix -g failed: ${err instanceof Error ? err.message : err}`) @@ -56,11 +72,11 @@ export function resolveClaudeCli(): string | undefined { // 3. Common install locations const candidates = isWindows ? [ - // npm global (npm install -g creates .cmd wrappers here) - join(process.env.APPDATA || '', 'npm', 'claude.cmd'), + // npm global (.cmd + .ps1) + ...winNpmCandidates(join(process.env.APPDATA || '', 'npm'), 'claude'), // nvm-windows / fnm - join(process.env.NVM_SYMLINK || '', 'claude.cmd'), - join(process.env.FNM_MULTISHELL_PATH || '', 'claude.cmd'), + ...winNpmCandidates(join(process.env.NVM_SYMLINK || ''), 'claude'), + ...winNpmCandidates(join(process.env.FNM_MULTISHELL_PATH || ''), 'claude'), // Native .exe install locations join(process.env.LOCALAPPDATA || '', 'Programs', 'claude-code', 'claude.exe'), join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'claude.exe'), diff --git a/src/components/panels/ai-chat-handlers.ts b/src/components/panels/ai-chat-handlers.ts index e94bc3f1..1c57353d 100644 --- a/src/components/panels/ai-chat-handlers.ts +++ b/src/components/panels/ai-chat-handlers.ts @@ -3,6 +3,7 @@ import { nanoid } from 'nanoid' import { useAIStore } from '@/stores/ai-store' import { useCanvasStore } from '@/stores/canvas-store' import { useDocumentStore } from '@/stores/document-store' +import { getActivePageChildren } from '@/stores/document-tree-utils' import { streamChat } from '@/services/ai/ai-service' import { CHAT_SYSTEM_PROMPT } from '@/services/ai/ai-prompts' import { @@ -18,15 +19,18 @@ import { CHAT_STREAM_THINKING_CONFIG } from '@/services/ai/ai-runtime-config' /** Intent classification prompt — lightweight LLM call to determine message routing */ const CLASSIFY_PROMPT = `You are a UI design tool assistant. Classify the user's message intent. Reply with EXACTLY one of these tags, nothing else: -- DESIGN — user wants to create, generate, or modify any UI element, component, screen, or page +- DESIGN_NEW — user wants to create or generate a NEW design, screen, page, or component from scratch +- DESIGN_MODIFY — user wants to modify, adjust, refine, or iterate on an EXISTING design (e.g. change colors, resize, restyle, add/remove elements) - CHAT — user is asking a question, seeking help, or having a conversation` +type DesignIntent = 'new' | 'modify' | 'chat' + /** Classify user intent via a lightweight LLM call instead of hardcoded keyword matching */ async function classifyIntent( text: string, model: string, provider?: string, -): Promise<{ isDesign: boolean }> { +): Promise<{ intent: DesignIntent }> { try { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 8_000) @@ -48,10 +52,15 @@ async function classifyIntent( const data = await response.json() const upper = (data.text ?? '').trim().toUpperCase() - return { isDesign: upper.includes('DESIGN') } + if (upper.includes('DESIGN_MODIFY')) return { intent: 'modify' } + if (upper.includes('DESIGN_NEW') || upper.includes('DESIGN')) return { intent: 'new' } + if (upper.includes('CHAT')) return { intent: 'chat' } + + // Fallback: in a design tool, default to new design mode + return { intent: 'new' } } catch { - // Fallback: in a design tool, default to design mode - return { isDesign: true } + // Fallback: in a design tool, default to new design mode + return { intent: 'new' } } } @@ -169,24 +178,44 @@ export function useChatHandlers() { useAIStore.getState().setAbortController(abortController) try { - // Classify intent via lightweight LLM call + // Classify intent via lightweight LLM call (three-way: new / modify / chat) const classified = await classifyIntent( messageText, model, currentProvider, ) - isDesign = classified.isDesign - const isModification = isDesign && hasSelection + let intent = classified.intent + + // When LLM says "modify" but canvas is empty, degrade to new design + const { document: currentDoc } = useDocumentStore.getState() + const activePageId = useCanvasStore.getState().activePageId + const pageChildren = getActivePageChildren(currentDoc, activePageId) + if (intent === 'modify' && pageChildren.length === 0) { + intent = 'new' + } + + isDesign = intent === 'new' || intent === 'modify' + + // Determine modification target: explicit selection or auto-selected frame + const isModification = intent === 'modify' && (hasSelection || pageChildren.length > 0) if (isDesign) { if (isModification) { // --- MODIFICATION MODE --- const { getNodeById, document: modDoc } = useDocumentStore.getState() - const selectedNodes = selectedIds.map(id => getNodeById(id)).filter(Boolean) as any[] + let modTargets: any[] + if (hasSelection) { + // User explicitly selected nodes + modTargets = selectedIds.map(id => getNodeById(id)).filter(Boolean) + } else { + // Auto-select: last top-level frame on the active page + const frames = pageChildren.filter(n => n.type === 'frame') + modTargets = frames.length > 0 ? [frames[frames.length - 1]] : [pageChildren[pageChildren.length - 1]] + } // We update the UI to show we are working accumulated = 'Analyzing modification request...' updateLastMessage(accumulated) - const { rawResponse, nodes } = await generateDesignModification(selectedNodes, messageText, { + const { rawResponse, nodes } = await generateDesignModification(modTargets, messageText, { variables: modDoc.variables, themes: modDoc.themes, model, diff --git a/src/components/shared/agent-settings-dialog.tsx b/src/components/shared/agent-settings-dialog.tsx index 67d9a100..7d4af98a 100644 --- a/src/components/shared/agent-settings-dialog.tsx +++ b/src/components/shared/agent-settings-dialog.tsx @@ -50,7 +50,7 @@ type SettingsTab = 'agents' | 'mcp' | 'system' async function connectAgent( agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot', -): Promise<{ connected: boolean; models: GroupedModel[]; error?: string; notInstalled?: boolean; connectionInfo?: string; hintPath?: string }> { +): Promise<{ connected: boolean; models: GroupedModel[]; error?: string; warning?: string; notInstalled?: boolean; connectionInfo?: string; hintPath?: string }> { try { const res = await fetch('/api/ai/connect-agent', { method: 'POST', @@ -104,6 +104,7 @@ function ProviderCard({ type }: { type: AIProviderType }) { const [isConnecting, setIsConnecting] = useState(false) const [error, setError] = useState(null) + const [warning, setWarning] = useState(null) const [notInstalled, setNotInstalled] = useState(false) const [isInstalling, setIsInstalling] = useState(false) const [installInfo, setInstallInfo] = useState<{ command: string; docsUrl: string } | null>(null) @@ -113,12 +114,14 @@ function ProviderCard({ type }: { type: AIProviderType }) { const handleConnect = useCallback(async () => { setIsConnecting(true) setError(null) + setWarning(null) setNotInstalled(false) setInstallInfo(null) const result = await connectAgent(meta.agent) if (result.connected) { connect(type, meta.agent, result.models, result.connectionInfo, result.hintPath) persist() + if (result.warning) setWarning(result.warning) } else if (result.notInstalled) { setNotInstalled(true) } else { @@ -257,6 +260,9 @@ function ProviderCard({ type }: { type: AIProviderType }) { {error && ( {error} )} + {warning && !error && ( + {warning} + )} {/* Action */} diff --git a/src/services/ai/ai-service.ts b/src/services/ai/ai-service.ts index 12749ea7..ea7d8bec 100644 --- a/src/services/ai/ai-service.ts +++ b/src/services/ai/ai-service.ts @@ -136,6 +136,22 @@ export async function* streamChat( return } + // Server returned JSON instead of SSE stream — read body as JSON error + const contentType = response.headers.get('content-type') ?? '' + if (contentType.includes('application/json')) { + const body = await response.text() + try { + const jsonBody = JSON.parse(body) + yield { type: 'error', content: jsonBody.error || jsonBody.message || `Unexpected JSON response: ${body.slice(0, 200)}` } + } catch { + yield { type: 'error', content: `Unexpected server response: ${body.slice(0, 200)}` } + } + clearTimeout(hardTimeout) + clearNoTextTimeout() + clearFirstTextTimeout() + return + } + const reader = response.body?.getReader() if (!reader) { yield { type: 'error', content: 'No response stream available' } @@ -150,7 +166,20 @@ export async function* streamChat( while (true) { const { done, value } = await reader.read() - if (done) break + if (done) { + if (buffer.trim().length > 0) { + // Remaining buffer may be a non-SSE response (e.g. JSON error) + try { + const jsonErr = JSON.parse(buffer.trim()) + if (jsonErr.error) { + yield { type: 'error', content: jsonErr.error } as AIStreamChunk + } + } catch { + // Not JSON, ignore remaining buffer + } + } + break + } buffer += decoder.decode(value, { stream: true }) @@ -164,6 +193,7 @@ export async function* streamChat( if (!data) continue try { const chunk = JSON.parse(data) as AIStreamChunk + if (chunk.type === 'done') { clearTimeout(hardTimeout) clearNoTextTimeout() diff --git a/src/services/ai/design-generator.ts b/src/services/ai/design-generator.ts index 7f02e8ad..34651b0c 100644 --- a/src/services/ai/design-generator.ts +++ b/src/services/ai/design-generator.ts @@ -7,7 +7,7 @@ import { DESIGN_MODIFIER_PROMPT } from './ai-prompts' import { executeOrchestration } from './orchestrator' import { DESIGN_STREAM_TIMEOUTS } from './ai-runtime-config' import { extractJsonFromResponse } from './design-parser' -import { resolveModelProfile, applyProfileToTimeouts, needsSimplifiedPrompt } from './model-profiles' +import { resolveModelProfile, applyProfileToTimeouts } from './model-profiles' // --------------------------------------------------------------------------- // Re-exports for backward compatibility — consumers that import from @@ -119,16 +119,8 @@ export async function generateDesignModification( const profile = resolveModelProfile(options?.model) const timeouts = applyProfileToTimeouts({ ...DESIGN_STREAM_TIMEOUTS }, profile) - // Basic-tier models (MiniMax, GLM, etc.) ignore system prompts via routers — - // inline the system prompt into the user message so the model sees it. - const inlineSystem = needsSimplifiedPrompt(profile) - const effectiveSystem = inlineSystem ? '' : DESIGN_MODIFIER_PROMPT - const effectiveUserContent = inlineSystem - ? DESIGN_MODIFIER_PROMPT + '\n\n---\n\n' + userMessage - : userMessage - - for await (const chunk of streamChat(effectiveSystem, [ - { role: 'user', content: effectiveUserContent }, + for await (const chunk of streamChat(DESIGN_MODIFIER_PROMPT, [ + { role: 'user', content: userMessage }, ], options?.model, timeouts, options?.provider, abortSignal)) { if (chunk.type === 'thinking') { // Ignore thinking chunks for modification -- caller already shows progress diff --git a/src/services/ai/model-profiles.ts b/src/services/ai/model-profiles.ts index 28639bb5..dc331246 100644 --- a/src/services/ai/model-profiles.ts +++ b/src/services/ai/model-profiles.ts @@ -37,16 +37,16 @@ const MODEL_PROFILES: ModelProfile[] = [ { match: 'deepseek', tier: 'standard', thinkingMode: 'disabled', label: 'DeepSeek' }, // Basic tier — disable thinking, use simplified prompt - { match: 'claude-haiku', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'Claude Haiku' }, - { match: 'gpt-4o-mini', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'GPT-4o Mini' }, - { match: 'gpt-4.1-mini', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'GPT-4.1 Mini' }, - { match: 'gpt-4.1-nano', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'GPT-4.1 Nano' }, - { match: 'minimax', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'MiniMax' }, - { match: 'qwen', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'Qwen' }, - { match: 'llama', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'Llama' }, - { match: 'mistral', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'Mistral' }, - { match: 'gemma', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'Gemma' }, - { match: 'glm', tier: 'basic', thinkingMode: 'disabled', simplifiedPrompt: true, label: 'GLM' }, + { match: 'claude-haiku', tier: 'basic', thinkingMode: 'disabled', label: 'Claude Haiku' }, + { match: 'gpt-4o-mini', tier: 'basic', thinkingMode: 'disabled', label: 'GPT-4o Mini' }, + { match: 'gpt-4.1-mini', tier: 'basic', thinkingMode: 'disabled', label: 'GPT-4.1 Mini' }, + { match: 'gpt-4.1-nano', tier: 'basic', thinkingMode: 'disabled', label: 'GPT-4.1 Nano' }, + { match: 'minimax', tier: 'basic', thinkingMode: 'disabled', label: 'MiniMax' }, + { match: 'qwen', tier: 'basic', thinkingMode: 'disabled', label: 'Qwen' }, + { match: 'llama', tier: 'basic', thinkingMode: 'disabled', label: 'Llama' }, + { match: 'mistral', tier: 'basic', thinkingMode: 'disabled', label: 'Mistral' }, + { match: 'gemma', tier: 'basic', thinkingMode: 'disabled', label: 'Gemma' }, + { match: 'glm', tier: 'basic', thinkingMode: 'disabled', label: 'GLM' }, ] const DEFAULT_PROFILE: ModelProfile = { diff --git a/src/services/ai/orchestrator-sub-agent.ts b/src/services/ai/orchestrator-sub-agent.ts index 44a067b3..fa854388 100644 --- a/src/services/ai/orchestrator-sub-agent.ts +++ b/src/services/ai/orchestrator-sub-agent.ts @@ -18,12 +18,11 @@ import type { SubAgentResult, } from './ai-types' import { streamChat } from './ai-service' -import { SUB_AGENT_PROMPT, SUB_AGENT_PROMPT_SIMPLIFIED } from './orchestrator-prompts' +import { SUB_AGENT_PROMPT } from './orchestrator-prompts' import { type PreparedDesignPrompt, getSubAgentTimeouts, } from './orchestrator-prompt-optimizer' -import { resolveModelProfile, needsSimplifiedPrompt } from './model-profiles' import { expandRootFrameHeight, extractStreamingNodes, @@ -243,22 +242,11 @@ async function executeSubAgent( request.context?.themes, ) - // Select prompt variant based on model profile - const profile = resolveModelProfile(request.model) - const basePrompt = needsSimplifiedPrompt(profile) ? SUB_AGENT_PROMPT_SIMPLIFIED : SUB_AGENT_PROMPT - const systemPrompt = preparedPrompt.designPrinciples && !needsSimplifiedPrompt(profile) + const basePrompt = SUB_AGENT_PROMPT + const systemPrompt = preparedPrompt.designPrinciples ? `${basePrompt}\n\n${preparedPrompt.designPrinciples}` : basePrompt - // For basic-tier models (MiniMax, GLM, Qwen, etc.) via third-party routers, - // system prompts may be ignored or lost. Inline the system prompt into the - // user message to ensure the model sees the instructions. - const inlineSystem = needsSimplifiedPrompt(profile) - const effectiveSystem = inlineSystem ? '' : systemPrompt - const effectiveUserContent = inlineSystem - ? `${systemPrompt}\n\n---\n\n${userPrompt}` - : userPrompt - let rawResponse = '' const nodes: PenNode[] = [] let streamOffset = 0 @@ -266,8 +254,8 @@ async function executeSubAgent( try { for await (const chunk of streamChat( - effectiveSystem, - [{ role: 'user', content: effectiveUserContent }], + systemPrompt, + [{ role: 'user', content: userPrompt }], request.model, timeoutOptions, request.provider, diff --git a/src/services/ai/orchestrator.ts b/src/services/ai/orchestrator.ts index 2daefbde..87b94c4c 100644 --- a/src/services/ai/orchestrator.ts +++ b/src/services/ai/orchestrator.ts @@ -23,8 +23,8 @@ import { ORCHESTRATOR_PROMPT } from './orchestrator-prompts' import { getOrchestratorTimeouts, prepareDesignPrompt, + buildFallbackPlanFromPrompt, } from './orchestrator-prompt-optimizer' -import { resolveModelProfile, needsSimplifiedPrompt } from './model-profiles' import { adjustRootFrameHeightToContent, insertStreamingNode, @@ -437,18 +437,9 @@ async function callOrchestrator( let rawResponse = '' let thinkingContent = '' - // For basic-tier models, inline system prompt into user message - // since third-party routers may drop or ignore system prompts. - const profile = resolveModelProfile(model) - const inlineSystem = needsSimplifiedPrompt(profile) - const effectiveSystem = inlineSystem ? '' : ORCHESTRATOR_PROMPT - const effectiveUserContent = inlineSystem - ? `${ORCHESTRATOR_PROMPT}\n\n---\n\nUSER REQUEST:\n${prompt}` - : prompt - for await (const chunk of streamChat( - effectiveSystem, - [{ role: 'user', content: effectiveUserContent }], + ORCHESTRATOR_PROMPT, + [{ role: 'user', content: prompt }], model, getOrchestratorTimeouts(timeoutHintLength, model), provider, @@ -465,15 +456,15 @@ async function callOrchestrator( } const plan = parseOrchestratorResponse(rawResponse) - if (!plan) { - const preview = rawResponse.trim().slice(0, 150) - const hint = rawResponse.trim().length === 0 - ? 'The model returned an empty response.' - : `Model output: "${preview}${rawResponse.length > 150 ? '…' : ''}"` - throw new Error(`Could not parse design plan from model response. ${hint}`) - } + if (plan) return plan - return plan + // Fallback: model returned non-JSON (e.g. markdown text). Use a heuristic + // plan derived from the user's prompt so generation can still proceed. + console.warn( + '[Orchestrator] Could not parse model response, using fallback plan. Preview:', + rawResponse.trim().slice(0, 150), + ) + return buildFallbackPlanFromPrompt(prompt) } function parseOrchestratorResponse(raw: string): OrchestratorPlan | null {