Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
164 commits
Select commit Hold shift + click to select a range
382399c
feat: add experimental.cache_command_markdown_files option
ariane-emory Dec 6, 2025
52bda69
refactor: remove code churn and duplication
ariane-emory Dec 6, 2025
a4716f3
- overkill tests
ariane-emory Dec 6, 2025
894b71a
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 6, 2025
12424cf
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 6, 2025
fa60c06
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 7, 2025
921e45a
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 7, 2025
8504263
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 7, 2025
41f5f13
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 7, 2025
2c2a109
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 8, 2025
e0f8591
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 8, 2025
d33f972
Fix TypeScript error: remove cacheKey from FileContents interface usage
ariane-emory Dec 8, 2025
38f5ea2
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 9, 2025
40ef02d
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 9, 2025
8fe6df9
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 9, 2025
059a025
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 10, 2025
1496a9f
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 10, 2025
e9223ad
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 10, 2025
6efe383
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 10, 2025
52ae607
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 10, 2025
4297d63
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 10, 2025
9e63955
Merge remote-tracking branch 'upstream/dev' into feat/experimental-do…
ariane-emory Dec 10, 2025
9e2fe75
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 11, 2025
556665c
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 11, 2025
367b1d7
tidy: revert unwanted change.
ariane-emory Dec 12, 2025
56716d2
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 12, 2025
f715e7c
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 12, 2025
bb7403a
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 13, 2025
ea82cfe
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 17, 2025
5a562a3
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 17, 2025
fa43236
Merge dev into feat/experimental-dont-cache-markdown
ariane-emory Dec 18, 2025
365cf4d
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 18, 2025
b3ccc0a
Merge branch 'feat/experimental-dont-cache-markdown' of github.com:ar…
ariane-emory Dec 18, 2025
ab88d5f
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 19, 2025
ea88b41
fix: add zod dependency to tauri package
ariane-emory Dec 19, 2025
834fd87
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 19, 2025
0ba9d2d
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 20, 2025
ebd238d
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 20, 2025
7079690
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 21, 2025
1292a24
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 21, 2025
ba81d5b
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 22, 2025
bcc654f
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 23, 2025
1aa5229
Resolve merge conflicts
ariane-emory Dec 23, 2025
a2a6a54
Merge dev into feat/experimental-dont-cache-markdown
ariane-emory Dec 23, 2025
3243034
Merge remote changes with local merge
ariane-emory Dec 23, 2025
d2899a3
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 23, 2025
669638f
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 23, 2025
ea85c12
Merge branch 'feat/experimental-dont-cache-markdown' of github.com:ar…
ariane-emory Dec 23, 2025
44b5fd3
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 23, 2025
457c5d2
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 23, 2025
d9dbf63
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 24, 2025
13dc46e
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 24, 2025
01bafa8
Merge dev branch into feat/experimental-dont-cache-markdown
ariane-emory Dec 24, 2025
461e1c0
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 24, 2025
a1d7de9
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 24, 2025
79988c6
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 25, 2025
e1818b2
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 25, 2025
0136cbe
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 25, 2025
94e7c4f
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 26, 2025
97f4d79
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 27, 2025
e19e51c
Merge remote-tracking branch 'origin/dev' into feat/experimental-dont…
ariane-emory Dec 27, 2025
d88e821
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 27, 2025
dba14d7
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 28, 2025
81ebea3
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 28, 2025
4894b27
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 28, 2025
8a8f918
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 28, 2025
a1b26f7
Merge branch 'feat/experimental-dont-cache-markdown' of github.com:ar…
ariane-emory Dec 28, 2025
4af85e5
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 28, 2025
9f7d7bd
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 29, 2025
cccb06b
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 29, 2025
62b5a03
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 29, 2025
7b6c052
Fix Link component to use TextAttributes.UNDERLINE instead of underli…
ariane-emory Dec 29, 2025
6c3cd56
Merge branch 'feat/experimental-dont-cache-markdown' of github.com:ar…
ariane-emory Dec 29, 2025
28e6b77
Revert link.tsx changes to preserve pre-existing underline error
ariane-emory Dec 29, 2025
0fe846b
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 29, 2025
8eeb64b
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 29, 2025
1b0603b
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 29, 2025
a4ce96b
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 30, 2025
fb4b670
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 30, 2025
bf5c52b
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 30, 2025
2d8412d
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 30, 2025
f2155d9
Merge remote-tracking branch 'origin/dev' into feat/experimental-dont…
ariane-emory Dec 30, 2025
eefab50
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 30, 2025
a469c68
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Dec 31, 2025
34b764d
Fix command loading after merge - use config state instead of private…
ariane-emory Dec 31, 2025
21375a1
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 1, 2026
d0efef8
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 1, 2026
a2fd4f0
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 1, 2026
ff84fb3
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 2, 2026
9bbd10f
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 3, 2026
136c6b1
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 3, 2026
6c3a4f5
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 4, 2026
dc1971b
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 4, 2026
022d9ba
Merge dev into feat/experimental-dont-cache-markdown
ariane-emory Jan 4, 2026
7cdbb2a
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 5, 2026
e59c860
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 5, 2026
612fae2
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 5, 2026
1fe9c12
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 6, 2026
3b51161
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 6, 2026
70752b9
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 6, 2026
9a66372
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 6, 2026
99fc3af
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 6, 2026
9779fd6
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 7, 2026
f7b2bf0
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 7, 2026
24fec23
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 7, 2026
2c6e680
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 8, 2026
5c3efaf
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 11, 2026
a8d1a99
Merge dev into feat/experimental-dont-cache-markdown
ariane-emory Jan 13, 2026
682d37a
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 13, 2026
e387509
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 15, 2026
3ac1e22
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 16, 2026
29c9542
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 17, 2026
7216606
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 17, 2026
2efc350
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 19, 2026
0e3b352
fix: loadFreshCommands now actually reloads from disk
ariane-emory Jan 19, 2026
2bc11be
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 19, 2026
74e4c34
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 22, 2026
ed1b2bf
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 23, 2026
6d9cc51
Merge remote-tracking branch 'origin/dev' into feat/experimental-dont…
ariane-emory Jan 25, 2026
9979877
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 25, 2026
7f24e32
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 26, 2026
2dc7e4f
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 27, 2026
f914cfe
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 29, 2026
f43cc13
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 29, 2026
59974df
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Jan 30, 2026
89bec9e
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 1, 2026
5b48e5a
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 2, 2026
421720f
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 3, 2026
c8391f5
Merge dev into feat/experimental-dont-cache-markdown
ariane-emory Feb 4, 2026
96631db
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 4, 2026
b28c5af
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 6, 2026
4276733
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 6, 2026
8ba8b89
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 6, 2026
781f9b7
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 7, 2026
f9675c7
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 10, 2026
4c3a8d3
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 11, 2026
fa6612e
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 12, 2026
23287ce
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 13, 2026
cdf3914
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 14, 2026
2e39612
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 15, 2026
684d542
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 15, 2026
889aa81
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 17, 2026
33a9c8f
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 18, 2026
37d7a41
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 20, 2026
6695f0e
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 20, 2026
1a20639
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 21, 2026
49b1e97
fix: preserve unknown command properties in loadFreshCommands
ariane-emory Feb 24, 2026
5081519
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 26, 2026
53e281d
perf: use mtime-based caching for command reloading
ariane-emory Feb 26, 2026
4fcba36
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Feb 26, 2026
cd90917
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Mar 5, 2026
84a48f1
Fix cache_command_markdown_files setting
ariane-emory Mar 7, 2026
e8c6aee
Add null check for command lookup
ariane-emory Mar 7, 2026
3c8378b
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Mar 8, 2026
e4d6676
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Mar 8, 2026
2df89c9
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Mar 13, 2026
135e58a
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Mar 20, 2026
c28b151
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Mar 20, 2026
0cb19be
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Mar 24, 2026
33ca8bd
Fix type errors in command/index.ts after merge
ariane-emory Mar 24, 2026
44e9f41
Fix config tests to expect name field in markdown-loaded commands
ariane-emory Mar 24, 2026
fa413b0
Merge branch 'dev' into feat/experimental-dont-cache-markdown
ariane-emory Mar 24, 2026
1fd34da
feat: Complete implementation of cache_command_markdown_files: false
ariane-emory Mar 24, 2026
8bf6f33
Merge branch 'dev' into feat/experimental-dont-cache-command-markdown
ariane-emory Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { makeRuntime } from "@/effect/run-service"
import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, ServiceMap } from "effect"
import z from "zod"
import path from "path"
import fs from "fs/promises"
import { existsSync } from "fs"
import { Config } from "../config/config"
import { ConfigMarkdown } from "../config/markdown"
import { Glob } from "../util/glob"
import { MCP } from "../mcp"
import { Skill } from "../skill"
import { Log } from "../util/log"
Expand Down Expand Up @@ -50,12 +55,211 @@ export namespace Command {
// for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it
export type Info = Omit<z.infer<typeof Info>, "template"> & { template: Promise<string> | string }

// Cache for command file mtimes when cache_command_markdown_files is false
const commandCache = new Map<string, { command: Info; mtime: number; filePath: string }>()

function rel(item: string, patterns: string[]) {
const normalizedItem = item.replaceAll("\\", "/")
for (const pattern of patterns) {
const index = normalizedItem.indexOf(pattern)
if (index === -1) continue
return normalizedItem.slice(index + pattern.length)
}
}

function trim(file: string) {
const ext = path.extname(file)
return ext.length ? file.slice(0, -ext.length) : file
}

async function findCommandFile(name: string): Promise<{ path: string } | null> {
const directories = await Config.directories()
for (const dir of directories) {
const subdirs = ["command", "commands", ".opencode/command", ".opencode/commands"]
for (const subdir of subdirs) {
const filePath = path.join(dir, subdir, name + ".md")
if (existsSync(filePath)) {
return { path: filePath }
}
const nested = path.join(dir, subdir, name + "/index.md")
if (existsSync(nested)) {
return { path: nested }
}
}
}
return null
}

async function loadSingleCommand(filePath: string): Promise<Info | null> {
const md = await ConfigMarkdown.parse(filePath).catch(() => null)
if (!md) return null

const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"]
const file = rel(filePath, patterns) ?? path.basename(filePath)
const cmdName = trim(file)

const config = { name: cmdName, ...md.data, template: md.content.trim() }
const parsed = Config.Command.safeParse(config)
if (!parsed.success) return null

return {
...parsed.data,
name: cmdName,
hints: hints(parsed.data.template),
}
}

async function loadFreshCommands(): Promise<Record<string, Info>> {
const result: Record<string, Info> = {}
const cfg = await Config.get()

// Load commands from config file (non-markdown)
for (const [name, command] of Object.entries(cfg.command ?? {})) {
result[name] = {
...command,
name,
hints: Command.hints(command.template),
}
}

// Reload commands from markdown files in each config directory
const directories = await Config.directories()
for (const dir of directories) {
const commands = await Config.reloadCommands(dir)
for (const [name, command] of Object.entries(commands)) {
result[name] = {
...command,
name,
hints: Command.hints(command.template),
}
}
}

return result
}

async function loadFreshCommandsWithMtime(): Promise<Record<string, Info>> {
const result: Record<string, Info> = createBuiltInCommands()
const cfg = await Config.get()

// Load commands from config file (non-markdown)
for (const [name, command] of Object.entries(cfg.command ?? {})) {
result[name] = { ...command, name, hints: Command.hints(command.template) }
}

// Load commands from markdown files with mtime checking
const directories = await Config.directories()
for (const dir of directories) {
const subdirs = ["command", "commands", ".opencode/command", ".opencode/commands"]
for (const subdir of subdirs) {
const cmdDir = path.join(dir, subdir)
if (!existsSync(cmdDir)) continue

const files = await Glob.scan("**/*.md", { cwd: cmdDir, absolute: true, dot: true, symlink: true })
for (const filePath of files) {
const stat = await fs.stat(filePath)
const mtime = stat.mtimeMs
const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"]
const file = rel(filePath, patterns) ?? path.basename(filePath)
const cmdName = trim(file)

const cached = commandCache.get(cmdName)
if (cached && cached.filePath === filePath && cached.mtime === mtime) {
result[cmdName] = cached.command
continue
}

const command = await loadSingleCommand(filePath)
if (command) {
commandCache.set(cmdName, { command, mtime, filePath })
result[cmdName] = command
}
}
}
}

// Add MCP prompts
for (const [name, prompt] of Object.entries(await MCP.prompts())) {
if (!result[name]) {
result[name] = {
name,
source: "mcp",
description: prompt.description,
get template() {
return new Promise<string>(async (resolve, reject) => {
const template = await MCP.getPrompt(
prompt.client,
prompt.name,
prompt.arguments
? Object.fromEntries(prompt.arguments?.map((argument, i) => [argument.name, `$${i + 1}`]))
: {},
).catch(reject)
resolve(
template?.messages
.map((message) => (message.content.type === "text" ? message.content.text : ""))
.join("\n") || "",
)
})
},
hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [],
}
}
}

// Add skills
for (const skill of await Skill.all()) {
if (!result[skill.name]) {
result[skill.name] = {
name: skill.name,
description: skill.description,
source: "skill",
get template() {
return skill.content
},
hints: [],
}
}
}

return result
}

function createBuiltInCommands(): Record<string, Info> {
return {
[Default.INIT]: {
name: Default.INIT,
description: "create/update AGENTS.md",
source: "command",
get template() {
return PROMPT_INITIALIZE.replace("${path}", Instance.worktree)
},
hints: hints(PROMPT_INITIALIZE),
},
[Default.REVIEW]: {
name: Default.REVIEW,
description: "review changes [commit|branch|pr], defaults to uncommitted",
source: "command",
get template() {
return PROMPT_REVIEW.replace("${path}", Instance.worktree)
},
subtask: true,
hints: hints(PROMPT_REVIEW),
},
}
}

export function hints(template: string) {
const result: string[] = []
const numbered = template.match(/\$\d+/g)
if (numbered) {
for (const match of [...new Set(numbered)].sort()) result.push(match)
}
const extended = template.match(/\$\{(\d+|\d*\.\.\d*)\}/g)
if (extended) {
for (const match of [...new Set(extended)].sort()) {
if (!result.includes(match)) result.push(match)
}
}
if (template.includes("$ARGUMENTS")) result.push("$ARGUMENTS")
return result
}
Expand Down Expand Up @@ -168,11 +372,54 @@ export namespace Command {
const state = yield* InstanceState.make<State>((ctx) => init(ctx))

const get = Effect.fn("Command.get")(function* (name: string) {
const get = Effect.fn("Command.get")(function* (name: string) {
const cfg = yield* Effect.promise(() => Config.get())
// When experimental.cache_command_markdown_files is explicitly false,
// reload commands from disk on each call
if (cfg.experimental?.cache_command_markdown_files === false) {
const builtIn = createBuiltInCommands()
if (builtIn[name]) return builtIn[name]

const cached = commandCache.get(name)
const fileInfo = yield* Effect.promise(() => findCommandFile(name))

// Only use cfg.command as fallback if no markdown file exists on disk
if (!fileInfo && cfg.command?.[name]) {
return { ...cfg.command[name], name, hints: Command.hints(cfg.command[name].template) }
}

if (!fileInfo) {
return undefined
}

// Check if we have a cached version with valid mtime
if (cached && cached.filePath === fileInfo.path) {
const stat = yield* Effect.promise(() => fs.stat(fileInfo.path))
if (stat.mtimeMs === cached.mtime) {
return cached.command
}
}

// Load fresh from disk
const command = yield* Effect.promise(() => loadSingleCommand(fileInfo.path))
if (command) {
const stat = yield* Effect.promise(() => fs.stat(fileInfo.path))
commandCache.set(name, { command, mtime: stat.mtimeMs, filePath: fileInfo.path })
}
return command ?? undefined
}
const s = yield* InstanceState.get(state)
return s.commands[name]
})

const list = Effect.fn("Command.list")(function* () {
const cfg = yield* Effect.promise(() => Config.get())
// When experimental.cache_command_markdown_files is explicitly false,
// reload commands from disk on each call
if (cfg.experimental?.cache_command_markdown_files === false) {
const fresh = yield* Effect.promise(() => loadFreshCommandsWithMtime())
return Object.values(fresh)
}
const s = yield* InstanceState.get(state)
return Object.values(s.commands)
})
Expand Down
10 changes: 9 additions & 1 deletion packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ export namespace Config {
agent: z.string().optional(),
model: ModelId.optional(),
subtask: z.boolean().optional(),
})
}).catchall(z.any())
export type Command = z.infer<typeof Command>

export const Skills = z.object({
Expand Down Expand Up @@ -1027,6 +1027,11 @@ export namespace Config {
.array(z.string())
.optional()
.describe("Tools that should only be available to primary agents."),
cache_command_markdown_files: z
.boolean()
.optional()
.default(true)
.describe("Cache command markdown files on first load. Set to false to reload command files on every execution."),
continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
mcp_timeout: z
.number()
Expand Down Expand Up @@ -1572,4 +1577,7 @@ export namespace Config {
export async function waitForDependencies() {
return runPromise((svc) => svc.waitForDependencies())
}

// Re-export loadCommand for use when cache_command_markdown_files is false
export const reloadCommands = loadCommand
}
4 changes: 4 additions & 0 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,11 +645,13 @@ Nested command template`,
const config = await Config.get()

expect(config.command?.["hello"]).toEqual({
name: "hello",
description: "Test command",
template: "Hello from singular command",
})

expect(config.command?.["nested/child"]).toEqual({
name: "nested/child",
description: "Nested command",
template: "Nested command template",
})
Expand Down Expand Up @@ -690,11 +692,13 @@ Nested command template`,
const config = await Config.get()

expect(config.command?.["hello"]).toEqual({
name: "hello",
description: "Test command",
template: "Hello from plural commands",
})

expect(config.command?.["nested/child"]).toEqual({
name: "nested/child",
description: "Nested command",
template: "Nested command template",
})
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,10 @@ export type Config = {
* Tools that should only be available to primary agents.
*/
primary_tools?: Array<string>
/**
* Cache command markdown files on first load. Set to false to reload command files on every execution.
*/
cache_command_markdown_files?: boolean
/**
* Continue the agent loop when a tool call is denied
*/
Expand Down