From aab8a232437ae4da60575a4000781a6e66719f56 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 13 Feb 2026 11:08:15 +0900 Subject: [PATCH 1/3] fix(schema): generate full JSON schema with Zod v4 --- assets/oh-my-opencode.schema.json | 3295 ++++++++++++++++++++++++++++- script/build-schema-document.ts | 17 + script/build-schema.test.ts | 18 + script/build-schema.ts | 16 +- 4 files changed, 3331 insertions(+), 15 deletions(-) create mode 100644 script/build-schema-document.ts create mode 100644 script/build-schema.test.ts diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 343c3c078..6cb00bd75 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -2,5 +2,3298 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", "title": "Oh My OpenCode Configuration", - "description": "Configuration schema for oh-my-opencode plugin" + "description": "Configuration schema for oh-my-opencode plugin", + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "new_task_system_enabled": { + "type": "boolean" + }, + "default_run_agent": { + "type": "string" + }, + "disabled_mcps": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "disabled_agents": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "sisyphus", + "hephaestus", + "prometheus", + "oracle", + "librarian", + "explore", + "multimodal-looker", + "metis", + "momus", + "atlas" + ] + } + }, + "disabled_skills": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "playwright", + "agent-browser", + "dev-browser", + "frontend-ui-ux", + "git-master" + ] + } + }, + "disabled_hooks": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "todo-continuation-enforcer", + "context-window-monitor", + "session-recovery", + "session-notification", + "comment-checker", + "grep-output-truncator", + "tool-output-truncator", + "question-label-truncator", + "directory-agents-injector", + "directory-readme-injector", + "empty-task-response-detector", + "think-mode", + "subagent-question-blocker", + "anthropic-context-window-limit-recovery", + "preemptive-compaction", + "rules-injector", + "background-notification", + "auto-update-checker", + "startup-toast", + "keyword-detector", + "agent-usage-reminder", + "non-interactive-env", + "interactive-bash-session", + "thinking-block-validator", + "ralph-loop", + "category-skill-reminder", + "compaction-context-injector", + "compaction-todo-preserver", + "claude-code-hooks", + "auto-slash-command", + "edit-error-recovery", + "delegate-task-retry", + "prometheus-md-only", + "sisyphus-junior-notepad", + "start-work", + "atlas", + "unstable-agent-babysitter", + "task-reminder", + "task-resume-info", + "stop-continuation-guard", + "tasks-todowrite-disabler", + "write-existing-file-guard", + "anthropic-effort" + ] + } + }, + "disabled_commands": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "init-deep", + "ralph-loop", + "ulw-loop", + "cancel-ralph", + "refactor", + "start-work", + "stop-continuation" + ] + } + }, + "disabled_tools": { + "type": "array", + "items": { + "type": "string" + } + }, + "agents": { + "type": "object", + "properties": { + "build": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "plan": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "sisyphus": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "hephaestus": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "sisyphus-junior": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "OpenCode-Builder": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "prometheus": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "metis": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "momus": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "oracle": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "librarian": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "explore": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "multimodal-looker": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "atlas": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "prompt_append": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "task": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + }, + "additionalProperties": false + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "categories": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "prompt_append": { + "type": "string" + }, + "is_unstable_agent": { + "type": "boolean" + }, + "disable": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "claude_code": { + "type": "object", + "properties": { + "mcp": { + "type": "boolean" + }, + "commands": { + "type": "boolean" + }, + "skills": { + "type": "boolean" + }, + "agents": { + "type": "boolean" + }, + "hooks": { + "type": "boolean" + }, + "plugins": { + "type": "boolean" + }, + "plugins_override": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + } + }, + "additionalProperties": false + }, + "sisyphus_agent": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean" + }, + "default_builder_enabled": { + "type": "boolean" + }, + "planner_enabled": { + "type": "boolean" + }, + "replace_plan": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "comment_checker": { + "type": "object", + "properties": { + "custom_prompt": { + "type": "string" + } + }, + "additionalProperties": false + }, + "experimental": { + "type": "object", + "properties": { + "aggressive_truncation": { + "type": "boolean" + }, + "auto_resume": { + "type": "boolean" + }, + "preemptive_compaction": { + "type": "boolean" + }, + "truncate_all_tool_outputs": { + "type": "boolean" + }, + "dynamic_context_pruning": { + "type": "object", + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "notification": { + "default": "detailed", + "type": "string", + "enum": [ + "off", + "minimal", + "detailed" + ] + }, + "turn_protection": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "turns": { + "default": 3, + "type": "number", + "minimum": 1, + "maximum": 10 + } + }, + "required": [ + "enabled", + "turns" + ], + "additionalProperties": false + }, + "protected_tools": { + "default": [ + "task", + "todowrite", + "todoread", + "lsp_rename", + "session_read", + "session_write", + "session_search" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "strategies": { + "type": "object", + "properties": { + "deduplication": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "additionalProperties": false + }, + "supersede_writes": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "aggressive": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "enabled", + "aggressive" + ], + "additionalProperties": false + }, + "purge_errors": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "turns": { + "default": 5, + "type": "number", + "minimum": 1, + "maximum": 20 + } + }, + "required": [ + "enabled", + "turns" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "required": [ + "enabled", + "notification", + "protected_tools" + ], + "additionalProperties": false + }, + "task_system": { + "type": "boolean" + }, + "plugin_load_timeout_ms": { + "type": "number", + "minimum": 1000 + }, + "safe_hook_creation": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "auto_update": { + "type": "boolean" + }, + "skills": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "properties": { + "sources": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "recursive": { + "type": "boolean" + }, + "glob": { + "type": "string" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + } + ] + } + }, + "enable": { + "type": "array", + "items": { + "type": "string" + } + }, + "disable": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "template": { + "type": "string" + }, + "from": { + "type": "string" + }, + "model": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "subtask": { + "type": "boolean" + }, + "argument-hint": { + "type": "string" + }, + "license": { + "type": "string" + }, + "compatibility": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "allowed-tools": { + "type": "array", + "items": { + "type": "string" + } + }, + "disable": { + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + } + ] + }, + "ralph_loop": { + "type": "object", + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "default_max_iterations": { + "default": 100, + "type": "number", + "minimum": 1, + "maximum": 1000 + }, + "state_dir": { + "type": "string" + } + }, + "required": [ + "enabled", + "default_max_iterations" + ], + "additionalProperties": false + }, + "background_task": { + "type": "object", + "properties": { + "defaultConcurrency": { + "type": "number", + "minimum": 1 + }, + "providerConcurrency": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "number", + "minimum": 0 + } + }, + "modelConcurrency": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "number", + "minimum": 0 + } + }, + "staleTimeoutMs": { + "type": "number", + "minimum": 60000 + } + }, + "additionalProperties": false + }, + "notification": { + "type": "object", + "properties": { + "force_enable": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "babysitting": { + "type": "object", + "properties": { + "timeout_ms": { + "default": 120000, + "type": "number" + } + }, + "required": [ + "timeout_ms" + ], + "additionalProperties": false + }, + "git_master": { + "type": "object", + "properties": { + "commit_footer": { + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "include_co_authored_by": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "commit_footer", + "include_co_authored_by" + ], + "additionalProperties": false + }, + "browser_automation_engine": { + "type": "object", + "properties": { + "provider": { + "default": "playwright", + "type": "string", + "enum": [ + "playwright", + "agent-browser", + "dev-browser" + ] + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + "websearch": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "exa", + "tavily" + ] + } + }, + "additionalProperties": false + }, + "tmux": { + "type": "object", + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "layout": { + "default": "main-vertical", + "type": "string", + "enum": [ + "main-horizontal", + "main-vertical", + "tiled", + "even-horizontal", + "even-vertical" + ] + }, + "main_pane_size": { + "default": 60, + "type": "number", + "minimum": 20, + "maximum": 80 + }, + "main_pane_min_width": { + "default": 120, + "type": "number", + "minimum": 40 + }, + "agent_pane_min_width": { + "default": 40, + "type": "number", + "minimum": 20 + } + }, + "required": [ + "enabled", + "layout", + "main_pane_size", + "main_pane_min_width", + "agent_pane_min_width" + ], + "additionalProperties": false + }, + "sisyphus": { + "type": "object", + "properties": { + "tasks": { + "type": "object", + "properties": { + "storage_path": { + "type": "string" + }, + "task_list_id": { + "type": "string" + }, + "claude_code_compat": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "claude_code_compat" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "_migrations": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false } \ No newline at end of file diff --git a/script/build-schema-document.ts b/script/build-schema-document.ts new file mode 100644 index 000000000..aee14a4da --- /dev/null +++ b/script/build-schema-document.ts @@ -0,0 +1,17 @@ +import * as z from "zod" +import { OhMyOpenCodeConfigSchema } from "../src/config/schema" + +export function createOhMyOpenCodeJsonSchema(): Record { + const jsonSchema = z.toJSONSchema(OhMyOpenCodeConfigSchema, { + target: "draft-07", + unrepresentable: "any", + }) + + return { + $schema: "http://json-schema.org/draft-07/schema#", + $id: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + title: "Oh My OpenCode Configuration", + description: "Configuration schema for oh-my-opencode plugin", + ...jsonSchema, + } +} diff --git a/script/build-schema.test.ts b/script/build-schema.test.ts new file mode 100644 index 000000000..ff45f3db1 --- /dev/null +++ b/script/build-schema.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test" +import { createOhMyOpenCodeJsonSchema } from "./build-schema-document" + +describe("build-schema-document", () => { + test("generates schema with skills property", () => { + // given + const expectedDraft = "http://json-schema.org/draft-07/schema#" + + // when + const schema = createOhMyOpenCodeJsonSchema() + + // then + expect(schema.$schema).toBe(expectedDraft) + expect(schema.title).toBe("Oh My OpenCode Configuration") + expect(schema.properties).toBeDefined() + expect(schema.properties.skills).toBeDefined() + }) +}) diff --git a/script/build-schema.ts b/script/build-schema.ts index d0ed70d3a..7f55d74b1 100644 --- a/script/build-schema.ts +++ b/script/build-schema.ts @@ -1,24 +1,12 @@ #!/usr/bin/env bun -import * as z from "zod" -import { zodToJsonSchema } from "zod-to-json-schema" -import { OhMyOpenCodeConfigSchema } from "../src/config/schema" +import { createOhMyOpenCodeJsonSchema } from "./build-schema-document" const SCHEMA_OUTPUT_PATH = "assets/oh-my-opencode.schema.json" async function main() { console.log("Generating JSON Schema...") - const jsonSchema = zodToJsonSchema(OhMyOpenCodeConfigSchema, { - target: "draft7", - }) - - const finalSchema = { - $schema: "http://json-schema.org/draft-07/schema#", - $id: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", - title: "Oh My OpenCode Configuration", - description: "Configuration schema for oh-my-opencode plugin", - ...jsonSchema, - } + const finalSchema = createOhMyOpenCodeJsonSchema() await Bun.write(SCHEMA_OUTPUT_PATH, JSON.stringify(finalSchema, null, 2)) From 0001bc87c2f05746cba46890ca93e9fd18d837ca Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 13 Feb 2026 11:08:22 +0900 Subject: [PATCH 2/3] feat(skills): load config sources in runtime discovery --- src/config/schema.test.ts | 17 +++ src/config/schema/skills.ts | 16 +-- .../config-source-discovery.test.ts | 68 ++++++++++++ .../config-source-discovery.ts | 101 ++++++++++++++++++ src/features/opencode-skill-loader/index.ts | 1 + .../opencode-skill-loader/merger.test.ts | 55 ++++++++++ src/features/opencode-skill-loader/merger.ts | 2 + src/plugin-handlers/agent-config-handler.ts | 7 ++ src/plugin-handlers/command-config-handler.ts | 9 ++ src/plugin-handlers/config-handler.ts | 2 +- src/plugin/skill-context.ts | 8 +- 11 files changed, 273 insertions(+), 13 deletions(-) create mode 100644 src/features/opencode-skill-loader/config-source-discovery.test.ts create mode 100644 src/features/opencode-skill-loader/config-source-discovery.ts create mode 100644 src/features/opencode-skill-loader/merger.test.ts diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 704a7ea81..2d151ec53 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -733,3 +733,20 @@ describe("GitMasterConfigSchema", () => { expect(result.success).toBe(false) }) }) + +describe("skills schema", () => { + test("accepts skills.sources configuration", () => { + //#given + const config = { + skills: { + sources: [{ path: "skill/", recursive: true }], + }, + } + + //#when + const result = OhMyOpenCodeConfigSchema.safeParse(config) + + //#then + expect(result.success).toBe(true) + }) +}) diff --git a/src/config/schema/skills.ts b/src/config/schema/skills.ts index 0e7fbaa8a..07afd1fd2 100644 --- a/src/config/schema/skills.ts +++ b/src/config/schema/skills.ts @@ -28,17 +28,11 @@ export const SkillEntrySchema = z.union([z.boolean(), SkillDefinitionSchema]) export const SkillsConfigSchema = z.union([ z.array(z.string()), - z - .record(z.string(), SkillEntrySchema) - .and( - z - .object({ - sources: z.array(SkillSourceSchema).optional(), - enable: z.array(z.string()).optional(), - disable: z.array(z.string()).optional(), - }) - .partial() - ), + z.object({ + sources: z.array(SkillSourceSchema).optional(), + enable: z.array(z.string()).optional(), + disable: z.array(z.string()).optional(), + }).catchall(SkillEntrySchema), ]) export type SkillsConfig = z.infer diff --git a/src/features/opencode-skill-loader/config-source-discovery.test.ts b/src/features/opencode-skill-loader/config-source-discovery.test.ts new file mode 100644 index 000000000..98a1eb806 --- /dev/null +++ b/src/features/opencode-skill-loader/config-source-discovery.test.ts @@ -0,0 +1,68 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test" +import { mkdirSync, rmSync, writeFileSync } from "fs" +import { join } from "path" +import { tmpdir } from "os" +import { discoverConfigSourceSkills } from "./config-source-discovery" + +const TEST_DIR = join(tmpdir(), `config-source-discovery-test-${Date.now()}`) + +function writeSkill(path: string, name: string, description: string): void { + mkdirSync(path, { recursive: true }) + writeFileSync( + join(path, "SKILL.md"), + `---\nname: ${name}\ndescription: ${description}\n---\nBody\n`, + ) +} + +describe("config source discovery", () => { + beforeEach(() => { + mkdirSync(TEST_DIR, { recursive: true }) + }) + + afterEach(() => { + rmSync(TEST_DIR, { recursive: true, force: true }) + }) + + it("loads skills from local sources path", async () => { + // given + const configDir = join(TEST_DIR, "config") + const sourceDir = join(configDir, "custom-skills") + writeSkill(join(sourceDir, "local-skill"), "local-skill", "Loaded from local source") + + // when + const skills = await discoverConfigSourceSkills({ + config: { + sources: [{ path: "./custom-skills", recursive: true }], + }, + configDir, + }) + + // then + const localSkill = skills.find((skill) => skill.name === "local-skill") + expect(localSkill).toBeDefined() + expect(localSkill?.scope).toBe("config") + expect(localSkill?.definition.description).toContain("Loaded from local source") + }) + + it("filters discovered skills using source glob", async () => { + // given + const configDir = join(TEST_DIR, "config") + const sourceDir = join(configDir, "custom-skills") + + writeSkill(join(sourceDir, "keep", "kept"), "kept-skill", "Should be kept") + writeSkill(join(sourceDir, "skip", "skipped"), "skipped-skill", "Should be skipped") + + // when + const skills = await discoverConfigSourceSkills({ + config: { + sources: [{ path: "./custom-skills", recursive: true, glob: "keep/**" }], + }, + configDir, + }) + + // then + const names = skills.map((skill) => skill.name) + expect(names).toContain("keep/kept-skill") + expect(names).not.toContain("skip/skipped-skill") + }) +}) diff --git a/src/features/opencode-skill-loader/config-source-discovery.ts b/src/features/opencode-skill-loader/config-source-discovery.ts new file mode 100644 index 000000000..198bce852 --- /dev/null +++ b/src/features/opencode-skill-loader/config-source-discovery.ts @@ -0,0 +1,101 @@ +import { promises as fs } from "fs" +import { dirname, extname, isAbsolute, join, relative } from "path" +import picomatch from "picomatch" +import type { SkillsConfig } from "../../config/schema" +import { normalizeSkillsConfig } from "./merger/skills-config-normalizer" +import { deduplicateSkillsByName } from "./skill-deduplication" +import { loadSkillsFromDir } from "./skill-directory-loader" +import { inferSkillNameFromFileName, loadSkillFromPath } from "./loaded-skill-from-path" +import type { LoadedSkill } from "./types" + +const MAX_RECURSIVE_DEPTH = 10 + +function isHttpUrl(path: string): boolean { + return path.startsWith("http://") || path.startsWith("https://") +} + +function toAbsolutePath(path: string, configDir: string): string { + if (isAbsolute(path)) { + return path + } + return join(configDir, path) +} + +function isMarkdownPath(path: string): boolean { + return extname(path).toLowerCase() === ".md" +} + +function filterByGlob(skills: LoadedSkill[], sourceBaseDir: string, globPattern?: string): LoadedSkill[] { + if (!globPattern) return skills + + return skills.filter((skill) => { + if (!skill.path) return false + const rel = relative(sourceBaseDir, skill.path) + return picomatch.isMatch(rel, globPattern, { dot: true, bash: true }) + }) +} + +async function loadSourcePath(options: { + sourcePath: string + recursive: boolean + globPattern?: string + configDir: string +}): Promise { + if (isHttpUrl(options.sourcePath)) { + return [] + } + + const absolutePath = toAbsolutePath(options.sourcePath, options.configDir) + const stat = await fs.stat(absolutePath).catch(() => null) + if (!stat) return [] + + if (stat.isFile()) { + if (!isMarkdownPath(absolutePath)) return [] + const loaded = await loadSkillFromPath({ + skillPath: absolutePath, + resolvedPath: dirname(absolutePath), + defaultName: inferSkillNameFromFileName(absolutePath), + scope: "config", + }) + if (!loaded) return [] + return filterByGlob([loaded], dirname(absolutePath), options.globPattern) + } + + if (!stat.isDirectory()) return [] + + const directorySkills = await loadSkillsFromDir({ + skillsDir: absolutePath, + scope: "config", + maxDepth: options.recursive ? MAX_RECURSIVE_DEPTH : 0, + }) + return filterByGlob(directorySkills, absolutePath, options.globPattern) +} + +export async function discoverConfigSourceSkills(options: { + config: SkillsConfig | undefined + configDir: string +}): Promise { + const normalized = normalizeSkillsConfig(options.config) + if (normalized.sources.length === 0) return [] + + const loadedBySource = await Promise.all( + normalized.sources.map((source) => { + if (typeof source === "string") { + return loadSourcePath({ + sourcePath: source, + recursive: false, + configDir: options.configDir, + }) + } + + return loadSourcePath({ + sourcePath: source.path, + recursive: source.recursive ?? false, + globPattern: source.glob, + configDir: options.configDir, + }) + }), + ) + + return deduplicateSkillsByName(loadedBySource.flat()) +} diff --git a/src/features/opencode-skill-loader/index.ts b/src/features/opencode-skill-loader/index.ts index 68c556245..94ce077f3 100644 --- a/src/features/opencode-skill-loader/index.ts +++ b/src/features/opencode-skill-loader/index.ts @@ -14,3 +14,4 @@ export * from "./skill-discovery" export * from "./skill-resolution-options" export * from "./loaded-skill-template-extractor" export * from "./skill-template-resolver" +export * from "./config-source-discovery" diff --git a/src/features/opencode-skill-loader/merger.test.ts b/src/features/opencode-skill-loader/merger.test.ts new file mode 100644 index 000000000..0b5a3f649 --- /dev/null +++ b/src/features/opencode-skill-loader/merger.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "bun:test" +import type { BuiltinSkill } from "../builtin-skills/types" +import type { CommandDefinition } from "../claude-code-command-loader/types" +import { mergeSkills } from "./merger" +import type { LoadedSkill, SkillScope } from "./types" + +function createLoadedSkill(scope: SkillScope, name: string, description: string): LoadedSkill { + const definition: CommandDefinition = { + name, + description, + template: "template", + } + + return { + name, + definition, + scope, + } +} + +describe("mergeSkills", () => { + it("gives higher scopes priority over config source skills", () => { + // given + const builtinSkills: BuiltinSkill[] = [ + { + name: "priority-skill", + description: "builtin", + template: "builtin-template", + }, + ] + + const configSourceSkills: LoadedSkill[] = [ + createLoadedSkill("config", "priority-skill", "config source"), + ] + const userSkills: LoadedSkill[] = [ + createLoadedSkill("user", "priority-skill", "user skill"), + ] + + // when + const merged = mergeSkills( + builtinSkills, + undefined, + configSourceSkills, + userSkills, + [], + [], + [], + ) + + // then + expect(merged).toHaveLength(1) + expect(merged[0]?.scope).toBe("user") + expect(merged[0]?.definition.description).toBe("user skill") + }) +}) diff --git a/src/features/opencode-skill-loader/merger.ts b/src/features/opencode-skill-loader/merger.ts index c5598ca5d..04ff351c5 100644 --- a/src/features/opencode-skill-loader/merger.ts +++ b/src/features/opencode-skill-loader/merger.ts @@ -14,6 +14,7 @@ export interface MergeSkillsOptions { export function mergeSkills( builtinSkills: BuiltinSkill[], config: SkillsConfig | undefined, + configSourceSkills: LoadedSkill[], userClaudeSkills: LoadedSkill[], userOpencodeSkills: LoadedSkill[], projectClaudeSkills: LoadedSkill[], @@ -47,6 +48,7 @@ export function mergeSkills( } const fileSystemSkills = [ + ...configSourceSkills, ...userClaudeSkills, ...userOpencodeSkills, ...projectClaudeSkills, diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index 240cdab52..d08094551 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -4,6 +4,7 @@ import type { OhMyOpenCodeConfig } from "../config"; import { log, migrateAgentConfig } from "../shared"; import { AGENT_NAME_MAP } from "../shared/migration"; import { + discoverConfigSourceSkills, discoverOpencodeGlobalSkills, discoverOpencodeProjectSkills, discoverProjectClaudeSkills, @@ -34,11 +35,16 @@ export async function applyAgentConfig(params: { const includeClaudeSkillsForAwareness = params.pluginConfig.claude_code?.skills ?? true; const [ + discoveredConfigSourceSkills, discoveredUserSkills, discoveredProjectSkills, discoveredOpencodeGlobalSkills, discoveredOpencodeProjectSkills, ] = await Promise.all([ + discoverConfigSourceSkills({ + config: params.pluginConfig.skills, + configDir: params.ctx.directory, + }), includeClaudeSkillsForAwareness ? discoverUserClaudeSkills() : Promise.resolve([]), includeClaudeSkillsForAwareness ? discoverProjectClaudeSkills() @@ -48,6 +54,7 @@ export async function applyAgentConfig(params: { ]); const allDiscoveredSkills = [ + ...discoveredConfigSourceSkills, ...discoveredOpencodeProjectSkills, ...discoveredProjectSkills, ...discoveredOpencodeGlobalSkills, diff --git a/src/plugin-handlers/command-config-handler.ts b/src/plugin-handlers/command-config-handler.ts index 1e2ca9402..983c25a04 100644 --- a/src/plugin-handlers/command-config-handler.ts +++ b/src/plugin-handlers/command-config-handler.ts @@ -7,16 +7,19 @@ import { } from "../features/claude-code-command-loader"; import { loadBuiltinCommands } from "../features/builtin-commands"; import { + discoverConfigSourceSkills, loadUserSkills, loadProjectSkills, loadOpencodeGlobalSkills, loadOpencodeProjectSkills, + skillsToCommandDefinitionRecord, } from "../features/opencode-skill-loader"; import type { PluginComponents } from "./plugin-components-loader"; export async function applyCommandConfig(params: { config: Record; pluginConfig: OhMyOpenCodeConfig; + ctx: { directory: string }; pluginComponents: PluginComponents; }): Promise { const builtinCommands = loadBuiltinCommands(params.pluginConfig.disabled_commands); @@ -26,6 +29,7 @@ export async function applyCommandConfig(params: { const includeClaudeSkills = params.pluginConfig.claude_code?.skills ?? true; const [ + configSourceSkills, userCommands, projectCommands, opencodeGlobalCommands, @@ -35,6 +39,10 @@ export async function applyCommandConfig(params: { opencodeGlobalSkills, opencodeProjectSkills, ] = await Promise.all([ + discoverConfigSourceSkills({ + config: params.pluginConfig.skills, + configDir: params.ctx.directory, + }), includeClaudeCommands ? loadUserCommands() : Promise.resolve({}), includeClaudeCommands ? loadProjectCommands() : Promise.resolve({}), loadOpencodeGlobalCommands(), @@ -47,6 +55,7 @@ export async function applyCommandConfig(params: { params.config.command = { ...builtinCommands, + ...skillsToCommandDefinitionRecord(configSourceSkills), ...userCommands, ...userSkills, ...opencodeGlobalCommands, diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index 5eb7f242b..e9b814a4f 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -33,7 +33,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { applyToolConfig({ config, pluginConfig, agentResult }); await applyMcpConfig({ config, pluginConfig, pluginComponents }); - await applyCommandConfig({ config, pluginConfig, pluginComponents }); + await applyCommandConfig({ config, pluginConfig, ctx, pluginComponents }); log("[config-handler] config handler applied", { agentCount: Object.keys(agentResult).length, diff --git a/src/plugin/skill-context.ts b/src/plugin/skill-context.ts index 634cbc594..630cc2085 100644 --- a/src/plugin/skill-context.ts +++ b/src/plugin/skill-context.ts @@ -7,6 +7,7 @@ import type { } from "../features/opencode-skill-loader/types" import { + discoverConfigSourceSkills, discoverUserClaudeSkills, discoverProjectClaudeSkills, discoverOpencodeGlobalSkills, @@ -54,8 +55,12 @@ export async function createSkillContext(args: { }) const includeClaudeSkills = pluginConfig.claude_code?.skills !== false - const [userSkills, globalSkills, projectSkills, opencodeProjectSkills] = + const [configSourceSkills, userSkills, globalSkills, projectSkills, opencodeProjectSkills] = await Promise.all([ + discoverConfigSourceSkills({ + config: pluginConfig.skills, + configDir: directory, + }), includeClaudeSkills ? discoverUserClaudeSkills() : Promise.resolve([]), discoverOpencodeGlobalSkills(), includeClaudeSkills ? discoverProjectClaudeSkills() : Promise.resolve([]), @@ -65,6 +70,7 @@ export async function createSkillContext(args: { const mergedSkills = mergeSkills( builtinSkills, pluginConfig.skills, + configSourceSkills, userSkills, globalSkills, projectSkills, From 5a83c61d77349f9a988ebc15871d49f94b9a0532 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 13 Feb 2026 11:17:18 +0900 Subject: [PATCH 3/3] fix(skills): normalize windows separators for source globs --- .../config-source-discovery.test.ts | 28 ++++++++++++++----- .../config-source-discovery.ts | 6 +++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/features/opencode-skill-loader/config-source-discovery.test.ts b/src/features/opencode-skill-loader/config-source-discovery.test.ts index 98a1eb806..d10303ce0 100644 --- a/src/features/opencode-skill-loader/config-source-discovery.test.ts +++ b/src/features/opencode-skill-loader/config-source-discovery.test.ts @@ -2,7 +2,8 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test" import { mkdirSync, rmSync, writeFileSync } from "fs" import { join } from "path" import { tmpdir } from "os" -import { discoverConfigSourceSkills } from "./config-source-discovery" +import { SkillsConfigSchema } from "../../config/schema/skills" +import { discoverConfigSourceSkills, normalizePathForGlob } from "./config-source-discovery" const TEST_DIR = join(tmpdir(), `config-source-discovery-test-${Date.now()}`) @@ -28,12 +29,13 @@ describe("config source discovery", () => { const configDir = join(TEST_DIR, "config") const sourceDir = join(configDir, "custom-skills") writeSkill(join(sourceDir, "local-skill"), "local-skill", "Loaded from local source") + const config = SkillsConfigSchema.parse({ + sources: [{ path: "./custom-skills", recursive: true }], + }) // when const skills = await discoverConfigSourceSkills({ - config: { - sources: [{ path: "./custom-skills", recursive: true }], - }, + config, configDir, }) @@ -51,12 +53,13 @@ describe("config source discovery", () => { writeSkill(join(sourceDir, "keep", "kept"), "kept-skill", "Should be kept") writeSkill(join(sourceDir, "skip", "skipped"), "skipped-skill", "Should be skipped") + const config = SkillsConfigSchema.parse({ + sources: [{ path: "./custom-skills", recursive: true, glob: "keep/**" }], + }) // when const skills = await discoverConfigSourceSkills({ - config: { - sources: [{ path: "./custom-skills", recursive: true, glob: "keep/**" }], - }, + config, configDir, }) @@ -65,4 +68,15 @@ describe("config source discovery", () => { expect(names).toContain("keep/kept-skill") expect(names).not.toContain("skip/skipped-skill") }) + + it("normalizes windows separators before glob matching", () => { + // given + const windowsPath = "keep\\nested\\SKILL.md" + + // when + const normalized = normalizePathForGlob(windowsPath) + + // then + expect(normalized).toBe("keep/nested/SKILL.md") + }) }) diff --git a/src/features/opencode-skill-loader/config-source-discovery.ts b/src/features/opencode-skill-loader/config-source-discovery.ts index 198bce852..df3ee653e 100644 --- a/src/features/opencode-skill-loader/config-source-discovery.ts +++ b/src/features/opencode-skill-loader/config-source-discovery.ts @@ -25,12 +25,16 @@ function isMarkdownPath(path: string): boolean { return extname(path).toLowerCase() === ".md" } +export function normalizePathForGlob(path: string): string { + return path.split("\\").join("/") +} + function filterByGlob(skills: LoadedSkill[], sourceBaseDir: string, globPattern?: string): LoadedSkill[] { if (!globPattern) return skills return skills.filter((skill) => { if (!skill.path) return false - const rel = relative(sourceBaseDir, skill.path) + const rel = normalizePathForGlob(relative(sourceBaseDir, skill.path)) return picomatch.isMatch(rel, globPattern, { dot: true, bash: true }) }) }