mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-05-31 19:04:29 +07:00
V0.4.4 (#59)
* feat(ai): enhance Windows CLI binary resolution and connection handling - Introduced functions to handle both .cmd and .ps1 wrappers for Windows installations, improving compatibility for CLI tools like Codex and Copilot. - Updated connection logic in `connect-agent.ts` to utilize environment variables for Codex home directory, enhancing flexibility in locating configuration files. - Added warning messages in connection results to inform users when no models are found, guiding them to run the CLI tools for model population. - Modified the agent settings dialog to accommodate the new warning field in connection responses, improving user feedback during the connection process. This update significantly enhances the user experience for Windows users by ensuring better handling of CLI binaries and providing clearer connection status information. * feat(ai): improve Windows binary resolution for CLI tools - Added `resolveWinExtension` function to handle extensionless binaries returned by the `where` command on Windows, ensuring compatibility with `.cmd` and `.ps1` wrappers. - Updated `connect-agent.ts` and `copilot-client.ts` to utilize the new resolution function, enhancing the reliability of binary path lookups for Codex and Copilot. - Enhanced logging to provide clearer information on resolved paths and their existence status. This update significantly improves the handling of CLI binaries on Windows, ensuring users have a smoother experience when connecting to AI tools. * feat(ai): enhance binary path handling in connect-agent - Added logic to ensure the directory of the resolved OpenCode binary is included in the PATH environment variable, improving the execution of CLI tools. - Enhanced logging to provide feedback when the binary directory is prepended to the PATH, aiding in troubleshooting and user awareness. This update improves the reliability of connecting to AI tools by ensuring the necessary binaries are accessible during execution. * feat(patching): add patch for @opencode-ai/sdk and update package configurations - Introduced a new patch for the @opencode-ai/sdk version 1.2.6 to address specific issues. - Updated package.json and bun.lock to include the new patchedDependencies section, ensuring the patch is applied during installation. - Removed redundant binary path handling code in connect-agent.ts to streamline the connection process. This update enhances the SDK's functionality while simplifying the connection logic for improved performance. * fix(ai): add orchestrator fallback and three-way intent routing Orchestrator now falls back to a heuristic plan when the model returns non-JSON instead of throwing an error. Intent classification upgraded from binary (DESIGN/CHAT) to three-way (DESIGN_NEW/DESIGN_MODIFY/CHAT). Modification requests without an explicit selection auto-target the last top-level frame on the active page, preventing them from being misrouted to the orchestrator. * chore: bump version to 0.4.4 and remove deprecated patch for @opencode-ai/sdk - Updated package version in package.json from 0.4.3 to 0.4.4. - Removed the patchedDependencies section for @opencode-ai/sdk as the patch is no longer needed. - Added new utility function `buildSpawnClaudeCodeProcess` to enhance agent SDK functionality across multiple files. * chore: bump version to 0.4.4 in package.json * chore(electron): optimize application icon assets Reduce icon file sizes for faster builds and smaller distribution. * chore: remove deprecated patchedDependencies for @opencode-ai/sdk in bun.lock - Eliminated the patchedDependencies section for @opencode-ai/sdk as the patch is no longer necessary, streamlining the dependency management in the project. * fix(ai): improve Codex CLI error extraction for auth and structured log errors Parse Codex's structured log format (<timestamp> ERROR <module>: <message>) to surface real errors like expired auth tokens instead of the unhelpful "Warning: no last agent message" fallback. * fix(ai): update error handling and remove deprecated reasoning field - Updated error checks in chat and generate handlers to ensure proper validation of required fields. - Removed the deprecated reasoning field from the OpenCode SDK integration, streamlining the prompt parameters. - Enhanced logging for system prompt injection errors to improve debugging capabilities. * fix(ai): enhance Windows compatibility for Codex CLI execution - Updated the handling of prompts on Windows to avoid parsing errors caused by shell escaping in PowerShell and cmd.exe. - Introduced a temporary PowerShell script to manage prompt input and command execution, ensuring special characters are processed correctly. - Refactored the argument passing mechanism to accommodate the new script-based approach. * fix(electron): improve process cleanup and Windows compatibility - Simplified the application quit logic to always call app.quit() on window close. - Enhanced the before-quit event to ensure proper cleanup of the Nitro process and port file. - Updated the Nitro process termination logic to use SIGKILL for reliable cleanup on non-Windows platforms. - Refactored argument handling in the Codex CLI execution to utilize PowerShell array splatting for safer argument passing. * feat(ai): add fallback for model parsing from Codex's latest-model.md - Implemented a new function to parse model IDs from the latest-model.md file when models_cache.json is unavailable, enhancing compatibility for fresh installations. - Updated logging to reflect the loading of models from the fallback method and improved error handling for model loading scenarios. * feat(ai): update prompt structure for design generation assistant - Revised the prompt format to enhance clarity and organization for the design generation assistant. - Introduced clear section headers for system instructions and user tasks, improving user experience and guidance. * refactor(ai): streamline prompt handling for Codex CLI execution - Simplified the prompt input mechanism for all platforms by utilizing stdin mode, eliminating the need for temporary scripts on Windows. - Improved argument passing to avoid shell escaping issues and command-line length limits, enhancing compatibility and reliability across environments. - Updated the execution logic to handle prompts more efficiently, ensuring a smoother user experience. --------- Co-authored-by: Fini <fini.yang@gmail.com>
This commit is contained in:
parent
bdf37908fd
commit
b05ebb944a
58 changed files with 22573 additions and 185 deletions
BIN
build/icon.icns
BIN
build/icon.icns
Binary file not shown.
BIN
build/icon.ico
BIN
build/icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 56 KiB |
BIN
build/icon.png
BIN
build/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 915 KiB After Width: | Height: | Size: 544 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 894 KiB After Width: | Height: | Size: 544 KiB |
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<ChatBody>(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<string, unknown> | undefined {
|
||||
const reasoning: Record<string, unknown> = {}
|
||||
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<T>(
|
||||
|
|
@ -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<string, unknown> = {
|
||||
|
|
@ -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<void>(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<void>(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`))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ConnectResult> {
|
|||
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<GroupedModel[]> {
|
||||
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<string>()
|
||||
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<ConnectResult> {
|
||||
serverLog.info('[connect-agent] connecting to Codex CLI...')
|
||||
|
|
@ -258,13 +326,13 @@ async function connectCodexCli(): Promise<ConnectResult> {
|
|||
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<ConnectResult> {
|
|||
}).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<ConnectResult> {
|
|||
// 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<ConnectResult> {
|
|||
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<ConnectResult> {
|
|||
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<ConnectResult> {
|
|||
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<ConnectResult> {
|
|||
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<string | undefined> {
|
|||
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<string | undefined> {
|
|||
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<string | undefined> {
|
|||
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'),
|
||||
|
|
|
|||
|
|
@ -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<GenerateBody>(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.' }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
39
server/opencode/client.ts
Normal file
39
server/opencode/client.ts
Normal file
|
|
@ -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 })
|
||||
}
|
||||
18
server/opencode/gen/client.gen.ts
Normal file
18
server/opencode/gen/client.gen.ts
Normal file
|
|
@ -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<T extends ClientOptions = ClientOptions2> = (
|
||||
override?: Config<ClientOptions & T>,
|
||||
) => Config<Required<ClientOptions> & T>
|
||||
|
||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }))
|
||||
285
server/opencode/gen/client/client.gen.ts
Normal file
285
server/opencode/gen/client/client.gen.ts
Normal file
|
|
@ -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<RequestInit, "body" | "headers"> & {
|
||||
body?: any
|
||||
headers: ReturnType<typeof mergeHeaders>
|
||||
}
|
||||
|
||||
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<Request, Response, unknown, ResolvedRequestOptions>()
|
||||
|
||||
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<HttpMethod>) => (options: RequestOptions) => request({ ...options, method })
|
||||
|
||||
const makeSseFn = (method: Uppercase<HttpMethod>) => 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<string, string>,
|
||||
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
|
||||
}
|
||||
25
server/opencode/gen/client/index.ts
Normal file
25
server/opencode/gen/client/index.ts
Normal file
|
|
@ -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"
|
||||
202
server/opencode/gen/client/types.gen.ts
Normal file
202
server/opencode/gen/client/types.gen.ts
Normal file
|
|
@ -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<T extends ClientOptions = ClientOptions>
|
||||
extends Omit<RequestInit, "body" | "headers" | "method">,
|
||||
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<TData>,
|
||||
"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<string, unknown>
|
||||
query?: Record<string, unknown>
|
||||
/**
|
||||
* Security mechanism(s) to use for the request.
|
||||
*/
|
||||
security?: ReadonlyArray<Auth>
|
||||
url: Url
|
||||
}
|
||||
|
||||
export interface ResolvedRequestOptions<
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
ThrowOnError extends boolean = boolean,
|
||||
Url extends string = string,
|
||||
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
||||
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<string, unknown>
|
||||
? TData[keyof TData]
|
||||
: TData
|
||||
: {
|
||||
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||
request: Request
|
||||
response: Response
|
||||
}
|
||||
>
|
||||
: Promise<
|
||||
TResponseStyle extends "data"
|
||||
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
|
||||
: (
|
||||
| {
|
||||
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||
error: undefined
|
||||
}
|
||||
| {
|
||||
data: undefined
|
||||
error: TError extends Record<string, unknown> ? 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<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
|
||||
|
||||
type SseFn = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||
) => Promise<ServerSentEventsResult<TData, TError>>
|
||||
|
||||
type RequestFn = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
|
||||
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">,
|
||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
|
||||
|
||||
type BuildUrlFn = <
|
||||
TData extends {
|
||||
body?: unknown
|
||||
path?: Record<string, unknown>
|
||||
query?: Record<string, unknown>
|
||||
url: string
|
||||
},
|
||||
>(
|
||||
options: TData & Options<TData>,
|
||||
) => string
|
||||
|
||||
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
|
||||
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T extends ClientOptions = ClientOptions> = (
|
||||
override?: Config<ClientOptions & T>,
|
||||
) => Config<Required<ClientOptions> & T>
|
||||
|
||||
export interface TDataShape {
|
||||
body?: unknown
|
||||
headers?: unknown
|
||||
path?: unknown
|
||||
query?: unknown
|
||||
url: string
|
||||
}
|
||||
|
||||
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>
|
||||
|
||||
export type Options<
|
||||
TData extends TDataShape = TDataShape,
|
||||
ThrowOnError extends boolean = boolean,
|
||||
TResponse = unknown,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
> = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> &
|
||||
([TData] extends [never] ? unknown : Omit<TData, "url">)
|
||||
289
server/opencode/gen/client/utils.gen.ts
Normal file
289
server/opencode/gen/client/utils.gen.ts
Normal file
|
|
@ -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 = <T = unknown>({ 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<string, unknown>,
|
||||
...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<Config["parseAs"], "auto"> => {
|
||||
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<RequestOptions, "auth" | "query"> & {
|
||||
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<Required<RequestOptions>, "security"> &
|
||||
Pick<RequestOptions, "auth" | "query"> & {
|
||||
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<Required<Config>["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<Err, Res, Req, Options> = (
|
||||
error: Err,
|
||||
response: Res,
|
||||
request: Req,
|
||||
options: Options,
|
||||
) => Err | Promise<Err>
|
||||
|
||||
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>
|
||||
|
||||
type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>
|
||||
|
||||
class Interceptors<Interceptor> {
|
||||
fns: Array<Interceptor | null> = []
|
||||
|
||||
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<Req, Res, Err, Options> {
|
||||
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>
|
||||
request: Interceptors<ReqInterceptor<Req, Options>>
|
||||
response: Interceptors<ResInterceptor<Res, Req, Options>>
|
||||
}
|
||||
|
||||
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<Req, Res, Err, Options> => ({
|
||||
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
||||
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
||||
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
||||
})
|
||||
|
||||
const defaultQuerySerializer = createQuerySerializer({
|
||||
allowReserved: false,
|
||||
array: {
|
||||
explode: true,
|
||||
style: "form",
|
||||
},
|
||||
object: {
|
||||
explode: true,
|
||||
style: "deepObject",
|
||||
},
|
||||
})
|
||||
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
||||
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
||||
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||
...jsonBodySerializer,
|
||||
headers: defaultHeaders,
|
||||
parseAs: "auto",
|
||||
querySerializer: defaultQuerySerializer,
|
||||
...override,
|
||||
})
|
||||
41
server/opencode/gen/core/auth.gen.ts
Normal file
41
server/opencode/gen/core/auth.gen.ts
Normal file
|
|
@ -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) | AuthToken,
|
||||
): Promise<string | undefined> => {
|
||||
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
|
||||
}
|
||||
82
server/opencode/gen/core/bodySerializer.gen.ts
Normal file
82
server/opencode/gen/core/bodySerializer.gen.ts
Normal file
|
|
@ -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, unknown>) => string
|
||||
|
||||
export type BodySerializer = (body: any) => any
|
||||
|
||||
type QuerySerializerOptionsObject = {
|
||||
allowReserved?: boolean
|
||||
array?: Partial<SerializerOptions<ArrayStyle>>
|
||||
object?: Partial<SerializerOptions<ObjectStyle>>
|
||||
}
|
||||
|
||||
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
||||
/**
|
||||
* Per-parameter serialization overrides. When provided, these settings
|
||||
* override the global array/object settings for specific parameter names.
|
||||
*/
|
||||
parameters?: Record<string, QuerySerializerOptionsObject>
|
||||
}
|
||||
|
||||
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: <T extends Record<string, any> | Array<Record<string, any>>>(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: <T>(body: T): string =>
|
||||
JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)),
|
||||
}
|
||||
|
||||
export const urlSearchParamsBodySerializer = {
|
||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(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()
|
||||
},
|
||||
}
|
||||
169
server/opencode/gen/core/params.gen.ts
Normal file
169
server/opencode/gen/core/params.gen.ts
Normal file
|
|
@ -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<Slot, "body">
|
||||
/**
|
||||
* 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<Slot, "body">
|
||||
/**
|
||||
* 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<Record<Slot, boolean>>
|
||||
args?: ReadonlyArray<Field>
|
||||
}
|
||||
|
||||
export type FieldsConfig = ReadonlyArray<Field | Fields>
|
||||
|
||||
const extraPrefixesMap: Record<string, Slot> = {
|
||||
$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<string, unknown>
|
||||
path: Record<string, unknown>
|
||||
query: Record<string, unknown>
|
||||
}
|
||||
|
||||
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<unknown>, 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<string, unknown>)[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<string, unknown>)[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<string, unknown>)[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<string, unknown>)[key] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stripEmptySlots(params)
|
||||
|
||||
return params
|
||||
}
|
||||
167
server/opencode/gen/core/pathSerializer.gen.ts
Normal file
167
server/opencode/gen/core/pathSerializer.gen.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
|
||||
|
||||
interface SerializePrimitiveOptions {
|
||||
allowReserved?: boolean
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface SerializerOptions<T> {
|
||||
/**
|
||||
* @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<ArraySeparatorStyle> & {
|
||||
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<ObjectSeparatorStyle> & {
|
||||
value: Record<string, unknown> | 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
|
||||
}
|
||||
111
server/opencode/gen/core/queryKeySerializer.gen.ts
Normal file
111
server/opencode/gen/core/queryKeySerializer.gen.ts
Normal file
|
|
@ -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<string, unknown> => {
|
||||
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<string, JsonValue> = {}
|
||||
|
||||
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
|
||||
}
|
||||
239
server/opencode/gen/core/serverSentEvents.gen.ts
Normal file
239
server/opencode/gen/core/serverSentEvents.gen.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Config } from "./types.gen"
|
||||
|
||||
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> &
|
||||
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
|
||||
/**
|
||||
* 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<Request>
|
||||
/**
|
||||
* 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<TData>) => 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<void>
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface StreamEvent<TData = unknown> {
|
||||
data: TData
|
||||
event?: string
|
||||
id?: string
|
||||
retry?: number
|
||||
}
|
||||
|
||||
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
|
||||
stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>
|
||||
}
|
||||
|
||||
export const createSseClient = <TData = unknown>({
|
||||
onRequest,
|
||||
onSseError,
|
||||
onSseEvent,
|
||||
responseTransformer,
|
||||
responseValidator,
|
||||
sseDefaultRetryDelay,
|
||||
sseMaxRetryAttempts,
|
||||
sseMaxRetryDelay,
|
||||
sseSleepFn,
|
||||
url,
|
||||
...options
|
||||
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
||||
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<string, string> | 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<string> = []
|
||||
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 }
|
||||
}
|
||||
86
server/opencode/gen/core/types.gen.ts
Normal file
86
server/opencode/gen/core/types.gen.ts
Normal file
|
|
@ -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<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, SseFn = never> = {
|
||||
/**
|
||||
* 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) | 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<string, string | number | boolean | (string | number | boolean)[] | null | undefined | unknown>
|
||||
/**
|
||||
* The request method.
|
||||
*
|
||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
||||
*/
|
||||
method?: Uppercase<HttpMethod>
|
||||
/**
|
||||
* 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<unknown>
|
||||
/**
|
||||
* 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<unknown>
|
||||
/**
|
||||
* 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<unknown>
|
||||
}
|
||||
|
||||
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||
? true
|
||||
: [T] extends [never | undefined]
|
||||
? [undefined] extends [T]
|
||||
? false
|
||||
: true
|
||||
: false
|
||||
|
||||
export type OmitNever<T extends Record<string, unknown>> = {
|
||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K]
|
||||
}
|
||||
137
server/opencode/gen/core/utils.gen.ts
Normal file
137
server/opencode/gen/core/utils.gen.ts
Normal file
|
|
@ -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<string, unknown>
|
||||
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<string, unknown>,
|
||||
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<string, unknown>
|
||||
query?: Record<string, unknown>
|
||||
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
|
||||
}
|
||||
4033
server/opencode/gen/sdk.gen.ts
Normal file
4033
server/opencode/gen/sdk.gen.ts
Normal file
File diff suppressed because it is too large
Load diff
5000
server/opencode/gen/types.gen.ts
Normal file
5000
server/opencode/gen/types.gen.ts
Normal file
File diff suppressed because it is too large
Load diff
21
server/opencode/index.ts
Normal file
21
server/opencode/index.ts
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
125
server/opencode/server.ts
Normal file
125
server/opencode/server.ts
Normal file
|
|
@ -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<string>((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()
|
||||
},
|
||||
}
|
||||
}
|
||||
39
server/opencode/v2/client.ts
Normal file
39
server/opencode/v2/client.ts
Normal file
|
|
@ -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 })
|
||||
}
|
||||
18
server/opencode/v2/gen/client.gen.ts
Normal file
18
server/opencode/v2/gen/client.gen.ts
Normal file
|
|
@ -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<T extends ClientOptions = ClientOptions2> = (
|
||||
override?: Config<ClientOptions & T>,
|
||||
) => Config<Required<ClientOptions> & T>
|
||||
|
||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }))
|
||||
285
server/opencode/v2/gen/client/client.gen.ts
Normal file
285
server/opencode/v2/gen/client/client.gen.ts
Normal file
|
|
@ -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<RequestInit, "body" | "headers"> & {
|
||||
body?: any
|
||||
headers: ReturnType<typeof mergeHeaders>
|
||||
}
|
||||
|
||||
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<Request, Response, unknown, ResolvedRequestOptions>()
|
||||
|
||||
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<HttpMethod>) => (options: RequestOptions) => request({ ...options, method })
|
||||
|
||||
const makeSseFn = (method: Uppercase<HttpMethod>) => 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<string, string>,
|
||||
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
|
||||
}
|
||||
25
server/opencode/v2/gen/client/index.ts
Normal file
25
server/opencode/v2/gen/client/index.ts
Normal file
|
|
@ -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"
|
||||
202
server/opencode/v2/gen/client/types.gen.ts
Normal file
202
server/opencode/v2/gen/client/types.gen.ts
Normal file
|
|
@ -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<T extends ClientOptions = ClientOptions>
|
||||
extends Omit<RequestInit, "body" | "headers" | "method">,
|
||||
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<TData>,
|
||||
"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<string, unknown>
|
||||
query?: Record<string, unknown>
|
||||
/**
|
||||
* Security mechanism(s) to use for the request.
|
||||
*/
|
||||
security?: ReadonlyArray<Auth>
|
||||
url: Url
|
||||
}
|
||||
|
||||
export interface ResolvedRequestOptions<
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
ThrowOnError extends boolean = boolean,
|
||||
Url extends string = string,
|
||||
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
||||
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<string, unknown>
|
||||
? TData[keyof TData]
|
||||
: TData
|
||||
: {
|
||||
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||
request: Request
|
||||
response: Response
|
||||
}
|
||||
>
|
||||
: Promise<
|
||||
TResponseStyle extends "data"
|
||||
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
|
||||
: (
|
||||
| {
|
||||
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||
error: undefined
|
||||
}
|
||||
| {
|
||||
data: undefined
|
||||
error: TError extends Record<string, unknown> ? 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<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
|
||||
|
||||
type SseFn = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||
) => Promise<ServerSentEventsResult<TData, TError>>
|
||||
|
||||
type RequestFn = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
|
||||
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">,
|
||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
|
||||
|
||||
type BuildUrlFn = <
|
||||
TData extends {
|
||||
body?: unknown
|
||||
path?: Record<string, unknown>
|
||||
query?: Record<string, unknown>
|
||||
url: string
|
||||
},
|
||||
>(
|
||||
options: TData & Options<TData>,
|
||||
) => string
|
||||
|
||||
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
|
||||
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T extends ClientOptions = ClientOptions> = (
|
||||
override?: Config<ClientOptions & T>,
|
||||
) => Config<Required<ClientOptions> & T>
|
||||
|
||||
export interface TDataShape {
|
||||
body?: unknown
|
||||
headers?: unknown
|
||||
path?: unknown
|
||||
query?: unknown
|
||||
url: string
|
||||
}
|
||||
|
||||
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>
|
||||
|
||||
export type Options<
|
||||
TData extends TDataShape = TDataShape,
|
||||
ThrowOnError extends boolean = boolean,
|
||||
TResponse = unknown,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
> = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> &
|
||||
([TData] extends [never] ? unknown : Omit<TData, "url">)
|
||||
289
server/opencode/v2/gen/client/utils.gen.ts
Normal file
289
server/opencode/v2/gen/client/utils.gen.ts
Normal file
|
|
@ -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 = <T = unknown>({ 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<string, unknown>,
|
||||
...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<Config["parseAs"], "auto"> => {
|
||||
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<RequestOptions, "auth" | "query"> & {
|
||||
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<Required<RequestOptions>, "security"> &
|
||||
Pick<RequestOptions, "auth" | "query"> & {
|
||||
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<Required<Config>["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<Err, Res, Req, Options> = (
|
||||
error: Err,
|
||||
response: Res,
|
||||
request: Req,
|
||||
options: Options,
|
||||
) => Err | Promise<Err>
|
||||
|
||||
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>
|
||||
|
||||
type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>
|
||||
|
||||
class Interceptors<Interceptor> {
|
||||
fns: Array<Interceptor | null> = []
|
||||
|
||||
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<Req, Res, Err, Options> {
|
||||
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>
|
||||
request: Interceptors<ReqInterceptor<Req, Options>>
|
||||
response: Interceptors<ResInterceptor<Res, Req, Options>>
|
||||
}
|
||||
|
||||
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<Req, Res, Err, Options> => ({
|
||||
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
||||
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
||||
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
||||
})
|
||||
|
||||
const defaultQuerySerializer = createQuerySerializer({
|
||||
allowReserved: false,
|
||||
array: {
|
||||
explode: true,
|
||||
style: "form",
|
||||
},
|
||||
object: {
|
||||
explode: true,
|
||||
style: "deepObject",
|
||||
},
|
||||
})
|
||||
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
||||
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
||||
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||
...jsonBodySerializer,
|
||||
headers: defaultHeaders,
|
||||
parseAs: "auto",
|
||||
querySerializer: defaultQuerySerializer,
|
||||
...override,
|
||||
})
|
||||
41
server/opencode/v2/gen/core/auth.gen.ts
Normal file
41
server/opencode/v2/gen/core/auth.gen.ts
Normal file
|
|
@ -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) | AuthToken,
|
||||
): Promise<string | undefined> => {
|
||||
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
|
||||
}
|
||||
82
server/opencode/v2/gen/core/bodySerializer.gen.ts
Normal file
82
server/opencode/v2/gen/core/bodySerializer.gen.ts
Normal file
|
|
@ -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, unknown>) => string
|
||||
|
||||
export type BodySerializer = (body: any) => any
|
||||
|
||||
type QuerySerializerOptionsObject = {
|
||||
allowReserved?: boolean
|
||||
array?: Partial<SerializerOptions<ArrayStyle>>
|
||||
object?: Partial<SerializerOptions<ObjectStyle>>
|
||||
}
|
||||
|
||||
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
||||
/**
|
||||
* Per-parameter serialization overrides. When provided, these settings
|
||||
* override the global array/object settings for specific parameter names.
|
||||
*/
|
||||
parameters?: Record<string, QuerySerializerOptionsObject>
|
||||
}
|
||||
|
||||
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: <T extends Record<string, any> | Array<Record<string, any>>>(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: <T>(body: T): string =>
|
||||
JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)),
|
||||
}
|
||||
|
||||
export const urlSearchParamsBodySerializer = {
|
||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(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()
|
||||
},
|
||||
}
|
||||
169
server/opencode/v2/gen/core/params.gen.ts
Normal file
169
server/opencode/v2/gen/core/params.gen.ts
Normal file
|
|
@ -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<Slot, "body">
|
||||
/**
|
||||
* 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<Slot, "body">
|
||||
/**
|
||||
* 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<Record<Slot, boolean>>
|
||||
args?: ReadonlyArray<Field>
|
||||
}
|
||||
|
||||
export type FieldsConfig = ReadonlyArray<Field | Fields>
|
||||
|
||||
const extraPrefixesMap: Record<string, Slot> = {
|
||||
$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<string, unknown>
|
||||
path: Record<string, unknown>
|
||||
query: Record<string, unknown>
|
||||
}
|
||||
|
||||
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<unknown>, 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<string, unknown>)[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<string, unknown>)[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<string, unknown>)[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<string, unknown>)[key] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stripEmptySlots(params)
|
||||
|
||||
return params
|
||||
}
|
||||
167
server/opencode/v2/gen/core/pathSerializer.gen.ts
Normal file
167
server/opencode/v2/gen/core/pathSerializer.gen.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
|
||||
|
||||
interface SerializePrimitiveOptions {
|
||||
allowReserved?: boolean
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface SerializerOptions<T> {
|
||||
/**
|
||||
* @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<ArraySeparatorStyle> & {
|
||||
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<ObjectSeparatorStyle> & {
|
||||
value: Record<string, unknown> | 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
|
||||
}
|
||||
111
server/opencode/v2/gen/core/queryKeySerializer.gen.ts
Normal file
111
server/opencode/v2/gen/core/queryKeySerializer.gen.ts
Normal file
|
|
@ -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<string, unknown> => {
|
||||
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<string, JsonValue> = {}
|
||||
|
||||
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
|
||||
}
|
||||
239
server/opencode/v2/gen/core/serverSentEvents.gen.ts
Normal file
239
server/opencode/v2/gen/core/serverSentEvents.gen.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Config } from "./types.gen.js"
|
||||
|
||||
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> &
|
||||
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
|
||||
/**
|
||||
* 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<Request>
|
||||
/**
|
||||
* 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<TData>) => 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<void>
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface StreamEvent<TData = unknown> {
|
||||
data: TData
|
||||
event?: string
|
||||
id?: string
|
||||
retry?: number
|
||||
}
|
||||
|
||||
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
|
||||
stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>
|
||||
}
|
||||
|
||||
export const createSseClient = <TData = unknown>({
|
||||
onRequest,
|
||||
onSseError,
|
||||
onSseEvent,
|
||||
responseTransformer,
|
||||
responseValidator,
|
||||
sseDefaultRetryDelay,
|
||||
sseMaxRetryAttempts,
|
||||
sseMaxRetryDelay,
|
||||
sseSleepFn,
|
||||
url,
|
||||
...options
|
||||
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
||||
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<string, string> | 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<string> = []
|
||||
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 }
|
||||
}
|
||||
86
server/opencode/v2/gen/core/types.gen.ts
Normal file
86
server/opencode/v2/gen/core/types.gen.ts
Normal file
|
|
@ -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<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, SseFn = never> = {
|
||||
/**
|
||||
* 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) | 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<string, string | number | boolean | (string | number | boolean)[] | null | undefined | unknown>
|
||||
/**
|
||||
* The request method.
|
||||
*
|
||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
||||
*/
|
||||
method?: Uppercase<HttpMethod>
|
||||
/**
|
||||
* 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<unknown>
|
||||
/**
|
||||
* 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<unknown>
|
||||
/**
|
||||
* 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<unknown>
|
||||
}
|
||||
|
||||
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||
? true
|
||||
: [T] extends [never | undefined]
|
||||
? [undefined] extends [T]
|
||||
? false
|
||||
: true
|
||||
: false
|
||||
|
||||
export type OmitNever<T extends Record<string, unknown>> = {
|
||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K]
|
||||
}
|
||||
137
server/opencode/v2/gen/core/utils.gen.ts
Normal file
137
server/opencode/v2/gen/core/utils.gen.ts
Normal file
|
|
@ -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<string, unknown>
|
||||
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<string, unknown>,
|
||||
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<string, unknown>
|
||||
query?: Record<string, unknown>
|
||||
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
|
||||
}
|
||||
4033
server/opencode/v2/gen/sdk.gen.ts
Normal file
4033
server/opencode/v2/gen/sdk.gen.ts
Normal file
File diff suppressed because it is too large
Load diff
5000
server/opencode/v2/gen/types.gen.ts
Normal file
5000
server/opencode/v2/gen/types.gen.ts
Normal file
File diff suppressed because it is too large
Load diff
21
server/opencode/v2/index.ts
Normal file
21
server/opencode/v2/index.ts
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
125
server/opencode/v2/server.ts
Normal file
125
server/opencode/v2/server.ts
Normal file
|
|
@ -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<string>((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()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, string | undefined>),
|
||||
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: "<timestamp> ERROR <module>: <message>"
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<string, string | undefined>; 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',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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 = '<step title="Checking guidelines">Analyzing modification request...</step>'
|
||||
updateLastMessage(accumulated)
|
||||
|
||||
const { rawResponse, nodes } = await generateDesignModification(selectedNodes, messageText, {
|
||||
const { rawResponse, nodes } = await generateDesignModification(modTargets, messageText, {
|
||||
variables: modDoc.variables,
|
||||
themes: modDoc.themes,
|
||||
model,
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null)
|
||||
const [warning, setWarning] = useState<string | null>(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 && (
|
||||
<span className="text-[11px] text-destructive leading-tight mt-0.5 block">{error}</span>
|
||||
)}
|
||||
{warning && !error && (
|
||||
<span className="text-[11px] text-amber-500 leading-tight mt-0.5 block">{warning}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action */}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue