Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function FormatError(input: unknown) {
return `Directory "${input.data.dir}" in ${input.data.path} is not valid. Rename the directory to "${input.data.suggestion}" or remove it. This is a common typo.`
}
if (ConfigMarkdown.FrontmatterError.isInstance(input)) {
return `Failed to parse frontmatter in ${input.data.path}:\n${input.data.message}`
return input.data.message
}
if (Config.InvalidError.isInstance(input))
return [
Expand Down
6 changes: 3 additions & 3 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export namespace Config {
})) {
const md = await ConfigMarkdown.parse(item).catch((err) => {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
? `${err.data.path}: ${err.data.message}`
? err.data.message
: `Failed to parse command ${item}`
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
log.error("failed to load command", { command: item, err })
Expand Down Expand Up @@ -274,7 +274,7 @@ export namespace Config {
})) {
const md = await ConfigMarkdown.parse(item).catch((err) => {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
? `${err.data.path}: ${err.data.message}`
? err.data.message
: `Failed to parse agent ${item}`
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
log.error("failed to load agent", { agent: item, err })
Expand Down Expand Up @@ -312,7 +312,7 @@ export namespace Config {
})) {
const md = await ConfigMarkdown.parse(item).catch((err) => {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
? `${err.data.path}: ${err.data.message}`
? err.data.message
: `Failed to parse mode ${item}`
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
log.error("failed to load mode", { mode: item, err })
Expand Down
56 changes: 54 additions & 2 deletions packages/opencode/src/config/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,60 @@ export namespace ConfigMarkdown {
return Array.from(template.matchAll(SHELL_REGEX))
}

export function preprocessFrontmatter(content: string): string {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
if (!match) return content

const frontmatter = match[1]
const lines = frontmatter.split("\n")
const result: string[] = []

for (const line of lines) {
// skip comments and empty lines
if (line.trim().startsWith("#") || line.trim() === "") {
result.push(line)
continue
}

// skip lines that are continuations (indented)
if (line.match(/^\s+/)) {
result.push(line)
continue
}

// match key: value pattern
const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/)
if (!kvMatch) {
result.push(line)
continue
}

const key = kvMatch[1]
const value = kvMatch[2].trim()

// skip if value is empty, already quoted, or uses block scalar
if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) {
result.push(line)
continue
}

// if value contains a colon, convert to block scalar
if (value.includes(":")) {
result.push(`${key}: |`)
result.push(` ${value}`)
continue
}

result.push(line)
}

const processed = result.join("\n")
return content.replace(frontmatter, () => processed)
}

export async function parse(filePath: string) {
const template = await Bun.file(filePath).text()
const raw = await Bun.file(filePath).text()
const template = preprocessFrontmatter(raw)

try {
const md = matter(template)
Expand All @@ -24,7 +76,7 @@ export namespace ConfigMarkdown {
throw new FrontmatterError(
{
path: filePath,
message: `Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
},
{ cause: err },
)
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/skill/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export namespace Skill {
const addSkill = async (match: string) => {
const md = await ConfigMarkdown.parse(match).catch((err) => {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
? `${err.data.path}: ${err.data.message}`
? err.data.message
: `Failed to parse skill ${match}`
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
log.error("failed to load skill", { skill: match, err })
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/test/config/fixtures/empty-frontmatter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Content
28 changes: 28 additions & 0 deletions packages/opencode/test/config/fixtures/frontmatter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
description: "This is a description wrapped in quotes"
# field: this is a commented out field that should be ignored
occupation: This man has the following occupation: Software Engineer
title: 'Hello World'
name: John "Doe"

family: He has no 'family'
summary: >
This is a summary
url: https://example.com:8080/path?query=value
time: The time is 12:30:00 PM
nested: First: Second: Third: Fourth
quoted_colon: "Already quoted: no change needed"
single_quoted_colon: 'Single quoted: also fine'
mixed: He said "hello: world" and then left
empty:
dollar: Use $' and $& for special patterns
---

Content that should not be parsed:

fake_field: this is not yaml
another: neither is this
time: 10:30:00 AM
url: https://should-not-be-parsed.com:3000

The above lines look like YAML but are just content.
1 change: 1 addition & 0 deletions packages/opencode/test/config/fixtures/no-frontmatter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Content
Loading