一键导入
opencode-drive
Use when an agent needs to drive OpenCode with an Effect program or interact with an isolated instance
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when an agent needs to drive OpenCode with an Effect program or interact with an isolated instance
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | opencode-drive |
| description | Use when an agent needs to drive OpenCode with an Effect program or interact with an isolated instance |
Use opencode-drive to launch an isolated OpenCode instance and control its TUI and simulated LLM.
Default to a one-shot Effect program. Use defineScript for named, visible,
restartable, or manual-launch workflows; it is also Effect-only. Use live
commands only for interactive development against a persistent or visible
instance.
Browse and copy OpenCode terminal state IDs from:
https://catalog.kitlangton.dev
Replayable flow states expose canonical <flow-id>/<state-id> addresses, for example:
patch-success-lifecycle/permission-prompt
Reproduce one from an opencode-drive source checkout:
bun run catalog:reproduce -- patch-success-lifecycle/permission-prompt \
--opencode /path/to/opencode \
--output /tmp/permission-prompt.frame.json
The command executes the registered recipe only through that checkpoint and writes an opencode-terminal-frame-v1 artifact. Only flows in apps/catalog/scenarios/index.ts are replayable. Browse-only flows and screen cards copy standalone capture IDs instead; do not invent a flow prefix. Use a protocol-compatible OpenCode checkout, ideally the source revision shown by the selected capture set.
To compare a committed local OpenCode branch against current v2 across every catalog state:
bun run catalog:capture -- \
--opencode /path/to/opencode \
--revision origin/v2 \
--revision HEAD
Add repeated --theme flags to capture the same commit pair under multiple themes. Capture resolves detached immutable worktrees, retains earlier sets, and sorts sets by commit time. Uncommitted changes are excluded by design.
Write drive.ts as a default-exported, fully provided Effect, then run it directly:
import { Effect } from "effect"
import { Llm, OpenCodeDriver } from "opencode-drive"
export default OpenCodeDriver.use(
{
project: {
git: true,
files: {
"src/value.ts": "export const value = 1\n",
},
},
},
({ ui, llm }) =>
Effect.gen(function* () {
yield* llm.queue(Llm.text("The value is 1."))
yield* ui.submit("Read src/value.ts")
yield* ui.waitFor("The value is 1.")
yield* ui.screenshot("result")
}),
)
opencode-drive run ./drive.ts
run type-checks the module before importing it and requires its default export to be an Effect<unknown, unknown, never>. It accepts exactly one module path; it does not accept --command.* flags or application arguments after --.
OpenCodeDriver.use is the normal lifecycle boundary. It creates an isolated project, starts the server and primary TUI, races the program against backend failure, settles queued LLM work, closes all TUIs, exports recordings, and removes the artifact directory unless keepArtifacts: true is set. Settlement failures fail the program.
Use OpenCodeDriver.make only when explicit settlement is necessary. It requires a scope, and the program must call driver.settle() before leaving that scope.
Declare the project, semantic OpenCode configuration, and TUI configuration in OpenCodeDriver.use options:
export default OpenCodeDriver.use(
{
project: {
git: true,
files: { "README.md": "# Fixture\n" },
},
config: {
autoupdate: false,
username: "Drive",
},
tuiConfig: {
theme: "system",
scroll_speed: 1,
},
setup: ({ fs, config, tuiConfig }) =>
Effect.gen(function* () {
yield* fs.writeFile("src/setup.ts", "export const ready = true\n")
config.username = "Setup wins"
tuiConfig.scroll_speed = 2
}),
},
({ ui }) => ui.screenshot("home"),
)
The DSL is applied in this order:
project.files is written into the isolated project.config and tuiConfig are deeply merged over .opencode/opencode.jsonc and .opencode/tui.jsonc fixture values. Objects merge recursively; arrays and scalar values replace existing values.setup runs and may write project files or mutate the merged config and tuiConfig objects. Its mutations take final precedence.project.git: true, it creates a repository and commits the complete pre-launch state with fixed Git identity and timestamps.fs.writeFile is rooted inside the simulated project and creates parent directories. project.git: true refuses to replace existing Git metadata; omit it when prepared fixtures already include a repository.
UI operations are Effects:
ui.submit(text) types and presses Enter.ui.state(), ui.capture(), and ui.matches(text) inspect the terminal.ui.snapshot() returns the versioned semantic tree; ui.getNode(query, options?) polls for one exact semantic match.ui.waitFor(textOrPredicate, options?) polls until a match.ui.getElement(query, options?), ui.focus(...), and ui.click(...) target interactive elements.ui.screenshot(name?) exports an image and returns its absolute path.ui.resize({ cols, rows }), ui.press(...), and ui.arrow(...) control the TUI.Build deterministic simulated responses with the Llm namespace and schedule them through the driver's llm controller:
yield* llm.queue(
Llm.reasoning("Checking the fixture"),
Llm.pause(20),
Llm.text("The value is 1.", { delay: 2, chunkSize: 15 }),
)
llm.queue(...) declares the next response without waiting. llm.send(...)
waits for the next request and completes its response. For ongoing responses,
the handler passed to llm.serve returns an Effect Stream; registering the
handler is an Effect. Available outputs include text, reasoning, pause,
toolCall, raw, finish, and disconnect; a normal response gets
finish("stop") when no terminal output is supplied.
import { Stream } from "effect"
import { Llm } from "opencode-drive"
yield* llm.serve((_request, index) =>
Stream.make(Llm.text(`Response ${index + 1}`)),
)
Use the capability names literally: opencode is the generated OpenCode SDK,
tui is the primary frontend process, ui is tui.ui, and tuis launches
additional frontend processes.
Additional TUIs share the server and LLM controller:
const secondary = yield* tuis.launch({
viewport: { cols: 120, rows: 40 },
recording: true,
})
yield* secondary.ui.screenshot("secondary")
OpenCodeDriver exports a branded AbsolutePath schema and a compact RunReport containing the artifact root, retention, recording paths, and endpoint compatibility. It also exports decodeAbsolutePath and decodeRunReport for validating unknown values.
driver.settle() returns the report with its recording paths. Use OpenCodeDriver.useReport(options, run) when a safe lifecycle program also needs the report alongside its result.
Drive prefers protocol negotiation and reports explicit legacy fallback. Set opencode.compatibility to "required" when protocol skew must fail before the program runs. Additional built-in tool adapters remain follow-ups.
Use runtime tools.attach for tools that do not have a shipped Drive adapter.
Attachment replaces the complete dynamic set without affecting configured
static adapters. Take invocations by the model call ID, while Drive owns
producer IDs, progress sequences, reconnect replay, and exactly-one terminal
commitment.
yield* tools.attach({
tools: [
{
name: "lookup",
description: "Look up a value",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
options: { codemode: false },
},
],
})
const lookup = yield* tools.take("call_lookup")
yield* lookup.progress({ structured: { phase: "searching" } })
yield* lookup.finish({
structured: { answer: 42 },
content: [{ type: "text", text: "42" }],
})
Use awaitCancelled() to observe native interruption. Do not synthesize
cancellation or expose transport sequence numbers. Dynamic effective names may
not collide with configured shell, webfetch, or websearch adapters.
Declare the built-in tools Drive should intercept, then control each invocation
from the running program. Unregistered tools remain real. Calls may be accepted
in arrival order or by the stable ID supplied in Llm.toolCall, so parallel
invocations can progress and settle independently.
import { Effect } from "effect"
import { Llm, OpenCodeDriver } from "opencode-drive"
export default OpenCodeDriver.use({ tools: ["shell"] }, ({ tools, llm, ui }) =>
Effect.gen(function* () {
const shells = yield* tools.control("shell")
yield* llm.queue(
Llm.toolCall({
index: 0,
id: "call_shell",
name: "shell",
input: { command: "compile" },
}),
Llm.finish("tool-calls"),
)
yield* ui.submit("Compile the project")
const shell = yield* shells.take("call_shell")
yield* shell.progress(`Running ${shell.input.command}\n`)
yield* shell.succeed({ output: "Controlled success\n", exit: 0 })
}),
)
The same declaration and runtime tools capability are available in
defineScript. Supported adapters are shell, webfetch, and websearch.
Each progress value replaces the visible tool output, so send accumulated text
when earlier lines should remain visible. Calls settle exactly once;
awaitInterrupted() observes session interruption or transport disconnection.
The callback form remains available for fixed behavior that does not need runtime orchestration:
import { Effect } from "effect"
import { Tool } from "opencode-drive"
const tools = (registry: Tool.Registry) => {
registry.handle("shell", ({ input, progress }) =>
Effect.gen(function* () {
yield* progress(`Running ${input.command}\n`)
return { output: "Controlled success\n", exit: 0 }
}),
)
}
Foreground callback handler Effects are interrupted when OpenCode interrupts the session, the transport disconnects, or Drive shuts down. Detached background shell handlers continue after their launch response and are interrupted when Drive shuts down.
Use defineScript with start --script when the workflow must have a stable
instance name, be visible, rerun on restart, or explicitly launch and kill
its server and TUIs. setup and run return Effects. Operations on fs,
ui, llm, tools, server, and tuis also return Effects; there is no
Promise API or compatibility shim.
import { Effect } from "effect"
import { defineScript, Llm } from "opencode-drive"
export default defineScript({
config: { autoupdate: false },
tuiConfig: { theme: "system" },
project: {
git: true,
files: { "src/value.ts": "export const value = 1\n" },
},
run: ({ ui, llm }) =>
Effect.gen(function* () {
yield* llm.queue(Llm.text("The value is 1."))
yield* ui.submit("Read src/value.ts")
yield* ui.waitFor("The value is 1.")
}),
})
Always type-check a script before starting it:
opencode-drive check ./drive.ts
opencode-drive start --name demo --script ./drive.ts
For a new script, run opencode-drive script init ./drive.ts once. It creates a
canonical Effect-native starter and refuses to overwrite an existing file.
check adds focused migration guidance when it finds Promise-style script
callbacks.
The script DSL applies project, config, tuiConfig, and setup with the same deterministic ordering described above. Automatic scripts run again after opencode-drive restart --name demo.
Use launch: "manual" only when the workflow must control server and TUI restarts itself. In manual mode tui and ui are null; run server.launch() before tuis.launch(name). Only one server may run at a time, server.kill() permits relaunch, and a closed TUI name may be reused.
export default defineScript({
launch: "manual",
run: ({ server, tuis }) =>
Effect.gen(function* () {
yield* server.launch()
const alice = yield* tuis.launch("alice", { recording: true })
yield* alice.ui.screenshot("alice")
yield* alice.close()
}),
})
Cancellation uses Effect interruption. Interrupting the script or an
operation's fiber interrupts in-flight work and runs scoped finalizers; do not
introduce AbortSignal or Promise cancellation wrappers.
Use init only when files must be copied into an isolated home or project before a named live or scripted instance starts:
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. A later start --name demo reuses the prepared artifacts; otherwise start initializes them automatically.
Drive uses an in-memory OpenCode database by default. For a script that restarts
the OpenCode service and must recover the same sessions, set
OPENCODE_DRIVE_DB to a file-backed path. Relative paths resolve inside the
isolated run's OpenCode data directory:
OPENCODE_DRIVE_DB=restart.sqlite \
opencode-drive start --name restart-demo --script ./restart.ts
Use live commands to inspect or iterate on a persistent instance. Headless start requires a unique --name; visible instances may omit it. Headless start detaches after the instance is ready, so do not add &. 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 send --name demo --command.ui.state
opencode-drive send --name demo --command.ui.capture
opencode-drive send --name demo --command.ui.screenshot
opencode-drive stop --name demo
send executes command flags from left to right. JSON-valued commands take one JSON argument. Supported commands are:
--command.ui.type '{"text":"..."}'--command.ui.press '{"key":"p","modifiers":{"ctrl":true}}'--command.ui.enter--command.ui.arrow '{"direction":"down"}'--command.ui.focus '{"target":12}'--command.ui.click '{"target":12,"x":4,"y":1}'--command.ui.resize '{"cols":120,"rows":40}'--command.ui.screenshot or --command.ui.screenshot '{"name":"home"}'--command.ui.state--command.ui.snapshot--command.ui.capture--command.ui.matches '{"text":"OpenCode"}'--command.ui.recording.finishStart with --record to record a headless live instance. stop finishes the recording, exports the MP4, performs owner cleanup, and prints the path.
opencode-drive start --name demo --record
opencode-drive stop --name demo
dir prints a live instance's artifact directory, and list lists active instances:
opencode-drive dir --name demo
opencode-drive list
prune removes inactive artifact directories. To remove one instance's artifacts, pass the instance name supplied to init or start, not the generated run-* artifact directory name:
opencode-drive prune --name demo
# Force removal of all artifact directories, including active ones.
opencode-drive prune --force