1use 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
18pub type PermissionObjectConfig = StringMap<PermissionRuleConfig>;
20pub type StringMap<T> = BTreeMap<String, T>;
22#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum AgentMode {
26 Subagent,
28 Primary,
30 All,
32}
33#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(untagged)]
36pub enum AutoUpdateConfig {
37 Bool(bool),
39 Notify(Notify),
41}
42#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(untagged)]
45pub enum BoolOrMap<T> {
46 Bool(bool),
48 Map(StringMap<T>),
50}
51#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum LayoutConfig {
55 Auto,
57 Stretch,
59}
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum LogLevel {
63 DEBUG,
65 INFO,
67 WARN,
69 ERROR,
71}
72#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(untagged)]
75pub enum LspConfig {
76 Disabled {
78 disabled: bool,
80 },
81 Server(LspServerConfig),
83}
84#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(tag = "type")]
87pub enum McpConfig {
88 #[serde(rename = "local")]
90 Local(McpLocalConfig),
91 #[serde(rename = "remote")]
93 Remote(McpRemoteConfig),
94 #[serde(untagged)]
96 EnabledOnly {
97 enabled: bool,
99 },
100}
101#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(untagged)]
104pub enum McpOAuthOrFalse {
105 Config(McpOAuthConfig),
107 Disabled(bool),
109}
110#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "lowercase")]
113pub enum Notify {
114 Notify,
116}
117#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(rename_all = "lowercase")]
120pub enum PermissionAction {
121 Ask,
123 Allow,
125 Deny,
127}
128#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(untagged)]
131pub enum PermissionConfig {
132 Action(PermissionAction),
134 Object(PermissionObjectConfig),
136}
137#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(untagged)]
140pub enum PermissionRuleConfig {
141 Action(PermissionAction),
143 Object(StringMap<PermissionAction>),
145}
146#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(untagged)]
149pub enum PluginConfig {
150 Name(String),
152 WithOptions(String, Value),
154}
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub enum PolicyAction {
158 #[serde(rename = "provider.use")]
160 ProviderUse,
161}
162#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(rename_all = "lowercase")]
165pub enum PolicyEffect {
166 Allow,
168 Deny,
170}
171#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(untagged)]
174pub enum ReferenceConfig {
175 String(String),
177 Git(ReferenceGit),
179 Local(ReferenceLocal),
181}
182#[derive(Debug, Clone, Serialize, Deserialize)]
184#[serde(rename_all = "lowercase")]
185pub enum ShareConfig {
186 Manual,
188 Auto,
190 Disabled,
192}
193#[derive(Debug, Clone, Serialize, Deserialize, Default)]
195pub struct AgentConfig {
196 pub model: Option<String>,
198 pub variant: Option<String>,
200 pub temperature: Option<f64>,
202 pub top_p: Option<f64>,
204 pub prompt: Option<String>,
206 #[deprecated(note = "Use `tools` instead")]
208 pub tools: Option<StringMap<bool>>,
209 pub disable: Option<bool>,
211 pub description: Option<String>,
213 pub mode: Option<AgentMode>,
215 pub hidden: Option<bool>,
217 pub options: Option<Value>,
219 pub color: Option<String>,
221 pub steps: Option<u64>,
223 #[deprecated(note = "Use `steps` instead")]
225 #[serde(rename = "maxSteps")]
226 pub max_steps: Option<u64>,
227 pub permission: Option<PermissionConfig>,
229}
230#[derive(Debug, Clone, Serialize, Deserialize, Default)]
232#[serde(deny_unknown_fields)]
233pub struct AttachmentConfig {
234 pub image: Option<ImageAttachmentConfig>,
236}
237#[skip_serializing_none]
239#[derive(Debug, Clone, Serialize, Deserialize, Default)]
240#[serde(deny_unknown_fields)]
241pub struct Config {
242 #[serde(skip)]
244 pub cst: Option<CstRootNode>,
245 #[serde(skip)]
247 pub path: Option<PathBuf>,
248 pub agent: Option<StringMap<AgentConfig>>,
250 pub attachment: Option<AttachmentConfig>,
252 #[deprecated(note = "Use `share` instead")]
254 pub autoshare: Option<bool>,
255 pub autoupdate: Option<AutoUpdateConfig>,
257 pub command: Option<StringMap<CommandConfig>>,
259 pub compaction: Option<CompactionConfig>,
261 pub default_agent: Option<String>,
263 pub disabled_providers: Option<Vec<String>>,
265 pub enabled_providers: Option<Vec<String>>,
267 pub enterprise: Option<EnterpriseConfig>,
269 pub experimental: Option<ExperimentalConfig>,
271 pub formatter: Option<BoolOrMap<FormatterConfig>>,
273 pub instructions: Option<Vec<String>>,
275 #[deprecated(note = "Remove this field; layout is no longer configurable")]
277 pub layout: Option<LayoutConfig>,
278 #[serde(rename = "logLevel")]
280 pub log_level: Option<LogLevel>,
281 pub lsp: Option<BoolOrMap<LspConfig>>,
283 pub mcp: Option<StringMap<McpConfig>>,
285 #[deprecated(note = "Use `agent` instead")]
287 pub mode: Option<StringMap<AgentConfig>>,
288 pub model: Option<String>,
290 pub permission: Option<PermissionConfig>,
292 pub plugin: Option<Vec<PluginConfig>>,
294 pub provider: Option<StringMap<Value>>,
296 #[deprecated(note = "Use `references` instead")]
298 pub reference: Option<StringMap<ReferenceConfig>>,
299 pub references: Option<StringMap<ReferenceConfig>>,
301 #[serde(rename = "$schema")]
303 pub schema: Option<String>,
304 pub server: Option<ServerConfig>,
306 pub share: Option<ShareConfig>,
308 pub shell: Option<String>,
310 pub skills: Option<SkillsConfig>,
312 pub small_model: Option<String>,
314 pub snapshot: Option<bool>,
316 pub tool_output: Option<ToolOutputConfig>,
318 pub tools: Option<StringMap<bool>>,
320 pub username: Option<String>,
322 pub watcher: Option<WatcherConfig>,
324}
325#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(deny_unknown_fields)]
328pub struct CommandConfig {
329 pub template: String,
331 pub description: Option<String>,
333 pub agent: Option<String>,
335 pub model: Option<String>,
337 pub variant: Option<String>,
339 pub subtask: Option<bool>,
341}
342#[derive(Debug, Clone, Serialize, Deserialize, Default)]
344#[serde(deny_unknown_fields)]
345pub struct CompactionConfig {
346 pub auto: Option<bool>,
348 pub prune: Option<bool>,
350 pub tail_turns: Option<u64>,
352 pub preserve_recent_tokens: Option<u64>,
354 pub reserved: Option<u64>,
356}
357#[derive(Debug, Clone, Serialize, Deserialize, Default)]
359#[serde(deny_unknown_fields)]
360pub struct EnterpriseConfig {
361 pub url: Option<String>,
363}
364#[derive(Debug, Clone, Serialize, Deserialize, Default)]
366#[serde(deny_unknown_fields)]
367pub struct ExperimentalConfig {
368 pub disable_paste_summary: Option<bool>,
370 pub batch_tool: Option<bool>,
372 #[serde(rename = "openTelemetry")]
374 pub open_telemetry: Option<bool>,
375 pub primary_tools: Option<Vec<String>>,
377 pub continue_loop_on_deny: Option<bool>,
379 pub mcp_timeout: Option<u64>,
381 pub policies: Option<Vec<ExperimentalPolicy>>,
383}
384#[derive(Debug, Clone, Serialize, Deserialize)]
386#[serde(deny_unknown_fields)]
387pub struct ExperimentalPolicy {
388 pub action: PolicyAction,
390 pub effect: PolicyEffect,
392 pub resource: Provider,
394}
395#[derive(Debug, Clone, Serialize, Deserialize, Default)]
397#[serde(deny_unknown_fields)]
398pub struct FormatterConfig {
399 pub disabled: Option<bool>,
401 pub command: Option<Vec<String>>,
403 pub environment: Option<StringMap<String>>,
405 pub extensions: Option<Vec<String>>,
407}
408#[derive(Debug, Clone, Serialize, Deserialize, Default)]
410#[serde(deny_unknown_fields)]
411pub struct ImageAttachmentConfig {
412 pub auto_resize: Option<bool>,
414 pub max_width: Option<u64>,
416 pub max_height: Option<u64>,
418 pub max_base64_bytes: Option<u64>,
420}
421#[derive(Debug, Clone, Serialize, Deserialize)]
423#[serde(deny_unknown_fields)]
424pub struct LspServerConfig {
425 pub command: Vec<String>,
427 pub extensions: Option<Vec<String>>,
429 pub disabled: Option<bool>,
431 pub env: Option<StringMap<String>>,
433 pub initialization: Option<Value>,
435}
436#[derive(Debug, Clone, Serialize, Deserialize)]
438#[serde(deny_unknown_fields)]
439pub struct McpLocalConfig {
440 pub command: Vec<String>,
442 pub cwd: Option<String>,
444 pub environment: Option<StringMap<String>>,
446 pub enabled: Option<bool>,
448 pub timeout: Option<u64>,
450}
451#[derive(Debug, Clone, Serialize, Deserialize, Default)]
453#[serde(deny_unknown_fields)]
454pub struct McpOAuthConfig {
455 #[serde(rename = "clientId")]
457 pub client_id: Option<String>,
458 #[serde(rename = "clientSecret")]
460 pub client_secret: Option<String>,
461 pub scope: Option<String>,
463 #[serde(rename = "callbackPort")]
465 pub callback_port: Option<u16>,
466 #[serde(rename = "redirectUri")]
468 pub redirect_uri: Option<String>,
469}
470#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(deny_unknown_fields)]
473pub struct McpRemoteConfig {
474 pub url: String,
476 pub enabled: Option<bool>,
478 pub headers: Option<StringMap<String>>,
480 pub oauth: Option<McpOAuthOrFalse>,
482 pub timeout: Option<u64>,
484}
485#[derive(Debug, Clone, Serialize, Deserialize)]
487#[serde(deny_unknown_fields)]
488pub struct ReferenceGit {
489 pub repository: String,
491 pub branch: Option<String>,
493 pub description: Option<String>,
495 pub hidden: Option<bool>,
497}
498#[derive(Debug, Clone, Serialize, Deserialize)]
500#[serde(deny_unknown_fields)]
501pub struct ReferenceLocal {
502 pub path: String,
504 pub description: Option<String>,
506 pub hidden: Option<bool>,
508}
509#[derive(Debug, Clone, Serialize, Deserialize, Default)]
511#[serde(deny_unknown_fields)]
512pub struct ServerConfig {
513 pub port: Option<u64>,
515 pub hostname: Option<String>,
517 pub mdns: Option<bool>,
519 #[serde(rename = "mdnsDomain")]
521 pub mdns_domain: Option<String>,
522 pub cors: Option<Vec<String>>,
524}
525#[derive(Debug, Clone, Serialize, Deserialize, Default)]
527#[serde(deny_unknown_fields)]
528pub struct SkillsConfig {
529 pub paths: Option<Vec<String>>,
531 pub urls: Option<Vec<String>>,
533}
534#[derive(Debug, Clone, Serialize, Deserialize, Default)]
536#[serde(deny_unknown_fields)]
537pub struct ToolOutputConfig {
538 pub max_lines: Option<u64>,
540 pub max_bytes: Option<u64>,
542}
543#[derive(Debug, Clone, Serialize, Deserialize, Default)]
545#[serde(deny_unknown_fields)]
546pub struct WatcherConfig {
547 pub ignore: Option<Vec<String>>,
549}
550impl Config {
551 #[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 pub fn with_provider(self, provider: StringMap<Value>) -> Self {
562 Self {
563 provider: Some(provider),
564 ..self
565 }
566 }
567 pub fn with_model(self, model: String) -> Self {
569 Self { model: Some(model), ..self }
570 }
571 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 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 #[cfg(feature = "std")]
587 pub fn from_path(path: impl AsRef<Path>) -> ApiResult<Self> {
588 Self::read(path.as_ref())
589 }
590 #[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 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 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 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 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 fn read_yaml(_path: PathBuf) -> ApiResult<Self> {
649 Err(eyre!("YAML format is not supported for OpenCode configuration"))
650 }
651 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 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 fn write_yaml(&self, _path: impl Into<PathBuf>) -> ApiResult<()> {
668 Err(eyre!("YAML format is not supported for OpenCode configuration"))
669 }
670}