Skip to main content

acorn/schema/agent/
opencode.rs

1//! OpenCode configuration data model
2//!
3//! See <https://opencode.ai/docs/config> for more information on OpenCode configuration.
4use crate::error::ApiResult;
5use crate::io::{parse_jsonc_cst, read_file, write_file, CstRootNode, FromPath, InputOutput};
6#[cfg(feature = "std")]
7use crate::prelude::Path;
8use crate::prelude::PathBuf;
9use crate::schema::agent::Provider;
10use crate::util::MimeType;
11use alloc::collections::BTreeMap;
12use color_eyre::eyre::eyre;
13use directories::BaseDirs;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use serde_with::skip_serializing_none;
17
18/// Permission rules keyed by tool or command pattern
19pub type PermissionObjectConfig = StringMap<PermissionRuleConfig>;
20/// String-keyed map used throughout OpenCode config sections
21pub type StringMap<T> = BTreeMap<String, T>;
22/// Agent availability mode
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum AgentMode {
26    /// Available only as a subagent
27    Subagent,
28    /// Available only as a primary agent
29    Primary,
30    /// Available as both a primary agent and subagent
31    All,
32}
33/// Automatic update behavior
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(untagged)]
36pub enum AutoUpdateConfig {
37    /// Enable or disable automatic updates
38    Bool(bool),
39    /// Show update notifications without installing updates
40    Notify(Notify),
41}
42/// Value that can be a simple boolean or named configuration map
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(untagged)]
45pub enum BoolOrMap<T> {
46    /// Enables or disables built-in behavior
47    Bool(bool),
48    /// Enables built-ins with named overrides or custom entries
49    Map(StringMap<T>),
50}
51/// Layout setting (deprecated; no longer configurable)
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum LayoutConfig {
55    /// Automatic layout selection
56    Auto,
57    /// Stretch layout
58    Stretch,
59}
60/// Runtime log level
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum LogLevel {
63    /// Debug-level logging
64    DEBUG,
65    /// Informational logging
66    INFO,
67    /// Warning-level logging
68    WARN,
69    /// Error-level logging
70    ERROR,
71}
72/// Language server configuration entry
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(untagged)]
75pub enum LspConfig {
76    /// Disables a named language server
77    Disabled {
78        /// Whether the language server is disabled
79        disabled: bool,
80    },
81    /// Configures a language server process
82    Server(LspServerConfig),
83}
84/// Model Context Protocol server configuration
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(tag = "type")]
87pub enum McpConfig {
88    /// Local MCP server launched by OpenCode
89    #[serde(rename = "local")]
90    Local(McpLocalConfig),
91    /// Remote MCP server reached over HTTP
92    #[serde(rename = "remote")]
93    Remote(McpRemoteConfig),
94    /// Minimal entry that only toggles an inherited server
95    #[serde(untagged)]
96    EnabledOnly {
97        /// Whether the MCP server is enabled on startup
98        enabled: bool,
99    },
100}
101/// OAuth setting for a remote MCP server
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(untagged)]
104pub enum McpOAuthOrFalse {
105    /// OAuth client configuration
106    Config(McpOAuthConfig),
107    /// Disables OAuth auto-detection when set to false
108    Disabled(bool),
109}
110/// Update notification mode
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "lowercase")]
113pub enum Notify {
114    /// Notify when an update is available
115    Notify,
116}
117/// Permission action for a tool or command
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(rename_all = "lowercase")]
120pub enum PermissionAction {
121    /// Ask for approval before running
122    Ask,
123    /// Allow without approval
124    Allow,
125    /// Deny the operation
126    Deny,
127}
128/// Global permission configuration
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(untagged)]
131pub enum PermissionConfig {
132    /// Single action applied broadly
133    Action(PermissionAction),
134    /// Tool-specific permission rules
135    Object(PermissionObjectConfig),
136}
137/// Permission rule for a tool or nested command pattern
138#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(untagged)]
140pub enum PermissionRuleConfig {
141    /// Single action for this rule
142    Action(PermissionAction),
143    /// Nested command-pattern actions for this rule
144    Object(StringMap<PermissionAction>),
145}
146/// Plugin reference
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(untagged)]
149pub enum PluginConfig {
150    /// Package or plugin name
151    Name(String),
152    /// Package or plugin name with options
153    WithOptions(String, Value),
154}
155/// Experimental policy action
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub enum PolicyAction {
158    /// Controls whether a provider may be used
159    #[serde(rename = "provider.use")]
160    ProviderUse,
161}
162/// Experimental policy effect
163#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(rename_all = "lowercase")]
165pub enum PolicyEffect {
166    /// Allows the action
167    Allow,
168    /// Denies the action
169    Deny,
170}
171/// Named reference source
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(untagged)]
174pub enum ReferenceConfig {
175    /// Reference expressed as a string
176    String(String),
177    /// Reference to a git repository
178    Git(ReferenceGit),
179    /// Reference to a local path
180    Local(ReferenceLocal),
181}
182/// Conversation sharing behavior
183#[derive(Debug, Clone, Serialize, Deserialize)]
184#[serde(rename_all = "lowercase")]
185pub enum ShareConfig {
186    /// Share only when explicitly requested
187    Manual,
188    /// Automatically share new conversations
189    Auto,
190    /// Disable sharing
191    Disabled,
192}
193/// Specialized agent configuration
194#[derive(Debug, Clone, Serialize, Deserialize, Default)]
195pub struct AgentConfig {
196    /// Model to use for this agent
197    pub model: Option<String>,
198    /// Default variant for the configured model
199    pub variant: Option<String>,
200    /// Sampling temperature for this agent
201    pub temperature: Option<f64>,
202    /// Top-p sampling value for this agent
203    pub top_p: Option<f64>,
204    /// System prompt or instructions for this agent
205    pub prompt: Option<String>,
206    /// Per-tool enablement map
207    #[deprecated(note = "Use `tools` instead")]
208    pub tools: Option<StringMap<bool>>,
209    /// Whether this agent is disabled
210    pub disable: Option<bool>,
211    /// Description of when to use this agent
212    pub description: Option<String>,
213    /// Whether this agent is primary, subagent, or both
214    pub mode: Option<AgentMode>,
215    /// Whether to hide this subagent from autocomplete
216    pub hidden: Option<bool>,
217    /// Provider-specific or agent-specific options
218    pub options: Option<Value>,
219    /// Hex color or theme color for this agent
220    pub color: Option<String>,
221    /// Maximum agentic iterations before forcing a text-only response
222    pub steps: Option<u64>,
223    /// Maximum step count
224    #[deprecated(note = "Use `steps` instead")]
225    #[serde(rename = "maxSteps")]
226    pub max_steps: Option<u64>,
227    /// Permission rules that apply to this agent
228    pub permission: Option<PermissionConfig>,
229}
230/// Attachment processing configuration
231#[derive(Debug, Clone, Serialize, Deserialize, Default)]
232#[serde(deny_unknown_fields)]
233pub struct AttachmentConfig {
234    /// Image attachment limits and resizing behavior
235    pub image: Option<ImageAttachmentConfig>,
236}
237/// Server and runtime OpenCode configuration
238#[skip_serializing_none]
239#[derive(Debug, Clone, Serialize, Deserialize, Default)]
240#[serde(deny_unknown_fields)]
241pub struct Config {
242    /// CST root for JSONC comment-preserving round-trips
243    #[serde(skip)]
244    pub cst: Option<CstRootNode>,
245    /// Source path used to load this configuration
246    #[serde(skip)]
247    pub path: Option<PathBuf>,
248    /// Named agent configurations
249    pub agent: Option<StringMap<AgentConfig>>,
250    /// Attachment processing configuration
251    pub attachment: Option<AttachmentConfig>,
252    /// Automatic sharing flag
253    #[deprecated(note = "Use `share` instead")]
254    pub autoshare: Option<bool>,
255    /// Automatic update behavior
256    pub autoupdate: Option<AutoUpdateConfig>,
257    /// Custom command configuration
258    pub command: Option<StringMap<CommandConfig>>,
259    /// Context compaction behavior
260    pub compaction: Option<CompactionConfig>,
261    /// Default primary agent used when none is specified
262    pub default_agent: Option<String>,
263    /// Provider IDs that should not be loaded
264    pub disabled_providers: Option<Vec<String>>,
265    /// Provider IDs that are allowed when an allowlist is desired
266    pub enabled_providers: Option<Vec<String>>,
267    /// Enterprise configuration
268    pub enterprise: Option<EnterpriseConfig>,
269    /// Experimental configuration
270    pub experimental: Option<ExperimentalConfig>,
271    /// Formatter enablement or formatter overrides
272    pub formatter: Option<BoolOrMap<FormatterConfig>>,
273    /// Instruction files or glob patterns to include
274    pub instructions: Option<Vec<String>>,
275    /// Layout option
276    #[deprecated(note = "Remove this field; layout is no longer configurable")]
277    pub layout: Option<LayoutConfig>,
278    /// Runtime log level
279    #[serde(rename = "logLevel")]
280    pub log_level: Option<LogLevel>,
281    /// LSP enablement or LSP server overrides
282    pub lsp: Option<BoolOrMap<LspConfig>>,
283    /// MCP server configurations
284    pub mcp: Option<StringMap<McpConfig>>,
285    /// Agent configuration map
286    #[deprecated(note = "Use `agent` instead")]
287    pub mode: Option<StringMap<AgentConfig>>,
288    /// Main model in `provider/model` format
289    pub model: Option<String>,
290    /// Permission rules for tools and operations
291    pub permission: Option<PermissionConfig>,
292    /// Plugins loaded from packages or configured with options
293    pub plugin: Option<Vec<PluginConfig>>,
294    /// Custom provider configuration and model overrides
295    pub provider: Option<StringMap<Value>>,
296    /// Named references field
297    #[deprecated(note = "Use `references` instead")]
298    pub reference: Option<StringMap<ReferenceConfig>>,
299    /// Named git or local directory references
300    pub references: Option<StringMap<ReferenceConfig>>,
301    /// JSON schema reference for config validation
302    #[serde(rename = "$schema")]
303    pub schema: Option<String>,
304    /// Server settings for `opencode serve` and `opencode web`
305    pub server: Option<ServerConfig>,
306    /// Conversation sharing behavior
307    pub share: Option<ShareConfig>,
308    /// Default shell used for terminal and agent tool calls
309    pub shell: Option<String>,
310    /// Additional skill paths or URLs
311    pub skills: Option<SkillsConfig>,
312    /// Small model for lightweight tasks such as title generation
313    pub small_model: Option<String>,
314    /// Whether filesystem snapshots are recorded for undo and revert
315    pub snapshot: Option<bool>,
316    /// Tool output truncation thresholds
317    pub tool_output: Option<ToolOutputConfig>,
318    /// Tool enablement map
319    pub tools: Option<StringMap<bool>>,
320    /// Username displayed in conversations
321    pub username: Option<String>,
322    /// File watcher settings
323    pub watcher: Option<WatcherConfig>,
324}
325/// Custom command configuration
326#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(deny_unknown_fields)]
328pub struct CommandConfig {
329    /// Prompt template for the command
330    pub template: String,
331    /// Human-readable command description
332    pub description: Option<String>,
333    /// Agent to run the command with
334    pub agent: Option<String>,
335    /// Model to run the command with
336    pub model: Option<String>,
337    /// Model variant to run the command with
338    pub variant: Option<String>,
339    /// Whether the command should run as a subtask
340    pub subtask: Option<bool>,
341}
342/// Context compaction behavior
343#[derive(Debug, Clone, Serialize, Deserialize, Default)]
344#[serde(deny_unknown_fields)]
345pub struct CompactionConfig {
346    /// Whether to compact automatically when context is full
347    pub auto: Option<bool>,
348    /// Whether to prune old tool outputs to save tokens
349    pub prune: Option<bool>,
350    /// Number of recent user turns to keep verbatim
351    pub tail_turns: Option<u64>,
352    /// Maximum recent-turn tokens to preserve verbatim
353    pub preserve_recent_tokens: Option<u64>,
354    /// Token buffer reserved for compaction
355    pub reserved: Option<u64>,
356}
357/// Enterprise configuration
358#[derive(Debug, Clone, Serialize, Deserialize, Default)]
359#[serde(deny_unknown_fields)]
360pub struct EnterpriseConfig {
361    /// Enterprise service URL
362    pub url: Option<String>,
363}
364/// Experimental settings that may change without notice
365#[derive(Debug, Clone, Serialize, Deserialize, Default)]
366#[serde(deny_unknown_fields)]
367pub struct ExperimentalConfig {
368    /// Whether paste summaries are disabled
369    pub disable_paste_summary: Option<bool>,
370    /// Whether the batch tool is enabled
371    pub batch_tool: Option<bool>,
372    /// Whether OpenTelemetry spans are enabled for AI SDK calls
373    #[serde(rename = "openTelemetry")]
374    pub open_telemetry: Option<bool>,
375    /// Tools that should only be available to primary agents
376    pub primary_tools: Option<Vec<String>>,
377    /// Whether the agent loop continues after a tool call is denied
378    pub continue_loop_on_deny: Option<bool>,
379    /// Timeout in milliseconds for MCP requests
380    pub mcp_timeout: Option<u64>,
381    /// Policy statements for supported resources such as providers
382    pub policies: Option<Vec<ExperimentalPolicy>>,
383}
384/// Experimental policy statement
385#[derive(Debug, Clone, Serialize, Deserialize)]
386#[serde(deny_unknown_fields)]
387pub struct ExperimentalPolicy {
388    /// Action controlled by the policy
389    pub action: PolicyAction,
390    /// Whether the action is allowed or denied
391    pub effect: PolicyEffect,
392    /// Resource affected by the policy
393    pub resource: Provider,
394}
395/// Formatter command configuration
396#[derive(Debug, Clone, Serialize, Deserialize, Default)]
397#[serde(deny_unknown_fields)]
398pub struct FormatterConfig {
399    /// Whether this formatter is disabled
400    pub disabled: Option<bool>,
401    /// Command and arguments used to run the formatter
402    pub command: Option<Vec<String>>,
403    /// Environment variables for the formatter process
404    pub environment: Option<StringMap<String>>,
405    /// File extensions handled by this formatter
406    pub extensions: Option<Vec<String>>,
407}
408/// Image attachment limits
409#[derive(Debug, Clone, Serialize, Deserialize, Default)]
410#[serde(deny_unknown_fields)]
411pub struct ImageAttachmentConfig {
412    /// Whether oversized images are resized before provider requests
413    pub auto_resize: Option<bool>,
414    /// Maximum image width before resizing or rejection
415    pub max_width: Option<u64>,
416    /// Maximum image height before resizing or rejection
417    pub max_height: Option<u64>,
418    /// Maximum base64 payload size before resizing or rejection
419    pub max_base64_bytes: Option<u64>,
420}
421/// Language server process configuration
422#[derive(Debug, Clone, Serialize, Deserialize)]
423#[serde(deny_unknown_fields)]
424pub struct LspServerConfig {
425    /// Command and arguments used to run the language server
426    pub command: Vec<String>,
427    /// File extensions handled by the language server
428    pub extensions: Option<Vec<String>>,
429    /// Whether this language server is disabled
430    pub disabled: Option<bool>,
431    /// Environment variables for the language server process
432    pub env: Option<StringMap<String>>,
433    /// Initialization options sent to the language server
434    pub initialization: Option<Value>,
435}
436/// Local MCP server process configuration
437#[derive(Debug, Clone, Serialize, Deserialize)]
438#[serde(deny_unknown_fields)]
439pub struct McpLocalConfig {
440    /// Command and arguments used to run the MCP server
441    pub command: Vec<String>,
442    /// Working directory for the MCP server process
443    pub cwd: Option<String>,
444    /// Environment variables for the MCP server process
445    pub environment: Option<StringMap<String>>,
446    /// Whether the MCP server is enabled on startup
447    pub enabled: Option<bool>,
448    /// Request timeout in milliseconds
449    pub timeout: Option<u64>,
450}
451/// OAuth configuration for a remote MCP server
452#[derive(Debug, Clone, Serialize, Deserialize, Default)]
453#[serde(deny_unknown_fields)]
454pub struct McpOAuthConfig {
455    /// OAuth client ID
456    #[serde(rename = "clientId")]
457    pub client_id: Option<String>,
458    /// OAuth client secret
459    #[serde(rename = "clientSecret")]
460    pub client_secret: Option<String>,
461    /// OAuth scopes requested during authorization
462    pub scope: Option<String>,
463    /// Local OAuth callback server port
464    #[serde(rename = "callbackPort")]
465    pub callback_port: Option<u16>,
466    /// OAuth redirect URI
467    #[serde(rename = "redirectUri")]
468    pub redirect_uri: Option<String>,
469}
470/// Remote MCP server configuration
471#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(deny_unknown_fields)]
473pub struct McpRemoteConfig {
474    /// URL of the remote MCP server
475    pub url: String,
476    /// Whether the MCP server is enabled on startup
477    pub enabled: Option<bool>,
478    /// Headers sent with remote MCP requests
479    pub headers: Option<StringMap<String>>,
480    /// OAuth configuration or disabled flag
481    pub oauth: Option<McpOAuthOrFalse>,
482    /// Request timeout in milliseconds
483    pub timeout: Option<u64>,
484}
485/// Git repository reference
486#[derive(Debug, Clone, Serialize, Deserialize)]
487#[serde(deny_unknown_fields)]
488pub struct ReferenceGit {
489    /// Repository URL or identifier
490    pub repository: String,
491    /// Branch to use from the repository
492    pub branch: Option<String>,
493    /// Description of this reference
494    pub description: Option<String>,
495    /// Whether this reference is hidden from selection surfaces
496    pub hidden: Option<bool>,
497}
498/// Local directory reference
499#[derive(Debug, Clone, Serialize, Deserialize)]
500#[serde(deny_unknown_fields)]
501pub struct ReferenceLocal {
502    /// Local path to the reference
503    pub path: String,
504    /// Description of this reference
505    pub description: Option<String>,
506    /// Whether this reference is hidden from selection surfaces
507    pub hidden: Option<bool>,
508}
509/// HTTP server settings
510#[derive(Debug, Clone, Serialize, Deserialize, Default)]
511#[serde(deny_unknown_fields)]
512pub struct ServerConfig {
513    /// Port to listen on
514    pub port: Option<u64>,
515    /// Hostname to listen on
516    pub hostname: Option<String>,
517    /// Whether mDNS service discovery is enabled
518    pub mdns: Option<bool>,
519    /// Custom mDNS domain name
520    #[serde(rename = "mdnsDomain")]
521    pub mdns_domain: Option<String>,
522    /// Additional CORS origins allowed for browser clients
523    pub cors: Option<Vec<String>>,
524}
525/// Additional skill sources
526#[derive(Debug, Clone, Serialize, Deserialize, Default)]
527#[serde(deny_unknown_fields)]
528pub struct SkillsConfig {
529    /// Additional paths to skill folders
530    pub paths: Option<Vec<String>>,
531    /// URLs used to fetch skills
532    pub urls: Option<Vec<String>>,
533}
534/// Tool output truncation thresholds
535#[derive(Debug, Clone, Serialize, Deserialize, Default)]
536#[serde(deny_unknown_fields)]
537pub struct ToolOutputConfig {
538    /// Maximum preview lines before full output is saved to disk
539    pub max_lines: Option<u64>,
540    /// Maximum preview bytes before full output is saved to disk
541    pub max_bytes: Option<u64>,
542}
543/// File watcher configuration
544#[derive(Debug, Clone, Serialize, Deserialize, Default)]
545#[serde(deny_unknown_fields)]
546pub struct WatcherConfig {
547    /// Glob patterns ignored by the file watcher
548    pub ignore: Option<Vec<String>>,
549}
550impl Config {
551    /// Load OpenCode configuration from a path or return an empty configuration.
552    #[cfg(feature = "std")]
553    pub fn load(path: impl AsRef<Path>) -> Self {
554        path.as_ref()
555            .is_file()
556            .then(|| Self::read(path.as_ref()).ok())
557            .flatten()
558            .unwrap_or_default()
559    }
560    /// Replace the configured providers and return the updated configuration.
561    pub fn with_provider(self, provider: StringMap<Value>) -> Self {
562        Self {
563            provider: Some(provider),
564            ..self
565        }
566    }
567    /// Set the primary model and return the updated configuration.
568    pub fn with_model(self, model: String) -> Self {
569        Self { model: Some(model), ..self }
570    }
571    /// Parse OpenCode configuration from a JSON string
572    fn parse_json(content: impl AsRef<str>) -> ApiResult<Self> {
573        serde_json::from_str(content.as_ref()).map_err(|why| eyre!("JSON parse error — {why}"))
574    }
575    /// Parse OpenCode configuration from a JSONC string
576    /// ### Note
577    /// Supports JavaScript-style comments (`//` and `/* */`) and trailing commas.
578    /// Comments are preserved in the CST and restored on write.
579    pub fn parse_jsonc(content: &str) -> ApiResult<Self> {
580        parse_jsonc_cst::<Config>(content).map(|(mut config, cst)| {
581            config.cst = Some(cst);
582            config
583        })
584    }
585    /// Load OpenCode configuration from a JSONC file path
586    #[cfg(feature = "std")]
587    pub fn from_path(path: impl AsRef<Path>) -> ApiResult<Self> {
588        Self::read(path.as_ref())
589    }
590    /// Discover OpenCode configuration in standard locations
591    /// ### Note
592    /// Search order is (in order of precedence):
593    /// 1. `opencode.jsonc` in the working directory (per-project)
594    /// 2. `opencode.json` in the working directory (per-project)
595    /// 3. `~/.config/opencode/opencode.jsonc` (user-level)
596    /// 4. `~/.config/opencode/opencode.json` (user-level)
597    ///
598    /// Returns the first successfully parsed configuration, or `None` if not found.
599    #[cfg(feature = "std")]
600    pub fn resolve() -> Option<Self> {
601        let home = BaseDirs::new().map(|dirs| dirs.config_dir().to_path_buf());
602        let candidates = [
603            Some(PathBuf::from("opencode.jsonc")),
604            Some(PathBuf::from("opencode.json")),
605            home.as_ref().map(|p| p.join("opencode").join("opencode.jsonc")),
606            home.as_ref().map(|p| p.join("opencode").join("opencode.json")),
607        ];
608        candidates
609            .into_iter()
610            .flatten()
611            .find_map(|path| if path.is_file() { Self::read(&path).ok() } else { None })
612    }
613    /// Serialize to JSONC string, preserving comments from the original parse if available
614    fn to_jsonc_string(&self) -> ApiResult<String> {
615        match &self.cst {
616            | Some(cst) => Ok(cst.to_string()),
617            | None => serde_json::to_string_pretty(self).map_err(|why| eyre!("Failed to serialize JSONC config — {why}")),
618        }
619    }
620}
621impl InputOutput for Config {
622    /// Read and parse OpenCode configuration file (JSON or JSONC)
623    fn read(path: impl Into<PathBuf>) -> ApiResult<Self> {
624        let source = path.into();
625        match MimeType::from_path(&source) {
626            | MimeType::Json => Self::read_json(source.clone()),
627            | MimeType::Jsonc => Self::read_jsonc(source.clone()),
628            | _ => Err(eyre!("Unsupported OpenCode configuration file extension")),
629        }
630        .map(|config| Self {
631            path: Some(source),
632            ..config
633        })
634    }
635    /// Read OpenCode configuration from a JSON file
636    fn read_json(path: PathBuf) -> ApiResult<Self> {
637        read_file(path.clone())
638            .and_then(|content| Self::parse_json(&content).map_err(|why| eyre!("Failed to read JSON config `{}` — {}", path.display(), why)))
639    }
640    /// Read OpenCode configuration from a JSONC file
641    ///
642    /// Supports comments (`//` and `/* */`) and trailing commas.
643    fn read_jsonc(path: PathBuf) -> ApiResult<Self> {
644        read_file(path.clone())
645            .and_then(|content| Self::parse_jsonc(&content).map_err(|why| eyre!("Failed to read JSONC config `{}` — {}", path.display(), why)))
646    }
647    /// Read OpenCode configuration from a YAML file — not supported for OpenCode configs
648    fn read_yaml(_path: PathBuf) -> ApiResult<Self> {
649        Err(eyre!("YAML format is not supported for OpenCode configuration"))
650    }
651    /// Write OpenCode configuration to specified path (detects JSON or JSONC from extension)
652    ///
653    /// JSONC files are written as strict JSON (no comments generated).
654    fn write(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
655        let target = path.into();
656        match MimeType::from_path(&target) {
657            | MimeType::Json | MimeType::Jsonc => self.write_json(&target),
658            | _ => Err(eyre!("Unsupported OpenCode configuration file extension")),
659        }
660    }
661    /// Write OpenCode configuration as JSON to specified path
662    fn write_json(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
663        let target = path.into();
664        self.to_jsonc_string().and_then(|content| write_file(target.clone(), content))
665    }
666    /// Not supported for OpenCode configs
667    fn write_yaml(&self, _path: impl Into<PathBuf>) -> ApiResult<()> {
668        Err(eyre!("YAML format is not supported for OpenCode configuration"))
669    }
670}