|
|
@@ -1,149 +1,12 @@
|
|
|
-import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
|
|
-import { FSUtil } from "@opencode-ai/core/fs-util"
|
|
|
-// CLI entry point for `opencode run` and `opencode mini`.
|
|
|
-//
|
|
|
-// Handles three modes:
|
|
|
-// 1. Non-interactive (default): sends a single prompt, streams events to
|
|
|
-// stdout, and exits when the session goes idle.
|
|
|
-// 2. Interactive local (`opencode mini`): boots the split-footer direct mode
|
|
|
-// with an in-process server (no external HTTP).
|
|
|
-// 3. Interactive attach (`opencode mini attach`): connects to a running
|
|
|
-// opencode server and runs interactive mode against it.
|
|
|
-//
|
|
|
-// Also supports `--command` for slash-command execution, `--format json` for
|
|
|
-// raw event streaming, `--continue` / `--session` for session resumption,
|
|
|
-// and `--fork` for forking before continuing.
|
|
|
-import type { Argv } from "yargs"
|
|
|
-import path from "path"
|
|
|
-import { pathToFileURL } from "url"
|
|
|
-import { open } from "node:fs/promises"
|
|
|
import { Effect } from "effect"
|
|
|
-import { UI } from "../ui"
|
|
|
import { effectCmd } from "../effect-cmd"
|
|
|
-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 { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./run/runtime.stdin"
|
|
|
-import { isImageAttachment, isPdfAttachment } from "@/util/media"
|
|
|
-import { loadRunAgents, waitForCatalogReady } from "./run/catalog.shared"
|
|
|
-
|
|
|
-type ModelInput = Parameters<OpencodeClient["session"]["prompt"]>[0]["model"]
|
|
|
-
|
|
|
-function pick(value: string | undefined): ModelInput | undefined {
|
|
|
- if (!value) return undefined
|
|
|
- const [providerID, ...rest] = value.split("/")
|
|
|
- return {
|
|
|
- providerID,
|
|
|
- modelID: rest.join("/"),
|
|
|
- } as ModelInput
|
|
|
-}
|
|
|
-
|
|
|
-function resolveRunInput(value?: string, piped?: string): string | undefined {
|
|
|
- if (!value) {
|
|
|
- return piped
|
|
|
- }
|
|
|
-
|
|
|
- if (!piped) {
|
|
|
- return value
|
|
|
- }
|
|
|
-
|
|
|
- return value + "\n" + piped
|
|
|
-}
|
|
|
-
|
|
|
-function isBinaryContent(bytes: Uint8Array) {
|
|
|
- if (bytes.length === 0) return false
|
|
|
- if (bytes.includes(0)) return true
|
|
|
- return (
|
|
|
- bytes.reduce((count, byte) => count + Number(byte < 9 || (byte > 13 && byte < 32)), 0) / bytes.length > 0.3
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-type FilePart = {
|
|
|
- type: "file"
|
|
|
- url: string
|
|
|
- filename: string
|
|
|
- mime: string
|
|
|
-}
|
|
|
-
|
|
|
-const ATTACH_FILE_MAX_BYTES = 10 * 1024 * 1024
|
|
|
-
|
|
|
-type Inline = {
|
|
|
- icon: string
|
|
|
- title: string
|
|
|
- description?: string
|
|
|
-}
|
|
|
-
|
|
|
-type SessionInfo = {
|
|
|
- id: string
|
|
|
- title?: string
|
|
|
- directory?: string
|
|
|
- current?: boolean
|
|
|
-}
|
|
|
-
|
|
|
-function inline(info: Inline) {
|
|
|
- const suffix = info.description ? UI.Style.TEXT_DIM + ` ${info.description}` + UI.Style.TEXT_NORMAL : ""
|
|
|
- UI.println(UI.Style.TEXT_NORMAL + info.icon, UI.Style.TEXT_NORMAL + info.title + suffix)
|
|
|
-}
|
|
|
-
|
|
|
-function block(info: Inline, output?: string) {
|
|
|
- UI.empty()
|
|
|
- inline(info)
|
|
|
- if (!output?.trim()) return
|
|
|
- UI.println(output)
|
|
|
- UI.empty()
|
|
|
-}
|
|
|
-
|
|
|
-function formatRunError(error: unknown) {
|
|
|
- return FormatError(error) ?? FormatUnknownError(error)
|
|
|
-}
|
|
|
-
|
|
|
-async function tool(part: ToolPart) {
|
|
|
- try {
|
|
|
- const { toolInlineInfo } = await import("./run/tool")
|
|
|
- const next = toolInlineInfo(part)
|
|
|
- if (next.mode === "block") {
|
|
|
- block(next, next.body)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- inline(next)
|
|
|
- } catch {
|
|
|
- inline({
|
|
|
- icon: "\u2699",
|
|
|
- title: part.tool,
|
|
|
- })
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function toolError(part: ToolPart) {
|
|
|
- try {
|
|
|
- const { toolInlineInfo } = await import("./run/tool")
|
|
|
- const next = toolInlineInfo(part)
|
|
|
- inline({
|
|
|
- icon: "✗",
|
|
|
- title: `${next.title} failed`,
|
|
|
- ...(next.description && { description: next.description }),
|
|
|
- })
|
|
|
- return
|
|
|
- } catch {
|
|
|
- inline({
|
|
|
- icon: "✗",
|
|
|
- title: `${part.tool} failed`,
|
|
|
- })
|
|
|
- }
|
|
|
-}
|
|
|
+import { v2ServerCommand } from "./v2-server-command"
|
|
|
|
|
|
export const RunCommand = effectCmd({
|
|
|
command: "run [message..]",
|
|
|
describe: "run opencode with a message",
|
|
|
- // --attach connects to a remote server (no local instance needed); the
|
|
|
- // default path runs an in-process server and needs the project instance.
|
|
|
- instance: (args) => !args.attach,
|
|
|
- // For --dir without --attach, load instance for the resolved target dir.
|
|
|
- // The handler also chdirs (preserving the legacy order: chdir → file resolution).
|
|
|
- directory: (args) => (args.dir && !args.attach ? path.resolve(process.cwd(), args.dir) : process.cwd()),
|
|
|
- builder: (yargs: Argv) =>
|
|
|
+ instance: false,
|
|
|
+ builder: (yargs) =>
|
|
|
yargs
|
|
|
.positional("message", {
|
|
|
describe: "message to send",
|
|
|
@@ -151,10 +14,6 @@ export const RunCommand = effectCmd({
|
|
|
array: true,
|
|
|
default: [],
|
|
|
})
|
|
|
- .option("command", {
|
|
|
- describe: "the command to run, use message for args",
|
|
|
- type: "string",
|
|
|
- })
|
|
|
.option("continue", {
|
|
|
alias: ["c"],
|
|
|
describe: "continue the last session",
|
|
|
@@ -180,8 +39,8 @@ export const RunCommand = effectCmd({
|
|
|
})
|
|
|
.option("format", {
|
|
|
type: "string",
|
|
|
- choices: ["default", "json"],
|
|
|
- default: "default",
|
|
|
+ choices: ["default", "json"] as const,
|
|
|
+ default: "default" as const,
|
|
|
describe: "format: default (formatted) or json (raw JSON events)",
|
|
|
})
|
|
|
.option("file", {
|
|
|
@@ -194,53 +53,36 @@ export const RunCommand = effectCmd({
|
|
|
type: "string",
|
|
|
describe: "title for the session (uses truncated prompt if no value provided)",
|
|
|
})
|
|
|
+ .option("server", {
|
|
|
+ type: "string",
|
|
|
+ describe: "connect to a running opencode server",
|
|
|
+ })
|
|
|
.option("attach", {
|
|
|
type: "string",
|
|
|
- describe: "attach to a running opencode server (e.g., http://localhost:4096)",
|
|
|
+ hidden: true,
|
|
|
})
|
|
|
.option("password", {
|
|
|
alias: ["p"],
|
|
|
type: "string",
|
|
|
- describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
|
|
|
+ hidden: true,
|
|
|
})
|
|
|
.option("username", {
|
|
|
alias: ["u"],
|
|
|
type: "string",
|
|
|
- describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
|
|
|
+ hidden: true,
|
|
|
})
|
|
|
.option("dir", {
|
|
|
type: "string",
|
|
|
- describe: "directory to run in, path on remote server if attaching",
|
|
|
- })
|
|
|
- .option("port", {
|
|
|
- type: "number",
|
|
|
- describe: "port for the local server (defaults to random port if no value provided)",
|
|
|
+ describe: "directory to run in, or a path on the remote server",
|
|
|
})
|
|
|
.option("variant", {
|
|
|
type: "string",
|
|
|
- describe: "model variant (provider-specific reasoning effort, e.g., high, max, minimal)",
|
|
|
+ describe: "model variant",
|
|
|
})
|
|
|
.option("thinking", {
|
|
|
type: "boolean",
|
|
|
describe: "show thinking blocks",
|
|
|
})
|
|
|
- .option("replay", {
|
|
|
- type: "boolean",
|
|
|
- default: true,
|
|
|
- hidden: true,
|
|
|
- describe: "replay interactive session history on resume and after resize (use --no-replay to disable)",
|
|
|
- })
|
|
|
- .option("replay-limit", {
|
|
|
- type: "number",
|
|
|
- hidden: true,
|
|
|
- describe: "cap visible interactive replay to the newest N messages",
|
|
|
- })
|
|
|
- .option("interactive", {
|
|
|
- alias: ["i"],
|
|
|
- type: "boolean",
|
|
|
- describe: "run in direct interactive split-footer mode",
|
|
|
- default: false,
|
|
|
- })
|
|
|
.option("auto", {
|
|
|
type: "boolean",
|
|
|
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
|
|
|
@@ -255,899 +97,29 @@ export const RunCommand = effectCmd({
|
|
|
type: "boolean",
|
|
|
hidden: true,
|
|
|
default: false,
|
|
|
- })
|
|
|
- .option("demo", {
|
|
|
- type: "boolean",
|
|
|
- default: false,
|
|
|
- hidden: true,
|
|
|
- describe: "enable direct interactive demo slash commands; pass one as the message to run it immediately",
|
|
|
}),
|
|
|
handler: Effect.fn("Cli.run")(function* (args) {
|
|
|
- const { Agent } = yield* Effect.promise(() => import("@/agent/agent"))
|
|
|
- const { RuntimeFlags } = yield* Effect.promise(() => import("@/effect/runtime-flags"))
|
|
|
- const { InstanceRef } = yield* Effect.promise(() => import("@/effect/instance-ref"))
|
|
|
- const { ServerAuth } = yield* Effect.promise(() => import("@/server/auth"))
|
|
|
- const agentSvc = yield* Agent.Service
|
|
|
- const flags = yield* RuntimeFlags.Service
|
|
|
- const localInstance = yield* InstanceRef
|
|
|
- yield* Effect.promise(async () => {
|
|
|
- const rawMessage = [...args.message, ...(args["--"] || [])].join(" ")
|
|
|
- const interactive = (args as typeof args & { mini?: boolean }).mini === true
|
|
|
- const auto = args.auto || args.yolo || args["dangerously-skip-permissions"]
|
|
|
- const thinking = interactive ? (args.thinking ?? true) : (args.thinking ?? false)
|
|
|
- const die = (message: string): never => {
|
|
|
- UI.error(message)
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
- const dieInteractive = (error: unknown): never => {
|
|
|
- if (error instanceof Error && error.message === INTERACTIVE_INPUT_ERROR) {
|
|
|
- die(error.message)
|
|
|
- }
|
|
|
-
|
|
|
- throw error
|
|
|
- }
|
|
|
-
|
|
|
- let message = [...args.message, ...(args["--"] || [])]
|
|
|
- .map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
|
|
- .join(" ")
|
|
|
-
|
|
|
- if (interactive && args.command) {
|
|
|
- die("opencode mini cannot be used with --command")
|
|
|
- }
|
|
|
-
|
|
|
- if (interactive && args._?.[0] !== "mini") {
|
|
|
- die("opencode mini must be run with the mini command")
|
|
|
- }
|
|
|
-
|
|
|
- if (args.demo && !interactive) {
|
|
|
- die("--demo requires opencode mini")
|
|
|
- }
|
|
|
-
|
|
|
- if (interactive && args.format === "json") {
|
|
|
- die("opencode mini cannot be used with --format json")
|
|
|
- }
|
|
|
-
|
|
|
- if (args["replay-limit"] !== undefined && !interactive) {
|
|
|
- die("--replay-limit requires opencode mini")
|
|
|
- }
|
|
|
-
|
|
|
- if (
|
|
|
- args["replay-limit"] !== undefined &&
|
|
|
- (!Number.isInteger(args["replay-limit"]) || args["replay-limit"] <= 0)
|
|
|
- ) {
|
|
|
- die("--replay-limit must be a positive integer")
|
|
|
- }
|
|
|
-
|
|
|
- if (interactive && !process.stdout.isTTY) {
|
|
|
- die("opencode mini requires a TTY stdout")
|
|
|
- }
|
|
|
-
|
|
|
- if (interactive) {
|
|
|
- try {
|
|
|
- resolveInteractiveStdin().cleanup?.()
|
|
|
- } catch (error) {
|
|
|
- dieInteractive(error)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const replay = args.replay === false ? false : args.replay || args["replay-limit"] !== undefined
|
|
|
-
|
|
|
- const root = Filesystem.resolve(process.env.PWD ?? process.cwd())
|
|
|
- const directory = (() => {
|
|
|
- if (!args.dir) return args.attach ? undefined : root
|
|
|
- if (args.attach) return args.dir
|
|
|
-
|
|
|
- try {
|
|
|
- process.chdir(path.isAbsolute(args.dir) ? args.dir : path.join(root, args.dir))
|
|
|
- return process.cwd()
|
|
|
- } catch {
|
|
|
- UI.error("Failed to change directory to " + args.dir)
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
- })()
|
|
|
- const attachHeaders = args.attach
|
|
|
- ? ServerAuth.headers({ password: args.password, username: args.username })
|
|
|
- : undefined
|
|
|
- const attachSDK = (dir?: string) => {
|
|
|
- return createOpencodeClient({
|
|
|
- baseUrl: args.attach!,
|
|
|
- directory: dir,
|
|
|
- headers: attachHeaders,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- const files: FilePart[] = []
|
|
|
- const fileInputs: Array<{
|
|
|
- filePath: string
|
|
|
- resolvedPath: string
|
|
|
- stat: ReturnType<typeof Filesystem.stat>
|
|
|
- isDirectory: boolean
|
|
|
- }> = []
|
|
|
- if (args.file) {
|
|
|
- const list = Array.isArray(args.file) ? args.file : [args.file]
|
|
|
-
|
|
|
- for (const filePath of list) {
|
|
|
- const resolvedPath = path.resolve(args.attach ? root : (directory ?? root), filePath)
|
|
|
- if (!(await Filesystem.exists(resolvedPath))) {
|
|
|
- UI.error(`File not found: ${filePath}`)
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
-
|
|
|
- const stat = Filesystem.stat(resolvedPath)
|
|
|
- const isDirectory = stat?.isDirectory() ?? false
|
|
|
- if (args.attach && isDirectory) {
|
|
|
- UI.error(`Cannot attach local directory without a shared filesystem: ${filePath}`)
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
- fileInputs.push({ filePath, resolvedPath, stat, isDirectory })
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text()
|
|
|
- message = resolveRunInput(message, piped) ?? ""
|
|
|
- const initialInput = resolveRunInput(rawMessage, piped)
|
|
|
-
|
|
|
- if (message.trim().length === 0 && !args.command && !interactive) {
|
|
|
- UI.error("You must provide a message or a command")
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
-
|
|
|
- if (args.fork && !args.continue && !args.session) {
|
|
|
- UI.error("--fork requires --continue or --session")
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
-
|
|
|
- const rules: PermissionV1.Ruleset = interactive
|
|
|
- ? []
|
|
|
- : [
|
|
|
- {
|
|
|
- permission: "question",
|
|
|
- action: "deny",
|
|
|
- pattern: "*",
|
|
|
- },
|
|
|
- {
|
|
|
- permission: "plan_enter",
|
|
|
- action: "deny",
|
|
|
- pattern: "*",
|
|
|
- },
|
|
|
- {
|
|
|
- permission: "plan_exit",
|
|
|
- action: "deny",
|
|
|
- pattern: "*",
|
|
|
- },
|
|
|
- ]
|
|
|
- const currentPrompt = !interactive && !args.command && fileInputs.every((file) => !file.isDirectory)
|
|
|
-
|
|
|
- const inlineFiles = interactive || currentPrompt
|
|
|
- for (const file of fileInputs) {
|
|
|
- const content = await (async () => {
|
|
|
- if (file.isDirectory || !inlineFiles) return
|
|
|
- if (!file.stat?.isFile() || file.stat.size > ATTACH_FILE_MAX_BYTES) {
|
|
|
- UI.error(`Cannot attach local file larger than 10 MiB or a special file: ${file.filePath}`)
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
- const handle = await open(file.resolvedPath, "r")
|
|
|
- try {
|
|
|
- const opened = await handle.stat()
|
|
|
- if (!opened.isFile() || Number(opened.size) > ATTACH_FILE_MAX_BYTES) {
|
|
|
- UI.error(`Cannot attach local file larger than 10 MiB or a special file: ${file.filePath}`)
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
- if (opened.size === 0) return Buffer.alloc(0)
|
|
|
- const buffer = Buffer.alloc(Number(opened.size))
|
|
|
- let offset = 0
|
|
|
- while (offset < buffer.length) {
|
|
|
- const read = await handle.read(buffer, offset, buffer.length - offset, offset)
|
|
|
- if (read.bytesRead === 0) break
|
|
|
- offset += read.bytesRead
|
|
|
- }
|
|
|
- return buffer.subarray(0, offset)
|
|
|
- } finally {
|
|
|
- await handle.close()
|
|
|
- }
|
|
|
- })()
|
|
|
- const detected = FSUtil.mimeType(file.resolvedPath)
|
|
|
- const text = content?.toString("utf8")
|
|
|
- const mime = file.isDirectory
|
|
|
- ? "application/x-directory"
|
|
|
- : isImageAttachment(detected) || isPdfAttachment(detected)
|
|
|
- ? detected
|
|
|
- : content && !isBinaryContent(content) && text !== undefined && Buffer.from(text, "utf8").equals(content)
|
|
|
- ? "text/plain"
|
|
|
- : detected
|
|
|
-
|
|
|
- files.push({
|
|
|
- type: "file",
|
|
|
- url: content ? `data:${mime};base64,${content.toString("base64")}` : pathToFileURL(file.resolvedPath).href,
|
|
|
- filename: path.basename(file.resolvedPath),
|
|
|
- mime,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- function title() {
|
|
|
- if (args.title === undefined) return
|
|
|
- if (args.title !== "") return args.title
|
|
|
- return message.slice(0, 50) + (message.length > 50 ? "..." : "")
|
|
|
- }
|
|
|
-
|
|
|
- async function currentSession(sdk: OpencodeClient, sessionID: string): Promise<SessionInfo | undefined> {
|
|
|
- const listed = await sdk.v2.session
|
|
|
- .list({
|
|
|
- directory: await current(sdk),
|
|
|
- limit: 50,
|
|
|
- order: "desc",
|
|
|
- })
|
|
|
- .then((result) => result.data?.data.find((item) => item.id === sessionID))
|
|
|
- .catch(() => undefined)
|
|
|
- const selected =
|
|
|
- listed ??
|
|
|
- (await sdk.v2.session
|
|
|
- .get({ sessionID })
|
|
|
- .then((result) => result.data?.data)
|
|
|
- .catch(() => undefined))
|
|
|
- const legacy =
|
|
|
- selected ??
|
|
|
- (await sdk.session
|
|
|
- .get({ sessionID })
|
|
|
- .then((result) => result.data)
|
|
|
- .catch(() => undefined))
|
|
|
- const transcript = await transcriptKind(sdk, legacy?.id ?? sessionID)
|
|
|
- if (!legacy && transcript === "empty") {
|
|
|
- return
|
|
|
- }
|
|
|
- if (interactive && transcript === "legacy") {
|
|
|
- throw new Error("Mini cannot resume a legacy Session transcript")
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- id: legacy?.id ?? sessionID,
|
|
|
- title: legacy?.title,
|
|
|
- directory: legacy ? ("location" in legacy ? legacy.location.directory : legacy.directory) : await current(sdk),
|
|
|
- current: transcript !== "legacy",
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function forkSession(sdk: OpencodeClient, session: SessionInfo): Promise<SessionInfo | undefined> {
|
|
|
- if (session.current !== false) {
|
|
|
- const forked = await sdk.v2.session.fork(
|
|
|
- { sessionID: session.id, messageID: undefined },
|
|
|
- { throwOnError: true },
|
|
|
- )
|
|
|
- await waitForFork(sdk, session.id, forked.data.data.id)
|
|
|
- return {
|
|
|
- id: forked.data.data.id,
|
|
|
- title: forked.data.data.title,
|
|
|
- directory: forked.data.data.location.directory,
|
|
|
- current: true,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const forked = await sdk.session.fork({
|
|
|
- sessionID: session.id,
|
|
|
- })
|
|
|
- const id = forked.data?.id
|
|
|
- if (!id) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- id,
|
|
|
- title: forked.data?.title ?? session.title,
|
|
|
- directory: forked.data?.directory ?? session.directory,
|
|
|
- current: false,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function waitForFork(sdk: OpencodeClient, parentID: string, sessionID: string) {
|
|
|
- const parentHasMessages = await sdk.v2.session
|
|
|
- .messages({ sessionID: parentID, limit: 1 })
|
|
|
- .then((result) => (result.data?.data.length ?? 0) > 0)
|
|
|
- .catch(() => false)
|
|
|
- if (!parentHasMessages) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const deadline = Date.now() + 3000
|
|
|
- while (Date.now() < deadline) {
|
|
|
- const forkedHasMessages = await sdk.v2.session
|
|
|
- .messages({ sessionID, limit: 1 })
|
|
|
- .then((result) => (result.data?.data.length ?? 0) > 0)
|
|
|
- .catch(() => false)
|
|
|
- if (forkedHasMessages) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- await Bun.sleep(25)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function session(sdk: OpencodeClient): Promise<SessionInfo | undefined> {
|
|
|
- if (args.session) {
|
|
|
- const current = await currentSession(sdk, args.session)
|
|
|
- if (!current) {
|
|
|
- UI.error("Session not found")
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
- if (!interactive && !currentPrompt && current.current !== false) {
|
|
|
- throw new Error("This operation is not available for a current Session transcript")
|
|
|
- }
|
|
|
-
|
|
|
- if (args.fork) {
|
|
|
- return forkSession(sdk, current)
|
|
|
- }
|
|
|
-
|
|
|
- return current
|
|
|
- }
|
|
|
-
|
|
|
- const base = args.continue ? await currentRootSession(sdk) : undefined
|
|
|
- if (base && !interactive && !currentPrompt && base.current !== false) {
|
|
|
- throw new Error("This operation is not available for a current Session transcript")
|
|
|
- }
|
|
|
-
|
|
|
- if (base && args.fork) {
|
|
|
- return forkSession(sdk, base)
|
|
|
- }
|
|
|
-
|
|
|
- if (base) {
|
|
|
- return {
|
|
|
- id: base.id,
|
|
|
- title: base.title,
|
|
|
- directory: base.directory,
|
|
|
- current: "current" in base ? base.current : false,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (interactive || currentPrompt) {
|
|
|
- const name = title()
|
|
|
- const result = await sdk.v2.session.create({
|
|
|
- location: { directory: await current(sdk) },
|
|
|
- })
|
|
|
- const created = result.data?.data
|
|
|
- if (!created) return
|
|
|
- if (name) await sdk.v2.session.rename({ sessionID: created.id, title: name })
|
|
|
- return {
|
|
|
- id: created.id,
|
|
|
- title: name ?? created.title,
|
|
|
- directory: created.location.directory,
|
|
|
- current: true,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const name = title()
|
|
|
- const result = await sdk.session.create({
|
|
|
- title: name,
|
|
|
- permission: [...rules],
|
|
|
- })
|
|
|
- const id = result.data?.id
|
|
|
- if (!id) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- id,
|
|
|
- title: result.data?.title ?? name,
|
|
|
- directory: result.data?.directory,
|
|
|
- current: false,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function currentRootSession(sdk: OpencodeClient): Promise<SessionInfo | undefined> {
|
|
|
- const response = await sdk.v2.session.list({
|
|
|
- directory: await current(sdk),
|
|
|
- limit: 50,
|
|
|
- order: "desc",
|
|
|
- })
|
|
|
- const root = (response.data?.data ?? [])
|
|
|
- .filter((session) => !session.parentID)
|
|
|
- .toSorted((a, b) => b.time.updated - a.time.updated)[0]
|
|
|
- if (!root) return
|
|
|
- return currentSession(sdk, root.id)
|
|
|
- }
|
|
|
-
|
|
|
- async function transcriptKind(sdk: OpencodeClient, sessionID: string) {
|
|
|
- const current = await sdk.v2.session.messages({ sessionID, limit: 1 }).then((result) => (result.data?.data.length ?? 0) > 0)
|
|
|
- // Ordinary prompt flows assume a transcript with current messages is
|
|
|
- // current-owned; only legacy-only modes (--command, directory
|
|
|
- // attachments) still probe legacy history for mixed transcripts.
|
|
|
- if (current && (interactive || currentPrompt)) return "current" as const
|
|
|
-
|
|
|
- const legacy = await sdk.session.messages({ sessionID, limit: 1 }).then((result) => (result.data?.length ?? 0) > 0)
|
|
|
- if (current) {
|
|
|
- if (legacy) throw new Error("Session contains mixed legacy and current transcripts")
|
|
|
- return "current" as const
|
|
|
- }
|
|
|
- if (legacy) return "legacy" as const
|
|
|
- return "empty" as const
|
|
|
- }
|
|
|
-
|
|
|
- async function createFreshSession(
|
|
|
- sdk: OpencodeClient,
|
|
|
- input: { agent: string | undefined; model: ModelInput | undefined; variant: string | undefined },
|
|
|
- ): Promise<SessionInfo> {
|
|
|
- const name = args.title !== undefined && args.title !== "" ? args.title : undefined
|
|
|
- const result = await sdk.v2.session.create({
|
|
|
- agent: input.agent,
|
|
|
- model: input.model
|
|
|
- ? {
|
|
|
- providerID: input.model.providerID,
|
|
|
- id: input.model.modelID,
|
|
|
- variant: input.variant,
|
|
|
- }
|
|
|
- : undefined,
|
|
|
- location: { directory: await current(sdk) },
|
|
|
- })
|
|
|
- const created = result.data?.data
|
|
|
- const id = created?.id
|
|
|
- if (!id) {
|
|
|
- throw new Error("Failed to create session")
|
|
|
- }
|
|
|
- if (name) await sdk.v2.session.rename({ sessionID: id, title: name })
|
|
|
-
|
|
|
- return {
|
|
|
- id,
|
|
|
- title: name ?? created.title,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function current(sdk: OpencodeClient): Promise<string> {
|
|
|
- if (!args.attach) {
|
|
|
- return directory ?? root
|
|
|
- }
|
|
|
-
|
|
|
- const next = await sdk.v2.location
|
|
|
- .get(undefined, { throwOnError: true })
|
|
|
- .then((x) => x.data?.directory)
|
|
|
- .catch(() => undefined)
|
|
|
- if (next) {
|
|
|
- return next
|
|
|
- }
|
|
|
-
|
|
|
- UI.error("Failed to resolve remote directory")
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
-
|
|
|
- async function localAgent() {
|
|
|
- if (!args.agent) return undefined
|
|
|
- const name = args.agent
|
|
|
-
|
|
|
- const entry = await Effect.runPromise(
|
|
|
- agentSvc.get(name).pipe(Effect.provideService(InstanceRef, localInstance)),
|
|
|
- )
|
|
|
- if (!entry) {
|
|
|
- UI.println(
|
|
|
- UI.Style.TEXT_WARNING_BOLD + "!",
|
|
|
- UI.Style.TEXT_NORMAL,
|
|
|
- `agent "${name}" not found. Falling back to default agent`,
|
|
|
- )
|
|
|
- return undefined
|
|
|
- }
|
|
|
- if (entry.mode === "subagent") {
|
|
|
- UI.println(
|
|
|
- UI.Style.TEXT_WARNING_BOLD + "!",
|
|
|
- UI.Style.TEXT_NORMAL,
|
|
|
- `agent "${name}" is a subagent, not a primary agent. Falling back to default agent`,
|
|
|
- )
|
|
|
- return undefined
|
|
|
- }
|
|
|
- return name
|
|
|
- }
|
|
|
-
|
|
|
- async function attachAgent(sdk: OpencodeClient) {
|
|
|
- if (!args.agent) return undefined
|
|
|
- const name = args.agent
|
|
|
-
|
|
|
- const modes = await loadRunAgents(sdk, await current(sdk)).catch(() => undefined)
|
|
|
-
|
|
|
- if (!modes) {
|
|
|
- UI.println(
|
|
|
- UI.Style.TEXT_WARNING_BOLD + "!",
|
|
|
- UI.Style.TEXT_NORMAL,
|
|
|
- `failed to list agents from ${args.attach}. Falling back to default agent`,
|
|
|
- )
|
|
|
- return undefined
|
|
|
- }
|
|
|
-
|
|
|
- const agent = modes.find((item) => item.name === name)
|
|
|
- if (!agent) {
|
|
|
- UI.println(
|
|
|
- UI.Style.TEXT_WARNING_BOLD + "!",
|
|
|
- UI.Style.TEXT_NORMAL,
|
|
|
- `agent "${name}" not found. Falling back to default agent`,
|
|
|
- )
|
|
|
- return undefined
|
|
|
- }
|
|
|
-
|
|
|
- if (agent.mode === "subagent") {
|
|
|
- UI.println(
|
|
|
- UI.Style.TEXT_WARNING_BOLD + "!",
|
|
|
- UI.Style.TEXT_NORMAL,
|
|
|
- `agent "${name}" is a subagent, not a primary agent. Falling back to default agent`,
|
|
|
- )
|
|
|
- return undefined
|
|
|
- }
|
|
|
-
|
|
|
- return name
|
|
|
- }
|
|
|
-
|
|
|
- async function pickAgent(sdk: OpencodeClient) {
|
|
|
- if (!args.agent) return undefined
|
|
|
- if (args.attach) {
|
|
|
- return attachAgent(sdk)
|
|
|
- }
|
|
|
-
|
|
|
- return localAgent()
|
|
|
- }
|
|
|
-
|
|
|
- async function execute(sdk: OpencodeClient) {
|
|
|
- const sess = await session(sdk)
|
|
|
- if (!sess?.id) {
|
|
|
- UI.error("Session not found")
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
- const sessionID = sess.id
|
|
|
-
|
|
|
- function emit(type: string, data: Record<string, unknown>) {
|
|
|
- if (args.format === "json") {
|
|
|
- process.stdout.write(
|
|
|
- JSON.stringify({
|
|
|
- type,
|
|
|
- timestamp: Date.now(),
|
|
|
- sessionID,
|
|
|
- ...data,
|
|
|
- }) + EOL,
|
|
|
- )
|
|
|
- return true
|
|
|
- }
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- // Consume one subscribed event stream for the active session and mirror it
|
|
|
- // to stdout/UI. `client` is passed explicitly because attach mode may
|
|
|
- // rebind the SDK to the session's directory after the subscription is
|
|
|
- // created, and replies issued from inside the loop must use that client.
|
|
|
- async function loop(client: OpencodeClient, events: Awaited<ReturnType<typeof sdk.event.subscribe>>) {
|
|
|
- const toggles = new Map<string, boolean>()
|
|
|
- let error: string | undefined
|
|
|
-
|
|
|
- for await (const event of events.stream) {
|
|
|
- if (
|
|
|
- event.type === "message.updated" &&
|
|
|
- event.properties.sessionID === sessionID &&
|
|
|
- event.properties.info.role === "assistant" &&
|
|
|
- args.format !== "json" &&
|
|
|
- toggles.get("start") !== true
|
|
|
- ) {
|
|
|
- UI.empty()
|
|
|
- UI.println(`> ${event.properties.info.agent} · ${event.properties.info.modelID}`)
|
|
|
- UI.empty()
|
|
|
- toggles.set("start", true)
|
|
|
- }
|
|
|
-
|
|
|
- if (event.type === "message.part.updated") {
|
|
|
- const part = event.properties.part
|
|
|
- if (part.sessionID !== sessionID) continue
|
|
|
-
|
|
|
- if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) {
|
|
|
- if (emit("tool_use", { part })) continue
|
|
|
- if (part.state.status === "completed") {
|
|
|
- await tool(part)
|
|
|
- continue
|
|
|
- }
|
|
|
- await toolError(part)
|
|
|
- UI.error(part.state.error)
|
|
|
- }
|
|
|
-
|
|
|
- if (
|
|
|
- part.type === "tool" &&
|
|
|
- part.tool === "task" &&
|
|
|
- part.state.status === "running" &&
|
|
|
- args.format !== "json"
|
|
|
- ) {
|
|
|
- if (toggles.get(part.id) === true) continue
|
|
|
- await tool(part)
|
|
|
- toggles.set(part.id, true)
|
|
|
- }
|
|
|
-
|
|
|
- if (part.type === "step-start") {
|
|
|
- if (emit("step_start", { part })) continue
|
|
|
- }
|
|
|
-
|
|
|
- if (part.type === "step-finish") {
|
|
|
- if (emit("step_finish", { part })) continue
|
|
|
- }
|
|
|
-
|
|
|
- if (part.type === "text" && part.time?.end) {
|
|
|
- if (emit("text", { part })) continue
|
|
|
- const text = part.text.trim()
|
|
|
- if (!text) continue
|
|
|
- if (!process.stdout.isTTY) {
|
|
|
- process.stdout.write(text + EOL)
|
|
|
- continue
|
|
|
- }
|
|
|
- UI.empty()
|
|
|
- UI.println(text)
|
|
|
- UI.empty()
|
|
|
- }
|
|
|
-
|
|
|
- if (part.type === "reasoning" && part.time?.end && thinking) {
|
|
|
- if (emit("reasoning", { part })) continue
|
|
|
- const text = part.text.trim()
|
|
|
- if (!text) continue
|
|
|
- const line = `Thinking: ${text}`
|
|
|
- if (process.stdout.isTTY) {
|
|
|
- UI.empty()
|
|
|
- UI.println(`${UI.Style.TEXT_DIM}\u001b[3m${line}\u001b[0m${UI.Style.TEXT_NORMAL}`)
|
|
|
- UI.empty()
|
|
|
- continue
|
|
|
- }
|
|
|
- process.stdout.write(line + EOL)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (event.type === "session.error") {
|
|
|
- const props = event.properties
|
|
|
- if (props.sessionID !== sessionID || !props.error) continue
|
|
|
- let err = String(props.error.name)
|
|
|
- if ("data" in props.error && props.error.data && "message" in props.error.data) {
|
|
|
- err = String(props.error.data.message)
|
|
|
- }
|
|
|
- error = error ? error + EOL + err : err
|
|
|
- if (emit("error", { error: props.error })) continue
|
|
|
- UI.error(err)
|
|
|
- }
|
|
|
-
|
|
|
- if (
|
|
|
- event.type === "session.status" &&
|
|
|
- event.properties.sessionID === sessionID &&
|
|
|
- event.properties.status.type === "idle"
|
|
|
- ) {
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- if (event.type === "permission.asked") {
|
|
|
- const permission = event.properties
|
|
|
- if (permission.sessionID !== sessionID) continue
|
|
|
-
|
|
|
- if (auto) {
|
|
|
- await client.permission.reply({
|
|
|
- requestID: permission.id,
|
|
|
- reply: "once",
|
|
|
- })
|
|
|
- } else {
|
|
|
- UI.println(
|
|
|
- UI.Style.TEXT_WARNING_BOLD + "!",
|
|
|
- UI.Style.TEXT_NORMAL +
|
|
|
- `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
|
|
|
- )
|
|
|
- await client.permission.reply({
|
|
|
- requestID: permission.id,
|
|
|
- reply: "reject",
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return error
|
|
|
- }
|
|
|
- const cwd = args.attach ? (directory ?? sess.directory ?? (await current(sdk))) : (directory ?? root)
|
|
|
- const client = args.attach ? attachSDK(cwd) : sdk
|
|
|
-
|
|
|
- // Current-session flows resolve explicit --model refs from a location
|
|
|
- // catalog that populates asynchronously after boot. Wait only when the
|
|
|
- // user supplied a model; default-model races.
|
|
|
- const selectedModel = pick(args.model)
|
|
|
- if (selectedModel && (interactive || (currentPrompt && sess.current !== false))) {
|
|
|
- await waitForCatalogReady({ sdk: client, directory: cwd, model: selectedModel })
|
|
|
- }
|
|
|
-
|
|
|
- // Validate agent if specified
|
|
|
- const agent = await pickAgent(client)
|
|
|
-
|
|
|
- if (!interactive) {
|
|
|
- if (currentPrompt && sess.current !== false) {
|
|
|
- const model = selectedModel
|
|
|
- const { runNonInteractivePrompt } = await import("./run/noninteractive")
|
|
|
- try {
|
|
|
- await runNonInteractivePrompt({
|
|
|
- client,
|
|
|
- sessionID,
|
|
|
- message,
|
|
|
- files,
|
|
|
- agent,
|
|
|
- model,
|
|
|
- variant: args.variant,
|
|
|
- thinking,
|
|
|
- format: args.format === "json" ? "json" : "default",
|
|
|
- dangerouslySkipPermissions: args["dangerously-skip-permissions"],
|
|
|
- attached: Boolean(args.attach),
|
|
|
- renderTool: tool,
|
|
|
- renderToolError: toolError,
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- const output = error instanceof Error ? { type: "unknown", message: error.message } : error
|
|
|
- if (!emit("error", { error: output })) UI.error(formatRunError(error))
|
|
|
- process.exitCode = 1
|
|
|
- }
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const events = await client.event.subscribe()
|
|
|
- const completed = loop(client, events).catch((e) => {
|
|
|
- console.error(e)
|
|
|
- process.exitCode = 1
|
|
|
- })
|
|
|
- async function finish() {
|
|
|
- if (args.attach) return
|
|
|
- const error = await completed
|
|
|
- if (error) process.exitCode = 1
|
|
|
- }
|
|
|
-
|
|
|
- if (args.command) {
|
|
|
- const result = await client.session.command({
|
|
|
- sessionID,
|
|
|
- agent,
|
|
|
- model: args.model,
|
|
|
- command: args.command,
|
|
|
- arguments: message,
|
|
|
- variant: args.variant,
|
|
|
- })
|
|
|
- if (result.error) {
|
|
|
- if (!emit("error", { error: result.error })) UI.error(formatRunError(result.error))
|
|
|
- process.exitCode = 1
|
|
|
- return
|
|
|
- }
|
|
|
- await finish()
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const model = pick(args.model)
|
|
|
- const result = await client.session.prompt({
|
|
|
- sessionID,
|
|
|
- agent,
|
|
|
- model,
|
|
|
- variant: args.variant,
|
|
|
- parts: [...files, { type: "text", text: message }],
|
|
|
- })
|
|
|
- if (result.error) {
|
|
|
- if (!emit("error", { error: result.error })) UI.error(formatRunError(result.error))
|
|
|
- process.exitCode = 1
|
|
|
- return
|
|
|
- }
|
|
|
- await finish()
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const model = pick(args.model)
|
|
|
- const { runInteractiveMode } = await import("./run/runtime")
|
|
|
- try {
|
|
|
- await runInteractiveMode({
|
|
|
- sdk: client,
|
|
|
- directory: cwd,
|
|
|
- sessionID,
|
|
|
- sessionTitle: sess.title,
|
|
|
- resume: Boolean(args.session || args.continue),
|
|
|
- replay,
|
|
|
- replayLimit: args["replay-limit"],
|
|
|
- agent,
|
|
|
- model,
|
|
|
- variant: args.variant,
|
|
|
- files,
|
|
|
- initialInput,
|
|
|
- createSession: createFreshSession,
|
|
|
- thinking,
|
|
|
- backgroundSubagents: flags.experimentalBackgroundSubagents,
|
|
|
- demo: args.demo,
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- dieInteractive(error)
|
|
|
- }
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (interactive && !args.attach && !args.session && !args.continue) {
|
|
|
- const model = pick(args.model)
|
|
|
- const { runInteractiveLocalMode } = await import("./run/runtime")
|
|
|
- const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
|
- const { Server } = await import("@/server/server")
|
|
|
- const request = new Request(input, init)
|
|
|
- const headers = new Headers(request.headers)
|
|
|
- const auth = ServerAuth.header()
|
|
|
- if (auth) headers.set("Authorization", auth)
|
|
|
- return Server.Default().app.fetch(new Request(request, { headers }))
|
|
|
- }) as typeof globalThis.fetch
|
|
|
-
|
|
|
- try {
|
|
|
- return await runInteractiveLocalMode({
|
|
|
- directory: directory ?? root,
|
|
|
- fetch: fetchFn,
|
|
|
- resolveAgent: localAgent,
|
|
|
- session,
|
|
|
- createSession: createFreshSession,
|
|
|
- agent: args.agent,
|
|
|
- model,
|
|
|
- variant: args.variant,
|
|
|
- replay,
|
|
|
- replayLimit: args["replay-limit"],
|
|
|
- files,
|
|
|
- initialInput,
|
|
|
- thinking,
|
|
|
- backgroundSubagents: flags.experimentalBackgroundSubagents,
|
|
|
- demo: args.demo,
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- dieInteractive(error)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (args.attach) {
|
|
|
- const sdk = attachSDK(directory)
|
|
|
- return await execute(sdk)
|
|
|
- }
|
|
|
-
|
|
|
- const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
|
- const { Server } = await import("@/server/server")
|
|
|
- const request = new Request(input, init)
|
|
|
- const headers = new Headers(request.headers)
|
|
|
- const auth = ServerAuth.header()
|
|
|
- if (auth) headers.set("Authorization", auth)
|
|
|
- return Server.Default().app.fetch(new Request(request, { headers }))
|
|
|
- }) as typeof globalThis.fetch
|
|
|
- const sdk = createOpencodeClient({
|
|
|
- baseUrl: "http://opencode.internal",
|
|
|
- fetch: fetchFn,
|
|
|
- directory,
|
|
|
- })
|
|
|
- await execute(sdk)
|
|
|
- })
|
|
|
+ const { runNonInteractive } = yield* Effect.promise(() => import("@opencode-ai/cli/mini"))
|
|
|
+ yield* Effect.promise(() =>
|
|
|
+ runNonInteractive({
|
|
|
+ message: [...args.message, ...(args["--"] || [])],
|
|
|
+ continue: args.continue,
|
|
|
+ session: args.session,
|
|
|
+ fork: args.fork,
|
|
|
+ model: args.model,
|
|
|
+ agent: args.agent,
|
|
|
+ format: args.format,
|
|
|
+ file: args.file ?? [],
|
|
|
+ title: args.title,
|
|
|
+ server: args.server ?? args.attach,
|
|
|
+ password: args.password ?? process.env.OPENCODE_PASSWORD ?? process.env.OPENCODE_SERVER_PASSWORD,
|
|
|
+ username: args.username ?? process.env.OPENCODE_SERVER_USERNAME,
|
|
|
+ directory: args.dir,
|
|
|
+ variant: args.variant,
|
|
|
+ thinking: args.thinking,
|
|
|
+ dangerouslySkipPermissions: args.auto || args.yolo || args["dangerously-skip-permissions"],
|
|
|
+ standaloneCommand: args.server || args.attach ? undefined : v2ServerCommand(),
|
|
|
+ }),
|
|
|
+ )
|
|
|
}),
|
|
|
})
|
|
|
-
|
|
|
-type MiniCommandInput = {
|
|
|
- directory?: string
|
|
|
- attach?: string
|
|
|
- password?: string
|
|
|
- username?: string
|
|
|
- continue?: boolean
|
|
|
- session?: string
|
|
|
- fork?: boolean
|
|
|
- model?: string
|
|
|
- agent?: string
|
|
|
- prompt?: string
|
|
|
- replay?: boolean
|
|
|
- replayLimit?: number
|
|
|
- demo?: boolean
|
|
|
-}
|
|
|
-
|
|
|
-export async function runMini(input: MiniCommandInput) {
|
|
|
- if (!RunCommand.handler) throw new Error("Mini command handler is unavailable")
|
|
|
- await RunCommand.handler({
|
|
|
- $0: "opencode",
|
|
|
- _: ["mini"],
|
|
|
- message: input.prompt ? [input.prompt] : [],
|
|
|
- command: undefined,
|
|
|
- continue: input.continue,
|
|
|
- session: input.session,
|
|
|
- fork: input.fork,
|
|
|
- model: input.model,
|
|
|
- agent: input.agent,
|
|
|
- format: "default",
|
|
|
- file: undefined,
|
|
|
- title: undefined,
|
|
|
- attach: input.attach,
|
|
|
- password: input.password,
|
|
|
- username: input.username,
|
|
|
- dir: input.directory,
|
|
|
- port: undefined,
|
|
|
- variant: undefined,
|
|
|
- thinking: undefined,
|
|
|
- mini: true,
|
|
|
- interactive: false,
|
|
|
- replay: input.replay ?? true,
|
|
|
- "replay-limit": input.replayLimit,
|
|
|
- replayLimit: input.replayLimit,
|
|
|
- auto: false,
|
|
|
- yolo: false,
|
|
|
- "dangerously-skip-permissions": false,
|
|
|
- dangerouslySkipPermissions: false,
|
|
|
- demo: input.demo ?? false,
|
|
|
- } as Parameters<NonNullable<typeof RunCommand.handler>>[0] & { mini: boolean })
|
|
|
-}
|