Skip to content
255 changes: 130 additions & 125 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { generateObject, type ModelMessage } from "ai"
import PROMPT_GENERATE from "./generate.txt"
import { SystemPrompt } from "../session/system"
import { Instance } from "../project/instance"
import { State } from "../project/state"
import { mergeDeep } from "remeda"

export namespace Agent {
Expand Down Expand Up @@ -39,145 +40,149 @@ export namespace Agent {
})
export type Info = z.infer<typeof Info>

const state = Instance.state(async () => {
const cfg = await Config.get()
const defaultTools = cfg.tools ?? {}
const defaultPermission: Info["permission"] = {
edit: "allow",
bash: {
"*": "allow",
},
webfetch: "allow",
doom_loop: "ask",
external_directory: "ask",
}
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})

const planPermission = mergeAgentPermissions(
{
edit: "deny",
const state = State.register(
"agent",
() => Instance.directory,
async () => {
const cfg = await Config.get()
const defaultTools = cfg.tools ?? {}
const defaultPermission: Info["permission"] = {
edit: "allow",
bash: {
"cut*": "allow",
"diff*": "allow",
"du*": "allow",
"file *": "allow",
"find * -delete*": "ask",
"find * -exec*": "ask",
"find * -fprint*": "ask",
"find * -fls*": "ask",
"find * -fprintf*": "ask",
"find * -ok*": "ask",
"find *": "allow",
"git diff*": "allow",
"git log*": "allow",
"git show*": "allow",
"git status*": "allow",
"git branch": "allow",
"git branch -v": "allow",
"grep*": "allow",
"head*": "allow",
"less*": "allow",
"ls*": "allow",
"more*": "allow",
"pwd*": "allow",
"rg*": "allow",
"sort --output=*": "ask",
"sort -o *": "ask",
"sort*": "allow",
"stat*": "allow",
"tail*": "allow",
"tree -o *": "ask",
"tree*": "allow",
"uniq*": "allow",
"wc*": "allow",
"whereis*": "allow",
"which*": "allow",
"*": "ask",
"*": "allow",
},
webfetch: "allow",
},
cfg.permission ?? {},
)
doom_loop: "ask",
external_directory: "ask",
}
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})

const result: Record<string, Info> = {
general: {
name: "general",
description:
"General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
tools: {
todoread: false,
todowrite: false,
...defaultTools,
const planPermission = mergeAgentPermissions(
{
edit: "deny",
bash: {
"cut*": "allow",
"diff*": "allow",
"du*": "allow",
"file *": "allow",
"find * -delete*": "ask",
"find * -exec*": "ask",
"find * -fprint*": "ask",
"find * -fls*": "ask",
"find * -fprintf*": "ask",
"find * -ok*": "ask",
"find *": "allow",
"git diff*": "allow",
"git log*": "allow",
"git show*": "allow",
"git status*": "allow",
"git branch": "allow",
"git branch -v": "allow",
"grep*": "allow",
"head*": "allow",
"less*": "allow",
"ls*": "allow",
"more*": "allow",
"pwd*": "allow",
"rg*": "allow",
"sort --output=*": "ask",
"sort -o *": "ask",
"sort*": "allow",
"stat*": "allow",
"tail*": "allow",
"tree -o *": "ask",
"tree*": "allow",
"uniq*": "allow",
"wc*": "allow",
"whereis*": "allow",
"which*": "allow",
"*": "ask",
},
webfetch: "allow",
},
options: {},
permission: agentPermission,
mode: "subagent",
builtIn: true,
},
build: {
name: "build",
tools: { ...defaultTools },
options: {},
permission: agentPermission,
mode: "primary",
builtIn: true,
},
plan: {
name: "plan",
options: {},
permission: planPermission,
tools: {
...defaultTools,
cfg.permission ?? {},
)

const result: Record<string, Info> = {
general: {
name: "general",
description:
"General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
tools: {
todoread: false,
todowrite: false,
...defaultTools,
},
options: {},
permission: agentPermission,
mode: "subagent",
builtIn: true,
},
mode: "primary",
builtIn: true,
},
}
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
if (value.disable) {
delete result[key]
continue
}
let item = result[key]
if (!item)
item = result[key] = {
name: key,
mode: "all",
build: {
name: "build",
tools: { ...defaultTools },
options: {},
permission: agentPermission,
mode: "primary",
builtIn: true,
},
plan: {
name: "plan",
options: {},
tools: {},
builtIn: false,
}
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value
item.options = {
...item.options,
...extra,
permission: planPermission,
tools: {
...defaultTools,
},
mode: "primary",
builtIn: true,
},
}
if (model) item.model = Provider.parseModel(model)
if (prompt) item.prompt = prompt
if (tools)
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
if (value.disable) {
delete result[key]
continue
}
let item = result[key]
if (!item)
item = result[key] = {
name: key,
mode: "all",
permission: agentPermission,
options: {},
tools: {},
builtIn: false,
}
const { name, model, prompt, tools, description, temperature, top_p, mode, color, permission, ...extra } = value
item.options = {
...item.options,
...extra,
}
if (model) item.model = Provider.parseModel(model)
if (prompt) item.prompt = prompt
if (tools)
item.tools = {
...item.tools,
...tools,
}
item.tools = {
...defaultTools,
...item.tools,
...tools,
}
item.tools = {
...defaultTools,
...item.tools,
}
if (description) item.description = description
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
if (color) item.color = color
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name
if (description) item.description = description
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
if (color) item.color = color
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name

if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
}
}
}
return result
})
return result
},
)

export async function get(agent: string) {
return state().then((x) => x[agent])
Expand Down
45 changes: 25 additions & 20 deletions packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { State } from "../project/state"
import PROMPT_INITIALIZE from "./template/initialize.txt"
import { Bus } from "../bus"
import { Identifier } from "../id/id"
Expand Down Expand Up @@ -36,32 +37,36 @@ export namespace Command {
})
export type Info = z.infer<typeof Info>

const state = Instance.state(async () => {
const cfg = await Config.get()
const state = State.register(
"command",
() => Instance.directory,
async () => {
const cfg = await Config.get()

const result: Record<string, Info> = {}
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,
subtask: command.subtask,
for (const [name, command] of Object.entries(cfg.command ?? {})) {
result[name] = {
name,
agent: command.agent,
model: command.model,
description: command.description,
template: command.template,
subtask: command.subtask,
}
}
}

if (result[Default.INIT] === undefined) {
result[Default.INIT] = {
name: Default.INIT,
description: "create/update AGENTS.md",
template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
if (result[Default.INIT] === undefined) {
result[Default.INIT] = {
name: Default.INIT,
description: "create/update AGENTS.md",
template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
}
}
}

return result
})
return result
},
)

export async function get(name: string) {
return state().then((x) => x[name])
Expand Down
17 changes: 17 additions & 0 deletions packages/opencode/src/config/backup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import fs from "fs/promises"

export async function createBackup(filepath: string): Promise<string> {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
const backupPath = `${filepath}.bak-${timestamp}`

if (await Bun.file(filepath).exists()) {
await fs.copyFile(filepath, backupPath)
}

return backupPath
}

export async function restoreBackup(backupPath: string, targetPath: string): Promise<void> {
await fs.copyFile(backupPath, targetPath)
await fs.unlink(backupPath)
}
Loading