mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-28 21:24:19 +01:00
slash commands (#2157)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { Config } from "../src/config/config"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
|
||||
const file = process.argv[2]
|
||||
console.log(file)
|
||||
|
||||
const result = zodToJsonSchema(Config.Info, {
|
||||
/**
|
||||
|
||||
44
packages/opencode/src/command/index.ts
Normal file
44
packages/opencode/src/command/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import z from "zod"
|
||||
import { App } from "../app/app"
|
||||
import { Config } from "../config/config"
|
||||
|
||||
export namespace Command {
|
||||
export const Info = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
template: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Command",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
const state = App.state("command", async () => {
|
||||
const cfg = await Config.get()
|
||||
|
||||
const result: Record<string, Info> = {}
|
||||
|
||||
for (const [name, command] of Object.entries(cfg.command ?? {})) {
|
||||
result[name] = {
|
||||
name,
|
||||
agent: command.agent,
|
||||
model: command.model,
|
||||
description: command.description,
|
||||
template: command.template,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
export async function get(name: string) {
|
||||
return state().then((x) => x[name])
|
||||
}
|
||||
|
||||
export async function list() {
|
||||
return state().then((x) => Object.values(x))
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,32 @@ export namespace Config {
|
||||
}
|
||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||
}
|
||||
|
||||
// Load command markdown files
|
||||
result.command = result.command || {}
|
||||
const markdownCommands = [
|
||||
...(await Filesystem.globUp("command/*.md", Global.Path.config, Global.Path.config)),
|
||||
...(await Filesystem.globUp(".opencode/command/*.md", app.path.cwd, app.path.root)),
|
||||
]
|
||||
for (const item of markdownCommands) {
|
||||
const content = await Bun.file(item).text()
|
||||
const md = matter(content)
|
||||
if (!md.data) continue
|
||||
|
||||
const config = {
|
||||
name: path.basename(item, ".md"),
|
||||
...md.data,
|
||||
template: md.content.trim(),
|
||||
}
|
||||
const parsed = Command.safeParse(config)
|
||||
if (parsed.success) {
|
||||
result.command = mergeDeep(result.command, {
|
||||
[config.name]: parsed.data,
|
||||
})
|
||||
continue
|
||||
}
|
||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||
}
|
||||
// Migrate deprecated mode field to agent field
|
||||
for (const [name, mode] of Object.entries(result.mode)) {
|
||||
result.agent = mergeDeep(result.agent ?? {}, {
|
||||
@@ -192,6 +218,14 @@ export namespace Config {
|
||||
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
|
||||
export type Permission = z.infer<typeof Permission>
|
||||
|
||||
export const Command = z.object({
|
||||
template: z.string(),
|
||||
description: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
})
|
||||
export type Command = z.infer<typeof Command>
|
||||
|
||||
export const Agent = z
|
||||
.object({
|
||||
model: z.string().optional(),
|
||||
@@ -305,6 +339,7 @@ export namespace Config {
|
||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||
tui: TUI.optional().describe("TUI specific settings"),
|
||||
command: z.record(z.string(), Command).optional(),
|
||||
plugin: z.string().array().optional(),
|
||||
snapshot: z.boolean().optional(),
|
||||
share: z
|
||||
|
||||
@@ -36,9 +36,9 @@ export namespace Provider {
|
||||
},
|
||||
}
|
||||
},
|
||||
async opencode() {
|
||||
async opencode(input) {
|
||||
return {
|
||||
autoload: true,
|
||||
autoload: Object.keys(input.models).length > 0,
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Permission } from "../permission"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Auth } from "../auth"
|
||||
import { Command } from "../command"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
@@ -611,10 +612,12 @@ export namespace Server {
|
||||
description: "Created message",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.object({
|
||||
schema: resolver(
|
||||
z.object({
|
||||
info: MessageV2.Assistant,
|
||||
parts: MessageV2.Part.array(),
|
||||
})),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -634,6 +637,41 @@ export namespace Server {
|
||||
return c.json(msg)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/command",
|
||||
describeRoute({
|
||||
description: "Send a new command to a session",
|
||||
operationId: "session.command",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Created message",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
info: MessageV2.Assistant,
|
||||
parts: MessageV2.Part.array(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
zValidator("json", Session.CommandInput.omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
const msg = await Session.command({ ...body, sessionID })
|
||||
return c.json(msg)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/shell",
|
||||
describeRoute({
|
||||
@@ -656,7 +694,7 @@ export namespace Server {
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
zValidator("json", Session.CommandInput.omit({ sessionID: true })),
|
||||
zValidator("json", Session.ShellInput.omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
@@ -753,6 +791,27 @@ export namespace Server {
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/command",
|
||||
describeRoute({
|
||||
description: "List all commands",
|
||||
operationId: "command.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of commands",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Command.Info.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const commands = await Command.list()
|
||||
return c.json(commands)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/config/providers",
|
||||
describeRoute({
|
||||
|
||||
@@ -47,6 +47,8 @@ import { Permission } from "../permission"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { ulid } from "ulid"
|
||||
import { defer } from "../util/defer"
|
||||
import { Command } from "../command"
|
||||
import { $ } from "bun"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -1025,13 +1027,13 @@ export namespace Session {
|
||||
return result
|
||||
}
|
||||
|
||||
export const CommandInput = z.object({
|
||||
export const ShellInput = z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
agent: z.string(),
|
||||
command: z.string(),
|
||||
})
|
||||
export type CommandInput = z.infer<typeof CommandInput>
|
||||
export async function shell(input: CommandInput) {
|
||||
export type ShellInput = z.infer<typeof ShellInput>
|
||||
export async function shell(input: ShellInput) {
|
||||
using abort = lock(input.sessionID)
|
||||
const msg: MessageV2.Assistant = {
|
||||
id: Identifier.ascending("message"),
|
||||
@@ -1155,6 +1157,72 @@ export namespace Session {
|
||||
return { info: msg, parts: [part] }
|
||||
}
|
||||
|
||||
export const CommandInput = z.object({
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
sessionID: Identifier.schema("session"),
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
arguments: z.string(),
|
||||
command: z.string(),
|
||||
})
|
||||
export type CommandInput = z.infer<typeof CommandInput>
|
||||
const bashRegex = /!`([^`]+)`/g
|
||||
const fileRegex = /@([^\s]+)/g
|
||||
|
||||
export async function command(input: CommandInput) {
|
||||
const command = await Command.get(input.command)
|
||||
const agent = input.agent ?? command.agent ?? "build"
|
||||
const model =
|
||||
input.model ??
|
||||
command.model ??
|
||||
(await Agent.get(agent).then((x) => (x.model ? `${x.model.providerID}/${x.model.modelID}` : undefined))) ??
|
||||
(await Provider.defaultModel().then((x) => `${x.providerID}/${x.modelID}`))
|
||||
let template = command.template.replace("$ARGUMENTS", input.arguments)
|
||||
|
||||
const bash = Array.from(template.matchAll(bashRegex))
|
||||
if (bash.length > 0) {
|
||||
const results = await Promise.all(
|
||||
bash.map(async ([, cmd]) => {
|
||||
try {
|
||||
return await $`${{ raw: cmd }}`.nothrow().text()
|
||||
} catch (error) {
|
||||
return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}),
|
||||
)
|
||||
let index = 0
|
||||
template = template.replace(bashRegex, () => results[index++])
|
||||
}
|
||||
|
||||
const parts = [
|
||||
{
|
||||
type: "text",
|
||||
text: template,
|
||||
},
|
||||
] as ChatInput["parts"]
|
||||
|
||||
const matches = template.matchAll(fileRegex)
|
||||
const app = App.info()
|
||||
|
||||
for (const match of matches) {
|
||||
const file = path.join(app.path.cwd, match[1])
|
||||
parts.push({
|
||||
type: "file",
|
||||
url: `file://${file}`,
|
||||
filename: match[1],
|
||||
mime: "text/plain",
|
||||
})
|
||||
}
|
||||
|
||||
return chat({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
...Provider.parseModel(model!),
|
||||
agent,
|
||||
parts,
|
||||
})
|
||||
}
|
||||
|
||||
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
|
||||
const toolcalls: Record<string, MessageV2.ToolPart> = {}
|
||||
let snapshot: string | undefined
|
||||
|
||||
Reference in New Issue
Block a user