diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index d1eeeac3821..6390091812b 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -6,6 +6,10 @@ export namespace ConfigMarkdown { export const FILE_REGEX = /(? process.env[varName] || "") + } + export function files(template: string) { return Array.from(template.matchAll(FILE_REGEX)) } @@ -67,7 +71,7 @@ export namespace ConfigMarkdown { export async function parse(filePath: string) { const raw = await Bun.file(filePath).text() - const template = preprocessFrontmatter(raw) + const template = preprocessFrontmatter(substituteEnv(raw)) try { const md = matter(template) diff --git a/packages/opencode/test/config/fixtures/env-frontmatter.md b/packages/opencode/test/config/fixtures/env-frontmatter.md new file mode 100644 index 00000000000..bb336d4d5b9 --- /dev/null +++ b/packages/opencode/test/config/fixtures/env-frontmatter.md @@ -0,0 +1,7 @@ +--- +description: "Token is {env:TEST_MCP_TOKEN}" +note: "{env:EMPTY_ENV_VAR}" +--- + +Token: {env:TEST_MCP_TOKEN} +Missing: {env:EMPTY_ENV_VAR} diff --git a/packages/opencode/test/config/markdown.test.ts b/packages/opencode/test/config/markdown.test.ts index b4263ee6b5c..af0aa8065a1 100644 --- a/packages/opencode/test/config/markdown.test.ts +++ b/packages/opencode/test/config/markdown.test.ts @@ -1,4 +1,4 @@ -import { expect, test, describe } from "bun:test" +import { expect, test, describe, beforeAll, afterAll } from "bun:test" import { ConfigMarkdown } from "../../src/config/markdown" describe("ConfigMarkdown: normal template", () => { @@ -190,3 +190,37 @@ describe("ConfigMarkdown: frontmatter parsing w/ no frontmatter", async () => { expect(result.content.trim()).toBe("Content") }) }) + +describe("ConfigMarkdown: env substitution", () => { + const tokenKey = "TEST_MCP_TOKEN" + const emptyKey = "EMPTY_ENV_VAR" + const prevToken = process.env[tokenKey] + const prevEmpty = process.env[emptyKey] + let parsed: Awaited> + + beforeAll(async () => { + process.env[tokenKey] = "abc123" + delete process.env[emptyKey] + parsed = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/env-frontmatter.md") + }) + + afterAll(() => { + if (prevToken === undefined) delete process.env[tokenKey] + else process.env[tokenKey] = prevToken + if (prevEmpty === undefined) delete process.env[emptyKey] + else process.env[emptyKey] = prevEmpty + }) + + test("should substitute env vars in frontmatter", () => { + expect(parsed.data.description).toBe("Token is abc123") + }) + + test("should substitute missing env vars with empty string in frontmatter", () => { + expect(parsed.data.note).toBe("") + }) + + test("should substitute env vars in content", () => { + expect(parsed.content).toContain("Token: abc123") + expect(parsed.content).toContain("Missing: ") + }) +}) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index ea1f779cd37..12504f38cd6 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -184,6 +184,38 @@ Provide constructive feedback without making direct changes. The markdown file name becomes the agent name. For example, `review.md` creates a `review` agent. +#### Variables + +You can substitute environment variables in markdown agent files using `{env:VAR_NAME}`. This works in both frontmatter and the prompt body. + +```markdown title="~/.config/opencode/agents/mcdonalds.md" +--- +name: "McDonalds" +description: Agent to help you ordering McDonalds +mode: primary +model: "{env:OPENCODE_SMALL_MODEL}" +tools: + question: true + bash: false + read: false + glob: false + grep: false + write: false + edit: false + task: false + todowrite: false + todoread: false + skill: false + "mcdonalds-mcp_*": true +--- + +You are a McDonalds clerk who can help user make ordering, save user money as much as possible, unless user say no. + +If MCP_TOKEN:`{env:MCDONALDS_MCP_TOKEN}` is empty, tell the user to apply for a token at https://open.mcd.cn/mcp/doc + +If MCP_TOKEN not set, it will be replaced with an empty string. Avoid echoing secrets back to the user. +``` + --- ## Options