diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index c29733999214..f287f01a2391 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1870,7 +1870,157 @@ NOTE: At any point in time through this workflow you should feel free to ask the export type CommandInput = z.infer export async function command(input: CommandInput) { - return runPromise((svc) => svc.command(CommandInput.parse(input))) + log.info("command", input) + const command = await Command.get(input.command) + const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent()) + + const raw = input.arguments.match(argsRegex) ?? [] + const args = raw.map((arg) => arg.replace(quoteTrimRegex, "")) + + const templateCommand = await command.template + + const placeholders = templateCommand.match(placeholderRegex) ?? [] + let last = 0 + for (const item of placeholders) { + const value = Number(item.slice(1)) + if (value > last) last = value + } + + // Let the final placeholder swallow any extra arguments so prompts read naturally + const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => { + const position = Number(index) + const argIndex = position - 1 + if (argIndex >= args.length) return "" + if (position === last) return args.slice(argIndex).join(" ") + return args[argIndex] + }) + const usesArgumentsPlaceholder = templateCommand.includes("$ARGUMENTS") + let template = withArgs.replaceAll("$ARGUMENTS", input.arguments) + + // If command doesn't explicitly handle arguments (no $N or $ARGUMENTS placeholders) + // but user provided arguments, append them to the template + if (placeholders.length === 0 && !usesArgumentsPlaceholder && input.arguments.trim()) { + template = template + "\n\n" + input.arguments + } + + const shell = ConfigMarkdown.shell(template) + if (shell.length > 0) { + const results = await Promise.all( + shell.map(async ([, cmd]) => { + try { + return await $`${{ raw: cmd }}`.quiet().nothrow().text() + } catch (error) { + return `Error executing command: ${error instanceof Error ? error.message : String(error)}` + } + }), + ) + let index = 0 + template = template.replace(bashRegex, () => results[index++]) + } + template = template.trim() + + const agent = await Agent.get(agentName) + const isSubtask = (agent?.mode === "subagent" && command.subtask !== false) || command.subtask === true + + const taskModel = await (async () => { + if (isSubtask) { + if (command.model) return Provider.parseModel(command.model) + if (command.agent) { + const m = command.agent === agentName + ? agent?.model + : (await Agent.get(command.agent))?.model + if (m) return m + } + if (input.model) return Provider.parseModel(input.model) + return await lastModel(input.sessionID) + } + if (input.model) return Provider.parseModel(input.model) + if (command.model) return Provider.parseModel(command.model) + if (command.agent) { + const m = command.agent === agentName + ? agent?.model + : (await Agent.get(command.agent))?.model + if (m) return m + } + return await lastModel(input.sessionID) + })() + + try { + await Provider.getModel(taskModel.providerID, taskModel.modelID) + } catch (e) { + if (Provider.ModelNotFoundError.isInstance(e)) { + const { providerID, modelID, suggestions } = e.data + const hint = suggestions?.length ? ` Did you mean: ${suggestions.join(", ")}?` : "" + Bus.publish(Session.Event.Error, { + sessionID: input.sessionID, + error: new NamedError.Unknown({ message: `Model not found: ${providerID}/${modelID}.${hint}` }).toObject(), + }) + } + throw e + } + if (!agent) { + const available = await Agent.list().then((agents) => agents.filter((a) => !a.hidden).map((a) => a.name)) + const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" + const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` }) + Bus.publish(Session.Event.Error, { + sessionID: input.sessionID, + error: error.toObject(), + }) + throw error + } + + const templateParts = await resolvePromptParts(template) + const parts = isSubtask + ? [ + { + type: "subtask" as const, + agent: agent.name, + description: command.description ?? "", + command: input.command, + model: { + providerID: taskModel.providerID, + modelID: taskModel.modelID, + }, + // TODO: how can we make task tool accept a more complex input? + prompt: templateParts.find((y) => y.type === "text")?.text ?? "", + }, + ] + : [...templateParts, ...(input.parts ?? [])] + + const userAgent = isSubtask ? (input.agent ?? (await Agent.defaultAgent())) : agentName + const userModel = isSubtask + ? input.model + ? Provider.parseModel(input.model) + : await lastModel(input.sessionID) + : taskModel + + await Plugin.trigger( + "command.execute.before", + { + command: input.command, + sessionID: input.sessionID, + arguments: input.arguments, + }, + { parts }, + ) + + const result = (await prompt({ + sessionID: input.sessionID, + messageID: input.messageID, + model: userModel, + agent: userAgent, + parts, + variant: input.variant, + })) as MessageV2.WithParts + + Bus.publish(Command.Event.Executed, { + name: input.command, + sessionID: input.sessionID, + arguments: input.arguments, + messageID: result.info.id, + }) + + return result } /** @internal Exported for testing */