|
|
@@ -49,6 +49,8 @@ export namespace Config {
|
|
|
}
|
|
|
|
|
|
result.agent = result.agent || {}
|
|
|
+ result.mode = result.mode || {}
|
|
|
+ result.plugin = result.plugin || []
|
|
|
|
|
|
const directories = [
|
|
|
Global.Path.config,
|
|
|
@@ -57,109 +59,14 @@ export namespace Config {
|
|
|
)),
|
|
|
]
|
|
|
|
|
|
- const markdownAgents = [
|
|
|
- ...(await Filesystem.globUp("agent/**/*.md", Global.Path.config, Global.Path.config)),
|
|
|
- ...(await Filesystem.globUp(".opencode/agent/**/*.md", Instance.directory, Instance.worktree)),
|
|
|
- ]
|
|
|
- for (const item of markdownAgents) {
|
|
|
- const content = await Bun.file(item).text()
|
|
|
- const md = matter(content)
|
|
|
- if (!md.data) continue
|
|
|
-
|
|
|
- // Extract relative path from agent folder for nested agents
|
|
|
- let agentName = path.basename(item, ".md")
|
|
|
- const agentFolderPath = item.includes("/.opencode/agent/")
|
|
|
- ? item.split("/.opencode/agent/")[1]
|
|
|
- : item.includes("/agent/")
|
|
|
- ? item.split("/agent/")[1]
|
|
|
- : agentName + ".md"
|
|
|
-
|
|
|
- // If agent is in a subfolder, include folder path in name
|
|
|
- if (agentFolderPath.includes("/")) {
|
|
|
- const relativePath = agentFolderPath.replace(".md", "")
|
|
|
- const pathParts = relativePath.split("/")
|
|
|
- agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
|
|
|
- }
|
|
|
-
|
|
|
- const config = {
|
|
|
- name: agentName,
|
|
|
- ...md.data,
|
|
|
- prompt: md.content.trim(),
|
|
|
- }
|
|
|
- const parsed = Agent.safeParse(config)
|
|
|
- if (parsed.success) {
|
|
|
- result.agent = mergeDeep(result.agent, {
|
|
|
- [config.name]: parsed.data,
|
|
|
- })
|
|
|
- continue
|
|
|
- }
|
|
|
- throw new InvalidError({ path: item }, { cause: parsed.error })
|
|
|
+ for (const dir of directories) {
|
|
|
+ await assertValid(dir)
|
|
|
+ result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
|
|
|
+ result.agent = mergeDeep(result.agent, await loadAgent(dir))
|
|
|
+ result.agent = mergeDeep(result.agent, await loadMode(dir))
|
|
|
+ result.plugin.push(...(await loadPlugin(dir)))
|
|
|
}
|
|
|
|
|
|
- // Load mode markdown files
|
|
|
- result.mode = result.mode || {}
|
|
|
- const markdownModes = [
|
|
|
- ...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)),
|
|
|
- ...(await Filesystem.globUp(".opencode/mode/*.md", Instance.directory, Instance.worktree)),
|
|
|
- ]
|
|
|
- for (const item of markdownModes) {
|
|
|
- const content = await Bun.file(item).text()
|
|
|
- const md = matter(content)
|
|
|
- if (!md.data) continue
|
|
|
-
|
|
|
- const config = {
|
|
|
- name: path.basename(item, ".md"),
|
|
|
- ...md.data,
|
|
|
- prompt: md.content.trim(),
|
|
|
- }
|
|
|
- const parsed = Agent.safeParse(config)
|
|
|
- if (parsed.success) {
|
|
|
- result.agent = mergeDeep(result.mode, {
|
|
|
- [config.name]: {
|
|
|
- ...parsed.data,
|
|
|
- mode: "primary" as const,
|
|
|
- },
|
|
|
- })
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Load command markdown files
|
|
|
- result.command = result.command || {}
|
|
|
- const markdownCommands = [
|
|
|
- ...(await Filesystem.globUp("command/**/*.md", Global.Path.config, Global.Path.config)),
|
|
|
- ...(await Filesystem.globUp(".opencode/command/**/*.md", Instance.directory, Instance.worktree)),
|
|
|
- ]
|
|
|
- for (const item of markdownCommands) {
|
|
|
- const content = await Bun.file(item).text()
|
|
|
- const md = matter(content)
|
|
|
- if (!md.data) continue
|
|
|
-
|
|
|
- const name = (() => {
|
|
|
- const patterns = ["/.opencode/command/", "/command/"]
|
|
|
- const pattern = patterns.find((p) => item.includes(p))
|
|
|
-
|
|
|
- if (pattern) {
|
|
|
- const index = item.indexOf(pattern)
|
|
|
- return item.slice(index + pattern.length, -3)
|
|
|
- }
|
|
|
- return path.basename(item, ".md")
|
|
|
- })()
|
|
|
-
|
|
|
- const config = {
|
|
|
- name,
|
|
|
- ...md.data,
|
|
|
- template: md.content.trim(),
|
|
|
- }
|
|
|
- const parsed = Command.safeParse(config)
|
|
|
- if (parsed.success) {
|
|
|
- result.command = mergeDeep(result.command, {
|
|
|
- [config.name]: parsed.data,
|
|
|
- })
|
|
|
- continue
|
|
|
- }
|
|
|
- throw new InvalidError({ path: item }, { cause: parsed.error })
|
|
|
- }
|
|
|
// Migrate deprecated mode field to agent field
|
|
|
for (const [name, mode] of Object.entries(result.mode)) {
|
|
|
result.agent = mergeDeep(result.agent ?? {}, {
|
|
|
@@ -170,14 +77,6 @@ export namespace Config {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- result.plugin = result.plugin || []
|
|
|
- result.plugin.push(
|
|
|
- ...[
|
|
|
- ...(await Filesystem.globUp("plugin/*.{ts,js}", Global.Path.config, Global.Path.config)),
|
|
|
- ...(await Filesystem.globUp(".opencode/plugin/*.{ts,js}", Instance.directory, Instance.worktree)),
|
|
|
- ].map((x) => "file://" + x),
|
|
|
- )
|
|
|
-
|
|
|
if (Flag.OPENCODE_PERMISSION) {
|
|
|
result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
|
|
|
}
|
|
|
@@ -218,6 +117,128 @@ export namespace Config {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+ async function assertValid(dir: string) {
|
|
|
+ const ALLOWED_DIRS = new Set(["agent", "command", "mode", "plugin"])
|
|
|
+ const UNEXPECTED_DIR_GLOB = new Bun.Glob("*/")
|
|
|
+ for await (const item of UNEXPECTED_DIR_GLOB.scan({ absolute: true, cwd: dir, onlyFiles: false })) {
|
|
|
+ const dirname = path.basename(item)
|
|
|
+ if (!ALLOWED_DIRS.has(dirname)) {
|
|
|
+ throw new InvalidError({
|
|
|
+ path: dir,
|
|
|
+ message: `Unexpected directory "${dirname}" found in "${dir}". Only ${ALLOWED_DIRS.values().toArray().join(", ")} directories are allowed.`,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
|
|
|
+ async function loadCommand(dir: string) {
|
|
|
+ const result: Record<string, Command> = {}
|
|
|
+ for await (const item of COMMAND_GLOB.scan({ absolute: true, cwd: dir })) {
|
|
|
+ const content = await Bun.file(item).text()
|
|
|
+ const md = matter(content)
|
|
|
+ if (!md.data) continue
|
|
|
+
|
|
|
+ const name = (() => {
|
|
|
+ const patterns = ["/.opencode/command/", "/command/"]
|
|
|
+ const pattern = patterns.find((p) => item.includes(p))
|
|
|
+
|
|
|
+ if (pattern) {
|
|
|
+ const index = item.indexOf(pattern)
|
|
|
+ return item.slice(index + pattern.length, -3)
|
|
|
+ }
|
|
|
+ return path.basename(item, ".md")
|
|
|
+ })()
|
|
|
+
|
|
|
+ const config = {
|
|
|
+ name,
|
|
|
+ ...md.data,
|
|
|
+ template: md.content.trim(),
|
|
|
+ }
|
|
|
+ const parsed = Command.safeParse(config)
|
|
|
+ if (parsed.success) {
|
|
|
+ result[config.name] = parsed.data
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ throw new InvalidError({ path: item }, { cause: parsed.error })
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ const AGENT_GLOB = new Bun.Glob("agent/**/*.md")
|
|
|
+ async function loadAgent(dir: string) {
|
|
|
+ const result: Record<string, Agent> = {}
|
|
|
+
|
|
|
+ for await (const item of AGENT_GLOB.scan({ absolute: true, cwd: dir })) {
|
|
|
+ const content = await Bun.file(item).text()
|
|
|
+ const md = matter(content)
|
|
|
+ if (!md.data) continue
|
|
|
+
|
|
|
+ // Extract relative path from agent folder for nested agents
|
|
|
+ let agentName = path.basename(item, ".md")
|
|
|
+ const agentFolderPath = item.includes("/.opencode/agent/")
|
|
|
+ ? item.split("/.opencode/agent/")[1]
|
|
|
+ : item.includes("/agent/")
|
|
|
+ ? item.split("/agent/")[1]
|
|
|
+ : agentName + ".md"
|
|
|
+
|
|
|
+ // If agent is in a subfolder, include folder path in name
|
|
|
+ if (agentFolderPath.includes("/")) {
|
|
|
+ const relativePath = agentFolderPath.replace(".md", "")
|
|
|
+ const pathParts = relativePath.split("/")
|
|
|
+ agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
|
|
|
+ }
|
|
|
+
|
|
|
+ const config = {
|
|
|
+ name: agentName,
|
|
|
+ ...md.data,
|
|
|
+ prompt: md.content.trim(),
|
|
|
+ }
|
|
|
+ const parsed = Agent.safeParse(config)
|
|
|
+ if (parsed.success) {
|
|
|
+ result[config.name] = parsed.data
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ throw new InvalidError({ path: item }, { cause: parsed.error })
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ const MODE_GLOB = new Bun.Glob("mode/*.md")
|
|
|
+ async function loadMode(dir: string) {
|
|
|
+ const result: Record<string, Agent> = {}
|
|
|
+ for await (const item of MODE_GLOB.scan({ absolute: true, cwd: dir })) {
|
|
|
+ const content = await Bun.file(item).text()
|
|
|
+ const md = matter(content)
|
|
|
+ if (!md.data) continue
|
|
|
+
|
|
|
+ const config = {
|
|
|
+ name: path.basename(item, ".md"),
|
|
|
+ ...md.data,
|
|
|
+ prompt: md.content.trim(),
|
|
|
+ }
|
|
|
+ const parsed = Agent.safeParse(config)
|
|
|
+ if (parsed.success) {
|
|
|
+ result[config.name] = {
|
|
|
+ ...parsed.data,
|
|
|
+ mode: "primary" as const,
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ const PLUGIN_GLOB = new Bun.Glob("plugin/*.{ts,js}")
|
|
|
+ async function loadPlugin(dir: string) {
|
|
|
+ const plugins: string[] = []
|
|
|
+
|
|
|
+ for await (const item of PLUGIN_GLOB.scan({ absolute: true, cwd: dir })) {
|
|
|
+ plugins.push("file://" + item)
|
|
|
+ }
|
|
|
+ return plugins
|
|
|
+ }
|
|
|
+
|
|
|
export const McpLocal = z
|
|
|
.object({
|
|
|
type: z.literal("local").describe("Type of MCP server connection"),
|