9.0 KiB
name, description
| name | description |
|---|---|
| opencode-drive | Use when an agent needs drive OpenCode via a script or interact with an isolated instance |
OpenCode Drive
Use opencode-drive to launch an isolated OpenCode instance and control it via commands or a script.
There are two modes. Always default to using a script unless specifically directed to be interactive (connect to an existing running instance, or start a new one, and make a few changes to the UI and read it, and iterate on changes).
Scripts allow you to run a full walkthrough in one run. When the script is done opencode-drive exits, stops all processes, and cleans up all artifacts.
Prepare The Environment
Use init when files must be added to the isolated home or project before OpenCode starts. It prints the artifact directory without launching OpenCode. A later start with the same name reuses it.
artifacts=$(opencode-drive init --name demo)
cp -R ./fixtures/home/. "$artifacts/"
cp -R ./fixtures/project/. "$artifacts/files/"
opencode-drive start --name demo --dev ~/projects/opencode
The simulated project is under $artifacts/files. Running start without a prior init initializes the artifacts automatically.
Scripted usage
You can write scripts that walk through entire flows, and gives you full access to controlling the backend too. See examples of the script API at the bottom of this file.
After creating or editing a script, always typecheck it before running. Never skip this step:
opencode-drive check ./reproduce-stale-exploring-empty.ts
Run it by passing --script to start:
opencode-drive start --name auto-stop-reproduction --script ./reproduce-stale-exploring-empty.ts
It will output information about the run, including paths to log files which you can read to inspect what happened. If you need to dig into failures that aren't clear, read those log files. If the script is unsuccessful, automatically fix the script and run it again.
Scripts use one typed definition object. setup runs before OpenCode starts,
and fs.writeFile always writes inside the simulated project.
You can read the full typed API here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/src/script/types.ts
import { defineScript } from "opencode-drive"
export default defineScript({
async setup({ fs, config }) {
config.autoupdate = false
await fs.writeFile("src/example.ts", "export const value = 1\n")
},
async run({ ui, llm }) {
await ui.submit("Open src/example.ts")
await llm.send(llm.text("The file exports `value`."))
await ui.waitFor("The file exports `value`.")
},
})
setup receives the current OpenCode config object, which starts from the
default drive config unless the prepared instance already has one. When a script
needs custom config, mutate this config parameter instead of generating and
writing a new config object from scratch, so the script keeps the default
provider/model settings unless it intentionally changes them.
Note that the simulated model is a GPT model type, and opencode uses the patch tool for working with files Do not use a edit or write tool to edit files.
Use launch: "manual" when the script needs to launch the server and every TUI
itself (this is extremely rare, do not use this unless explicitly asked). In this
mode ui is typed as null; call server.launch() exactly
once before launching clients. Each clients.launch(name) result provides the
same UI methods as the automatic client. You can see an example of this API
here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/multiple-clients.ts
Use the exported wait(milliseconds) utility for an unconditional delay.
await llm.send(...) waits for the next request and resolves after OpenCode
acknowledges its complete response. llm.queue(...) declares responses in
advance. Chunks may be built with text, reasoning, toolCall, raw,
finish, and disconnect. A normal response receives finish("stop")
automatically unless it yields or queues an explicit terminal event.
llm.text(text, { delay, chunkSize }) defaults to a 2 ms delay and a
15-character target varied by plus or minus 5 per chunk.
llm.reasoning accepts the same options, and llm.pause(milliseconds) adds a
delay between any two outputs.
Use llm.serve for an ongoing typed response generator:
llm.serve(async function* (request, index) {
yield llm.reasoning(`Handling request ${index + 1}`)
yield llm.text(`Received ${request.id}`)
yield llm.finish("stop")
})
The backend connection, response cleanup, cancellation, and recording completion are automatic.
You can see some example scripts here:
- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/simple.ts
- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/serve.ts
Prune
pruneremoves artifact directories. These are always cleaned up after running a script successfully, but leftover on failed runs. Always call this if a script fails.
opencode-drive prune --name demo
// --force cleans up all artifcat directories
opencode-dirve prune --force
Live interaction usage
- Always give headless instances a unique
--name. Visible instances may omit it. - A normal headless
startdetaches automatically and returns after the instance is ready. - Do not add
&; the long-running owner already runs in the background. - Configure simulated model responses after startup when needed.
- Send ordered UI commands with
send. - Always stop the instance when finished.
opencode-drive start --name demo
opencode-drive send --name demo \
--command.ui.type '{"text":"Explain this project"}' \
--command.ui.enter
opencode-drive stop --name demo
Send UI Commands
- Every
sendopens a connection to the named instance, runs its commands in order, and exits. - Combine typing and Enter in one command when submitting a prompt.
- JSON-valued commands require one JSON argument.
- Multiple command flags execute from left to right.
Commands:
--command.ui.type <json>types into the focused editor. Arguments:textstring.--command.ui.press <json>presses a key. Arguments:keystring; optionalmodifiersobject with booleanctrl,shift,meta,super, orhyper.--command.ui.enterpresses Enter. Arguments: none.--command.ui.arrow <json>presses an arrow key. Arguments:directionisup,down,left, orright.--command.ui.focus <json>focuses an element. Arguments:targetis the numeric elementnumreturned byui.state.--command.ui.click <json>clicks an element. Arguments: numerictarget,x, andy; use the elementnumreturned byui.stateastarget.--command.ui.stateprints focus and interactive element metadata as JSON. Arguments: none.--command.ui.matches <json>prints whether literal, case-sensitive text appears on screen. Arguments:textstring.
opencode-drive send --name demo \
--command.ui.type '{"text":"Find the relevant code and explain it"}' \
--command.ui.enter
opencode-drive send --name demo \
--command.ui.press '{"key":"p","modifiers":{"ctrl":true}}'
opencode-drive send --name demo \
--command.ui.arrow '{"direction":"down"}'
opencode-drive send --name demo \
--command.ui.focus '{"target":12}'
opencode-drive send --name demo \
--command.ui.click '{"target":12,"x":4,"y":1}'
opencode-drive send --name demo \
--command.ui.matches '{"text":"OpenCode"}'
To read the UI state and see information about interactable elements, use the ui.state command:
opencode-drive send --name demo --command.ui.state
Configure LLM Responses
responsescontrols what the LLM responds with- Only use this if you are wanting to reproduce an exact type of response
- Defaults are
text,reasoning,diff,toolwithwrite,apply_patch. - Supported types are
text,reasoning,diff, andtool. --toolslimits generated tool calls to names offered by OpenCode.
opencode-drive responses --name demo \
--types text,reasoning,diff,tool \
--tools write,apply_patch
opencode-drive responses --name demo \
--types tool \
--tools read,glob,grep
Inspect The UI
ui.stateprints focus and interactive element metadata as JSON.ui.matcheschecks for literal, case-sensitive screen text.screenshotprints the generated image path.
opencode-drive screenshot --name demo
Lifecycle
stopwaits for recording export and owner cleanup before returning.
opencode-drive stop --name demo
Record The UI
- Start with
--recordto capture a headless instance from its first rendered frame. stopfinishes the recording, exports an MP4, and prints its path.
opencode-drive start --name demo --record
opencode-drive send --name demo \
--command.ui.type '{"text":"Show me the current architecture"}' \
--command.ui.enter
opencode-drive stop --name demo
Artifacts dir
dirprints the artifact directory for the instance.
opencode-drive dir --name demo