Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
90ad37f
feat: add {env:VAR} interpolation support to markdown frontmatter
ariane-emory Dec 5, 2025
4c89ebc
refactor: extract interpolateData function to reduce code churn
ariane-emory Dec 5, 2025
dbeeb67
refactor: reduce test code churn with helper function
ariane-emory Dec 5, 2025
7d498f9
refactor: rename interpolateData to interpolateEnvironmentVariables f…
ariane-emory Dec 5, 2025
c8f4921
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 5, 2025
f277413
Merge branch 'dev' into feat/markdown-frontmatter-interpolation
ariane-emory Dec 5, 2025
6cb3bb7
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 5, 2025
454362e
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 6, 2025
c3ae916
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 6, 2025
e2b89fb
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 7, 2025
8150897
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 7, 2025
9d348b5
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 7, 2025
b45b2d7
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 7, 2025
0aadeea
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 8, 2025
9f0cb53
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 8, 2025
7a1f904
Fix TypeScript error: remove cacheKey from FileContents interface usage
ariane-emory Dec 8, 2025
14f7851
revert file
ariane-emory Dec 8, 2025
1f8d1f1
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 9, 2025
9f2e75d
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 9, 2025
43297fc
Merge branch 'dev' into feat/markdown-frontmatter-interpolation
ariane-emory Dec 9, 2025
0d32508
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 10, 2025
96b0905
Merge branch 'dev' into feat/markdown-frontmatter-interpolation
ariane-emory Dec 10, 2025
2ef2b89
Merge branch 'dev' into feat/markdown-frontmatter-interpolation
ariane-emory Dec 10, 2025
a7684bd
Merge remote-tracking branch 'upstream/dev' into feat/markdown-frontm…
ariane-emory Dec 10, 2025
eac918d
Merge branch 'dev' into feat/markdown-frontmatter-interpolation
ariane-emory Dec 10, 2025
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
19 changes: 19 additions & 0 deletions packages/opencode/src/config/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,30 @@ export namespace ConfigMarkdown {
return Array.from(template.matchAll(SHELL_REGEX))
}

// Perform {env:VAR} interpolation on frontmatter data only
function interpolateEnvironmentVariables(obj: any): any {
if (typeof obj === "string") {
return obj.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
})
} else if (Array.isArray(obj)) {
return obj.map(interpolateEnvironmentVariables)
} else if (obj && typeof obj === "object") {
const result: any = {}
for (const [key, value] of Object.entries(obj)) {
result[key] = interpolateEnvironmentVariables(value)
}
return result
}
return obj
}

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

try {
const md = matter(template)
md.data = interpolateEnvironmentVariables(md.data)
return md
} catch (err) {
throw new FrontmatterError(
Expand Down
85 changes: 85 additions & 0 deletions packages/opencode/test/config/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,88 @@ test("should not match email addresses", () => {
const emailMatches = ConfigMarkdown.files(emailTest)
expect(emailMatches.length).toBe(0)
})

// Helper function to reduce test code duplication
async function parseMarkdownWithEnv(markdown: string) {
const tempFile = `/tmp/test-agent-${Date.now()}.md`
await Bun.write(tempFile, markdown)
try {
return await ConfigMarkdown.parse(tempFile)
} finally {
await Bun.file(tempFile).delete()
}
}

// Tests for {env:VAR} interpolation in frontmatter
test("should interpolate {env:VAR} in frontmatter", async () => {
process.env.TEST_MODEL = "gpt-4"
process.env.TEST_DESCRIPTION = "Test agent description"

const markdownWithEnv = `---
description: "{env:TEST_DESCRIPTION}"
model: "{env:TEST_MODEL}"
mode: primary
---

# Agent Content

This is the agent content.`

const result = await parseMarkdownWithEnv(markdownWithEnv)

expect(result.data.description).toBe("Test agent description")
expect(result.data.model).toBe("gpt-4")
expect(result.data.mode).toBe("primary")
expect(result.content).toContain("Agent Content")
})

test("should handle missing environment variables gracefully", async () => {
delete process.env.NONEXISTENT_VAR

const markdownWithMissingEnv = `---
description: "Description with {env:NONEXISTENT_VAR} missing"
model: "gpt-3.5-turbo"
---

# Agent Content`

const result = await parseMarkdownWithEnv(markdownWithMissingEnv)

expect(result.data.description).toBe("Description with missing")
expect(result.data.model).toBe("gpt-3.5-turbo")
})

test("should interpolate multiple environment variables in same field", async () => {
process.env.PREFIX = "AI"
process.env.SUFFIX = "Assistant"

const markdownWithMultipleEnv = `---
description: "{env:PREFIX} {env:SUFFIX}"
model: "gpt-4"
---

# Agent Content`

const result = await parseMarkdownWithEnv(markdownWithMultipleEnv)

expect(result.data.description).toBe("AI Assistant")
expect(result.data.model).toBe("gpt-4")
})

test("should not interpolate {env:VAR} in markdown body content", async () => {
process.env.BODY_VAR = "should not appear"

const markdownWithEnvInBody = `---
description: "Test agent"
model: "gpt-4"
---

# Agent Content

This should not interpolate: {env:BODY_VAR}`

const result = await parseMarkdownWithEnv(markdownWithEnvInBody)

expect(result.data.description).toBe("Test agent")
expect(result.content).toContain("{env:BODY_VAR}")
})