import { Service } from "@opencode-ai/client/effect/service" import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" import { ServerConnection } from "../services/server-connection" import { waitForCatalogReady } from "./catalog.shared" import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin" import type { RunInput, RunTuiConfig } from "./types" import { readStdin } from "../util/io" import { setTimeout } from "node:timers/promises" export type MiniCommandInput = { server: ServerConnection.Resolved continue?: boolean session?: string fork?: boolean model?: string agent?: string prompt?: string replay?: boolean replayLimit?: number demo?: boolean tuiConfig?: RunTuiConfig | Promise } type Session = Awaited> export async function runMini(input: MiniCommandInput) { validate(input) const initialInput = mergeInput(process.stdin.isTTY ? undefined : await readStdin(), input.prompt) const runtimeTask = import("./runtime") const directory = localDirectory() try { const sdk = OpenCode.make({ baseUrl: input.server.endpoint.url, headers: Service.headers(input.server.endpoint), }) const model = parseModel(input.model) let agentTask: Promise | undefined const resolveAgent = () => { agentTask ??= validateAgent(sdk, directory, input.agent) return agentTask } const resolveSession = async () => { const [agent, selected] = await Promise.all([resolveAgent(), selectSession(sdk, directory, input)]) const readyModel = model ?? (selected?.model ? { providerID: selected.model.providerID, modelID: selected.model.id } : undefined) if (readyModel) await waitForCatalogReady({ sdk, directory, model: readyModel }) const session = selected ?? (await createSession(sdk, directory, agent, model)) return { id: session.id, title: session.title, resume: selected !== undefined } } const create = ( _sdk: OpenCodeClient, next: { agent: string | undefined; model: RunInput["model"]; variant: string | undefined }, ) => createSession(sdk, directory, next.agent, next.model, next.variant) const runtime = await runtimeTask await runtime.runInteractiveDeferredMode({ sdk, directory, resolveAgent, session: resolveSession, createSession: create, agent: input.agent, model, variant: undefined, files: [], initialInput, thinking: true, replay: input.replay ?? true, replayLimit: input.replayLimit, demo: input.demo, tuiConfig: input.tuiConfig, }) } catch (error) { if (error instanceof Error && error.message === INTERACTIVE_INPUT_ERROR) fail(error.message) throw error } } export function validateMiniTerminal() { if (!process.stdout.isTTY) fail("opencode mini requires a TTY stdout") } /** @internal Exported for testing. */ export function mergeInput(piped: string | undefined, prompt: string | undefined) { if (!prompt) return piped || undefined if (!piped) return prompt return piped + "\n" + prompt } function validate(input: MiniCommandInput) { validateMiniTerminal() if (input.replayLimit !== undefined && (!Number.isInteger(input.replayLimit) || input.replayLimit <= 0)) { fail("--replay-limit must be a positive integer") } if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session") resolveInteractiveStdin().cleanup?.() } function localDirectory(): string { const root = process.env.PWD ?? process.cwd() try { process.chdir(root) return process.cwd() } catch { fail(`Failed to change directory to ${root}`) } } function parseModel(value?: string): RunInput["model"] { if (!value) return const [providerID, ...rest] = value.split("/") const modelID = rest.join("/") if (!providerID || !modelID) fail("--model must use the format provider/model") return { providerID, modelID } } async function validateAgent(sdk: OpenCodeClient, directory: string, name?: string) { if (!name) return const deadline = Date.now() + 5_000 let agents: Awaited> | undefined while (Date.now() < deadline) { agents = await sdk.agent.list({ location: { directory } }).catch(() => undefined) const agent = agents?.data.find((item) => item.id === name) if (agent?.mode === "subagent") { warning(`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`) return } if (agent) return name await setTimeout(25) } if (!agents) { warning("failed to list agents. Falling back to default agent") return } warning(`agent "${name}" not found. Falling back to default agent`) } async function selectSession(sdk: OpenCodeClient, directory: string, input: MiniCommandInput, preselected?: Session) { const selected = preselected ?? (input.session ? await sdk.session.get({ sessionID: input.session }).catch(() => undefined) : input.continue ? await sdk.session .list({ directory, parentID: null, limit: 1, order: "desc" }) .then((result) => result.data[0]) : undefined) if (input.session && !selected) fail("Session not found") if (!selected) return if (!input.fork) return selected return sdk.session.fork({ sessionID: selected.id }) } async function createSession( sdk: OpenCodeClient, directory: string, agent: string | undefined, model: RunInput["model"], variant?: string, ): Promise { if (model) await waitForCatalogReady({ sdk, directory, model }) return sdk.session.create({ agent, model: model ? { providerID: model.providerID, id: model.modelID, variant } : undefined, location: { directory }, }) } function warning(message: string) { process.stderr.write(`\x1b[93m\x1b[1m!\x1b[0m ${message}\n`) } function fail(message: string): never { process.stderr.write(`\x1b[91m\x1b[1mError: \x1b[0m${message}\n`) process.exit(1) }