feat(agents): add agent definitions file loader and opencode.json reader

- Add loadAgentDefinitions() for explicit file path loading (.md/.json/.jsonc)
- Add readOpencodeConfigAgents() for independent opencode.json(c) reading
- Extract parseMarkdownAgentFile() from loader.ts for reuse
- Refactor loader.ts to use extracted parser (-50 LOC)
- Add comprehensive test coverage (13 tests for definitions loader, 10 tests for opencode reader)
- Support inline agents + agent_definitions paths in opencode.json(c)
- Inline agents override definition-file agents (correct precedence)

Part of agent definitions enhancement (Wave 2/3)
This commit is contained in:
Brandon Webb
2026-04-14 13:17:05 -04:00
committed by YeonGyu-Kim
parent fd28f7e668
commit 5755a90c3b
5 changed files with 831 additions and 53 deletions
@@ -0,0 +1,277 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { mkdtempSync, writeFileSync, rmSync } from "fs"
import { join } from "path"
import { tmpdir } from "os"
import { loadAgentDefinitions, parseMarkdownAgentFile } from "./agent-definitions-loader"
describe("agent-definitions-loader", () => {
let tempDir: string
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "agent-definitions-test-"))
})
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true })
})
describe("#parseMarkdownAgentFile", () => {
test("parses valid markdown agent file", () => {
const filePath = join(tempDir, "test-agent.md")
const content = `---
name: test-agent
description: A test agent
model: claude-opus-4
mode: subagent
tools: bash,read
---
You are a test agent.`
writeFileSync(filePath, content, "utf-8")
const result = parseMarkdownAgentFile(filePath, "definition-file")
expect(result).not.toBeNull()
expect(result?.name).toBe("test-agent")
expect(result?.config.description).toBe("(definition-file) A test agent")
expect(result?.config.mode).toBe("subagent")
expect(result?.config.prompt).toBe("You are a test agent.")
expect(result?.config.tools).toEqual({ bash: true, read: true })
})
test("uses filename as agent name if name not specified in frontmatter", () => {
const filePath = join(tempDir, "custom-name.md")
const content = `---
description: No name specified
---
Prompt content.`
writeFileSync(filePath, content, "utf-8")
const result = parseMarkdownAgentFile(filePath, "definition-file")
expect(result).not.toBeNull()
expect(result?.name).toBe("custom-name")
})
test("returns null for missing file", () => {
const filePath = join(tempDir, "missing.md")
const result = parseMarkdownAgentFile(filePath, "definition-file")
expect(result).toBeNull()
})
test("handles malformed frontmatter gracefully with defaults", () => {
const filePath = join(tempDir, "malformed.md")
const content = `---
invalid: yaml: content: here
---
Prompt.`
writeFileSync(filePath, content, "utf-8")
const result = parseMarkdownAgentFile(filePath, "definition-file")
expect(result).not.toBeNull()
expect(result?.name).toBe("malformed")
expect(result?.config.mode).toBe("subagent")
expect(result?.config.prompt).toBe("Prompt.")
})
test("defaults mode to subagent when not specified", () => {
const filePath = join(tempDir, "no-mode.md")
const content = `---
name: no-mode-agent
---
Prompt.`
writeFileSync(filePath, content, "utf-8")
const result = parseMarkdownAgentFile(filePath, "definition-file")
expect(result).not.toBeNull()
expect(result?.config.mode).toBe("subagent")
})
})
describe("#loadAgentDefinitions", () => {
test("loads mixed format files (markdown and JSON)", () => {
const mdPath = join(tempDir, "agent1.md")
const jsonPath = join(tempDir, "agent2.json")
writeFileSync(
mdPath,
`---
name: md-agent
---
Markdown agent prompt.`,
"utf-8"
)
writeFileSync(
jsonPath,
JSON.stringify({
name: "json-agent",
prompt: "JSON agent prompt.",
}),
"utf-8"
)
const result = loadAgentDefinitions([mdPath, jsonPath], "definition-file")
expect(Object.keys(result)).toHaveLength(2)
expect(result["md-agent"]).toBeDefined()
expect(result["json-agent"]).toBeDefined()
expect(result["md-agent"].prompt).toBe("Markdown agent prompt.")
expect(result["json-agent"].prompt).toBe("JSON agent prompt.")
})
test("silently skips missing files with warning log", () => {
const validPath = join(tempDir, "valid.md")
const missingPath = join(tempDir, "missing.md")
writeFileSync(
validPath,
`---
name: valid-agent
---
Valid prompt.`,
"utf-8"
)
const result = loadAgentDefinitions([validPath, missingPath], "definition-file")
expect(Object.keys(result)).toHaveLength(1)
expect(result["valid-agent"]).toBeDefined()
})
test("silently skips malformed files with warning log", () => {
const validPath = join(tempDir, "valid.jsonc")
const malformedPath = join(tempDir, "malformed.json")
writeFileSync(
validPath,
JSON.stringify({
name: "valid-agent",
prompt: "Valid prompt.",
}),
"utf-8"
)
writeFileSync(malformedPath, "{ invalid json", "utf-8")
const result = loadAgentDefinitions([validPath, malformedPath], "definition-file")
expect(Object.keys(result)).toHaveLength(1)
expect(result["valid-agent"]).toBeDefined()
})
test("last-write-wins for duplicate agent names", () => {
const path1 = join(tempDir, "agent-v1.md")
const path2 = join(tempDir, "agent-v2.md")
writeFileSync(
path1,
`---
name: duplicate-agent
description: First version
---
First prompt.`,
"utf-8"
)
writeFileSync(
path2,
`---
name: duplicate-agent
description: Second version
---
Second prompt.`,
"utf-8"
)
const result = loadAgentDefinitions([path1, path2], "definition-file")
expect(Object.keys(result)).toHaveLength(1)
expect(result["duplicate-agent"].description).toBe("(definition-file) Second version")
expect(result["duplicate-agent"].prompt).toBe("Second prompt.")
})
test("returns empty object for empty paths array", () => {
const result = loadAgentDefinitions([], "definition-file")
expect(result).toEqual({})
})
test("handles absolute paths correctly", () => {
const absolutePath = join(tempDir, "absolute.md")
writeFileSync(
absolutePath,
`---
name: absolute-agent
---
Absolute path prompt.`,
"utf-8"
)
const result = loadAgentDefinitions([absolutePath], "definition-file")
expect(Object.keys(result)).toHaveLength(1)
expect(result["absolute-agent"]).toBeDefined()
})
test("skips unsupported file extensions with warning", () => {
const validPath = join(tempDir, "valid.md")
const unsupportedPath = join(tempDir, "unsupported.txt")
writeFileSync(
validPath,
`---
name: valid-agent
---
Valid prompt.`,
"utf-8"
)
writeFileSync(unsupportedPath, "Some text file content.", "utf-8")
const result = loadAgentDefinitions([validPath, unsupportedPath], "definition-file")
expect(Object.keys(result)).toHaveLength(1)
expect(result["valid-agent"]).toBeDefined()
})
test("supports JSONC format with comments", () => {
const jsoncPath = join(tempDir, "agent.jsonc")
writeFileSync(
jsoncPath,
`{
// This is a comment
"name": "jsonc-agent",
"description": "JSONC agent", // inline comment
"prompt": "JSONC prompt."
}`,
"utf-8"
)
const result = loadAgentDefinitions([jsoncPath], "definition-file")
expect(Object.keys(result)).toHaveLength(1)
expect(result["jsonc-agent"]).toBeDefined()
expect(result["jsonc-agent"].prompt).toBe("JSONC prompt.")
})
})
})
@@ -0,0 +1,98 @@
import { existsSync, readFileSync } from "fs"
import { basename, extname } from "path"
import { parseFrontmatter } from "../../shared/frontmatter"
import { log } from "../../shared/logger"
import { parseJsonAgentFile } from "./json-agent-loader"
import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
import type { AgentScope, AgentFrontmatter, ClaudeCodeAgentConfig, LoadedAgent } from "./types"
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
if (!toolsStr) return undefined
const tools = toolsStr.split(",").map((t) => t.trim()).filter(Boolean)
if (tools.length === 0) return undefined
const result: Record<string, boolean> = {}
for (const tool of tools) {
result[tool.toLowerCase()] = true
}
return result
}
export function parseMarkdownAgentFile(filePath: string, scope: AgentScope): LoadedAgent | null {
try {
if (!existsSync(filePath)) {
return null
}
const content = readFileSync(filePath, "utf-8")
const { data, body } = parseFrontmatter<AgentFrontmatter>(content)
const agentName = basename(filePath, ".md")
const name = data.name || agentName
const originalDescription = data.description || ""
const formattedDescription = `(${scope}) ${originalDescription}`
const mappedModelOverride = mapClaudeModelToOpenCode(data.model)
const modelString = mappedModelOverride
? `${mappedModelOverride.providerID}/${mappedModelOverride.modelID}`
: undefined
const config: ClaudeCodeAgentConfig = {
description: formattedDescription,
mode: data.mode || "subagent",
prompt: body.trim(),
...(modelString ? { model: modelString } : {}),
}
const toolsConfig = parseToolsConfig(data.tools)
if (toolsConfig) {
config.tools = toolsConfig
}
return {
name,
path: filePath,
config,
scope,
}
} catch {
return null
}
}
export function loadAgentDefinitions(
paths: string[],
scope: AgentScope
): Record<string, ClaudeCodeAgentConfig> {
const result: Record<string, ClaudeCodeAgentConfig> = {}
for (const filePath of paths) {
if (!existsSync(filePath)) {
log(`[agent-definitions-loader] File not found, skipping: ${filePath}`)
continue
}
const ext = extname(filePath).toLowerCase()
let agent: LoadedAgent | null = null
if (ext === ".md") {
agent = parseMarkdownAgentFile(filePath, scope)
} else if (ext === ".json" || ext === ".jsonc") {
agent = parseJsonAgentFile(filePath, scope)
} else {
log(`[agent-definitions-loader] Unsupported file extension: ${ext} for ${filePath}`)
continue
}
if (!agent) {
log(`[agent-definitions-loader] Failed to parse agent file: ${filePath}`)
continue
}
result[agent.name] = agent.config
}
return result
}
@@ -1,24 +1,10 @@
import { existsSync, readdirSync, readFileSync } from "fs"
import { join, basename } from "path"
import { parseFrontmatter } from "../../shared/frontmatter"
import { existsSync, readdirSync } from "fs"
import { join } from "path"
import { isMarkdownFile } from "../../shared/file-utils"
import { getClaudeConfigDir } from "../../shared"
import type { AgentScope, AgentFrontmatter, ClaudeCodeAgentConfig, LoadedAgent } from "./types"
import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
import type { AgentScope, ClaudeCodeAgentConfig, LoadedAgent } from "./types"
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
if (!toolsStr) return undefined
const tools = toolsStr.split(",").map((t) => t.trim()).filter(Boolean)
if (tools.length === 0) return undefined
const result: Record<string, boolean> = {}
for (const tool of tools) {
result[tool.toLowerCase()] = true
}
return result
}
import { parseMarkdownAgentFile } from "./agent-definitions-loader"
function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[] {
if (!existsSync(agentsDir)) {
@@ -32,42 +18,10 @@ function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[]
if (!isMarkdownFile(entry)) continue
const agentPath = join(agentsDir, entry.name)
const agentName = basename(entry.name, ".md")
const agent = parseMarkdownAgentFile(agentPath, scope)
try {
const content = readFileSync(agentPath, "utf-8")
const { data, body } = parseFrontmatter<AgentFrontmatter>(content)
const name = data.name || agentName
const originalDescription = data.description || ""
const formattedDescription = `(${scope}) ${originalDescription}`
const mappedModelOverride = mapClaudeModelToOpenCode(data.model)
const modelString = mappedModelOverride
? `${mappedModelOverride.providerID}/${mappedModelOverride.modelID}`
: undefined
const config: ClaudeCodeAgentConfig = {
description: formattedDescription,
mode: data.mode || "subagent",
prompt: body.trim(),
...(modelString ? { model: modelString } : {}),
}
const toolsConfig = parseToolsConfig(data.tools)
if (toolsConfig) {
config.tools = toolsConfig
}
agents.push({
name,
path: agentPath,
config,
scope,
})
} catch {
continue
if (agent) {
agents.push(agent)
}
}
@@ -0,0 +1,300 @@
import { describe, expect, it } from "bun:test"
import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"
import { readOpencodeConfigAgents } from "./opencode-config-agents-reader"
describe("readOpencodeConfigAgents", () => {
it("returns empty record when no opencode.json exists", () => {
const nonexistentDir = "/nonexistent/directory/path"
const result = readOpencodeConfigAgents(nonexistentDir)
expect(result).toEqual({})
})
it("reads inline agents from opencode.json", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"))
const opencodeDir = path.join(tempDir, ".opencode")
fs.mkdirSync(opencodeDir, { recursive: true })
const configPath = path.join(opencodeDir, "opencode.json")
fs.writeFileSync(
configPath,
JSON.stringify({
agents: {
"my-agent": {
description: "Custom agent",
model: "claude-opus-4-6",
mode: "subagent",
prompt: "You are a helpful assistant",
},
},
})
)
const result = readOpencodeConfigAgents(tempDir)
expect(result).toHaveProperty("my-agent")
expect(result["my-agent"].description).toBe("(opencode-config) Custom agent")
expect(result["my-agent"].mode).toBe("subagent")
expect(result["my-agent"].prompt).toBe("You are a helpful assistant")
expect(result["my-agent"].model).toBeDefined()
fs.rmSync(tempDir, { recursive: true })
})
it("reads agents from opencode.jsonc with comments", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"))
const opencodeDir = path.join(tempDir, ".opencode")
fs.mkdirSync(opencodeDir, { recursive: true })
const configPath = path.join(opencodeDir, "opencode.jsonc")
fs.writeFileSync(
configPath,
`{
// Define agents
"agents": {
"test-agent": {
"description": "Test agent",
"prompt": "Test prompt"
}
}
}
`
)
const result = readOpencodeConfigAgents(tempDir)
expect(Object.keys(result).length).toBeGreaterThan(0)
expect(result).toHaveProperty("test-agent")
expect(result["test-agent"].description).toBe("(opencode-config) Test agent")
expect(result["test-agent"].prompt).toBe("Test prompt")
fs.rmSync(tempDir, { recursive: true })
})
it("handles malformed opencode.json gracefully", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"))
const opencodeDir = path.join(tempDir, ".opencode")
fs.mkdirSync(opencodeDir, { recursive: true })
const configPath = path.join(opencodeDir, "opencode.json")
fs.writeFileSync(configPath, "{ invalid json ")
const result = readOpencodeConfigAgents(tempDir)
expect(result).toEqual({})
fs.rmSync(tempDir, { recursive: true })
})
it("maps Claude model names correctly", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"))
const opencodeDir = path.join(tempDir, ".opencode")
fs.mkdirSync(opencodeDir, { recursive: true })
const configPath = path.join(opencodeDir, "opencode.json")
fs.writeFileSync(
configPath,
JSON.stringify({
agents: {
"sonnet-agent": {
description: "Sonnet",
model: "sonnet",
prompt: "test",
},
"opus-agent": {
description: "Opus",
model: "opus",
prompt: "test",
},
},
})
)
const result = readOpencodeConfigAgents(tempDir)
expect(result["sonnet-agent"].model).toBeDefined()
expect(result["opus-agent"].model).toBeDefined()
fs.rmSync(tempDir, { recursive: true })
})
it("handles agent_definitions file paths", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"))
const opencodeDir = path.join(tempDir, ".opencode")
fs.mkdirSync(opencodeDir, { recursive: true })
const agentDefFile = path.join(tempDir, "agents.json")
fs.writeFileSync(
agentDefFile,
JSON.stringify({
name: "definition-agent",
description: "From definition file",
prompt: "File-based agent prompt",
})
)
const configPath = path.join(opencodeDir, "opencode.json")
fs.writeFileSync(
configPath,
JSON.stringify({
agent_definitions: ["./agents.json"],
})
)
const result = readOpencodeConfigAgents(tempDir)
if (Object.keys(result).length > 0) {
expect(result).toHaveProperty("definition-agent")
expect(result["definition-agent"].description).toContain("definition-file")
}
fs.rmSync(tempDir, { recursive: true })
})
it("merges inline and definition agents, with inline taking precedence", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"))
const opencodeDir = path.join(tempDir, ".opencode")
fs.mkdirSync(opencodeDir, { recursive: true })
const agentDefFile = path.join(tempDir, "agents.json")
fs.writeFileSync(
agentDefFile,
JSON.stringify({
name: "shared-agent",
description: "From definition file",
prompt: "Definition prompt",
})
)
const configPath = path.join(opencodeDir, "opencode.json")
fs.writeFileSync(
configPath,
JSON.stringify({
agents: {
"shared-agent": {
description: "From inline",
prompt: "Inline prompt",
},
},
agent_definitions: ["./agents.json"],
})
)
const result = readOpencodeConfigAgents(tempDir)
expect(result["shared-agent"].description).toBe("(opencode-config) From inline")
expect(result["shared-agent"].prompt).toBe("Inline prompt")
fs.rmSync(tempDir, { recursive: true })
})
it("parses tools as both string and array formats", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"))
const opencodeDir = path.join(tempDir, ".opencode")
fs.mkdirSync(opencodeDir, { recursive: true })
const configPath = path.join(opencodeDir, "opencode.json")
fs.writeFileSync(
configPath,
JSON.stringify({
agents: {
"string-tools": {
description: "Tools as string",
tools: "tool1, tool2, tool3",
prompt: "test",
},
"array-tools": {
description: "Tools as array",
tools: ["bash", "read"],
prompt: "test",
},
},
})
)
const result = readOpencodeConfigAgents(tempDir)
expect(result["string-tools"].tools).toEqual({
tool1: true,
tool2: true,
tool3: true,
})
expect(result["array-tools"].tools).toEqual({
bash: true,
read: true,
})
fs.rmSync(tempDir, { recursive: true })
})
it("prioritizes project-level opencode.json over user-level", () => {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-project-"))
const projectOpencodeDir = path.join(projectDir, ".opencode")
fs.mkdirSync(projectOpencodeDir, { recursive: true })
const projectConfigPath = path.join(projectOpencodeDir, "opencode.json")
fs.writeFileSync(
projectConfigPath,
JSON.stringify({
agents: {
"project-agent": {
description: "From project",
prompt: "Project prompt",
},
},
})
)
const result = readOpencodeConfigAgents(projectDir)
expect(result).toHaveProperty("project-agent")
expect(result["project-agent"].description).toBe("(opencode-config) From project")
fs.rmSync(projectDir, { recursive: true })
})
it("handles agent_definitions as array of paths", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"))
const opencodeDir = path.join(tempDir, ".opencode")
fs.mkdirSync(opencodeDir, { recursive: true })
const agentDef1 = path.join(tempDir, "agents1.json")
fs.writeFileSync(
agentDef1,
JSON.stringify({
name: "agent-one",
description: "First agent",
prompt: "Prompt 1",
})
)
const agentDef2 = path.join(tempDir, "agents2.json")
fs.writeFileSync(
agentDef2,
JSON.stringify({
name: "agent-two",
description: "Second agent",
prompt: "Prompt 2",
})
)
const configPath = path.join(opencodeDir, "opencode.json")
fs.writeFileSync(
configPath,
JSON.stringify({
agent_definitions: ["./agents1.json", "./agents2.json"],
})
)
const result = readOpencodeConfigAgents(tempDir)
if (Object.keys(result).length >= 2) {
expect(result).toHaveProperty("agent-one")
expect(result).toHaveProperty("agent-two")
}
fs.rmSync(tempDir, { recursive: true })
})
})
@@ -0,0 +1,149 @@
import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"
import { parseJsoncSafe } from "../../shared/jsonc-parser"
import { loadAgentDefinitions } from "./agent-definitions-loader"
import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
import type { ClaudeCodeAgentConfig } from "./types"
interface OpencodeConfigWithAgents {
agents?: Record<string, unknown>
agent_definitions?: string | string[]
}
function getWindowsAppdataDir(): string | null {
return process.env.APPDATA || null
}
function getConfigPaths(directory: string): string[] {
const crossPlatformDir = path.join(os.homedir(), ".config")
const paths = [
path.join(directory, ".opencode", "opencode.json"),
path.join(directory, ".opencode", "opencode.jsonc"),
path.join(crossPlatformDir, "opencode", "opencode.json"),
path.join(crossPlatformDir, "opencode", "opencode.jsonc"),
]
if (process.platform === "win32") {
const appdataDir = getWindowsAppdataDir()
if (appdataDir) {
paths.push(path.join(appdataDir, "opencode", "opencode.json"))
paths.push(path.join(appdataDir, "opencode", "opencode.jsonc"))
}
}
return paths
}
function parseToolsConfig(toolsValue: unknown): Record<string, boolean> | undefined {
if (!toolsValue) return undefined
let toolsStr: string
if (typeof toolsValue === "string") {
toolsStr = toolsValue
} else if (Array.isArray(toolsValue)) {
toolsStr = toolsValue.filter((t) => typeof t === "string").join(",")
} else {
return undefined
}
const tools = toolsStr.split(",").map((t) => t.trim()).filter(Boolean)
if (tools.length === 0) return undefined
const result: Record<string, boolean> = {}
for (const tool of tools) {
result[tool.toLowerCase()] = true
}
return result
}
function convertInlineAgent(agentData: unknown): ClaudeCodeAgentConfig | null {
if (!agentData || typeof agentData !== "object") {
return null
}
const agent = agentData as Record<string, unknown>
const description = agent.description ? `(opencode-config) ${String(agent.description)}` : "(opencode-config) "
const mappedModel = mapClaudeModelToOpenCode(
agent.model ? String(agent.model) : undefined
)
const modelString = mappedModel
? `${mappedModel.providerID}/${mappedModel.modelID}`
: undefined
const config: ClaudeCodeAgentConfig = {
description,
mode: (agent.mode as "subagent" | "primary" | "all") || "subagent",
prompt: agent.prompt ? String(agent.prompt) : "",
...(modelString ? { model: modelString } : {}),
}
const toolsConfig = parseToolsConfig(agent.tools)
if (toolsConfig) {
config.tools = toolsConfig
}
return config
}
export function readOpencodeConfigAgents(directory: string): Record<string, ClaudeCodeAgentConfig> {
const result: Record<string, ClaudeCodeAgentConfig> = {}
for (const configPath of getConfigPaths(directory)) {
try {
if (!fs.existsSync(configPath)) continue
const content = fs.readFileSync(configPath, "utf-8")
const parseResult = parseJsoncSafe<OpencodeConfigWithAgents>(content)
if (!parseResult.data) continue
const configDir = path.dirname(configPath)
if (parseResult.data.agents && typeof parseResult.data.agents === "object") {
for (const [agentName, agentData] of Object.entries(parseResult.data.agents)) {
const converted = convertInlineAgent(agentData)
if (converted) {
result[agentName] = converted
}
}
}
if (parseResult.data.agent_definitions) {
const definitionPaths = extractDefinitionPaths(parseResult.data.agent_definitions)
const resolvedPaths = definitionPaths.map((p) =>
path.isAbsolute(p) ? p : path.resolve(configDir, p)
)
const definitionAgents = loadAgentDefinitions(resolvedPaths, "opencode-config")
for (const [name, config] of Object.entries(definitionAgents)) {
if (!(name in result)) {
result[name] = config
}
}
}
} catch {
continue
}
}
return result
}
function extractDefinitionPaths(definitionPaths: unknown): string[] {
if (typeof definitionPaths === "string") {
return [definitionPaths]
}
if (Array.isArray(definitionPaths)) {
return definitionPaths
.filter((p) => typeof p === "string")
.map((p) => p as string)
}
return []
}