diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 3927f615a080..2451242f3b47 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -24,6 +24,7 @@ import { EOL } from "os" import { Filesystem } from "@/util/filesystem" import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { FormatError, FormatUnknownError } from "../error" +import { createInterface } from "node:readline" import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./run/runtime.stdin" type ModelInput = Parameters[0]["model"] @@ -413,7 +414,8 @@ export const RunCommand = effectCmd({ } } - const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text() + // In --format json mode, keep stdin open for permission reply channel + const piped = (process.stdin.isTTY || args.format === "json") ? undefined : await Bun.stdin.text() message = resolveRunInput(message, piped) ?? "" const initialInput = resolveRunInput(rawMessage, piped) @@ -802,6 +804,20 @@ export const RunCommand = effectCmd({ requestID: permission.id, reply: "once", }) + } else if (args.format === "json") { + emit("permission_asked", { + permission: { + id: permission.id, + sessionID: permission.sessionID, + permission: permission.permission, + patterns: permission.patterns, + metadata: permission.metadata, + always: permission.always, + tool: permission.tool, + }, + }) + const reply = await readPermissionReply() + await client.permission.reply({ requestID: permission.id, reply }) } else { UI.println( UI.Style.TEXT_WARNING_BOLD + "!", @@ -817,6 +833,25 @@ export const RunCommand = effectCmd({ } return error } + + // Read a single JSON reply line from stdin, return the "reply" field. + // Used in --format json mode to receive permission responses. + // Times out after 120s; defaults to "reject" on timeout or parse failure. + async function readPermissionReply(): Promise { + return new Promise((resolve) => { + const rl = createInterface({ input: process.stdin }) + const timeout = setTimeout(() => { + rl.close() + resolve("reject") + }, 120000) + rl.on("line", (line: string) => { + if (!line.trim()) return + clearTimeout(timeout) + rl.close() + resolve(JSON.parse(line).reply || "reject") + }) + }).catch(() => "reject") + } const cwd = args.attach ? (directory ?? sess.directory ?? (await current(sdk))) : (directory ?? root) const client = args.attach ? attachSDK(cwd) : sdk