Skip to main content

muse_codes/
cli.rs

1//! Builder for spawning headless `muse exec --json` runs.
2
3use crate::error::{Error, Result};
4use std::path::PathBuf;
5use std::process::Stdio;
6
7/// Provider mode for a run.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Provider {
10    /// The Meta provider (default; requires credentials — `muse login`,
11    /// `META_API_KEY`, or `~/.config/muse/auth.json`).
12    Meta,
13    /// Credential-free echo provider — exercises the full event stream
14    /// without model calls. What this crate's committed captures use.
15    Echo,
16}
17
18impl Provider {
19    fn as_str(self) -> &'static str {
20        match self {
21            Provider::Meta => "meta",
22            Provider::Echo => "echo",
23        }
24    }
25}
26
27/// Builder for one `muse exec --json` invocation.
28#[derive(Debug, Clone)]
29pub struct MuseExecBuilder {
30    binary: String,
31    prompt: String,
32    provider: Option<Provider>,
33    preset: Option<String>,
34    model: Option<String>,
35    reasoning_effort: Option<String>,
36    base_url: Option<String>,
37    working_directory: Option<PathBuf>,
38    envs: Vec<(String, String)>,
39}
40
41impl MuseExecBuilder {
42    pub fn new(prompt: impl Into<String>) -> Self {
43        Self {
44            binary: "muse".to_string(),
45            prompt: prompt.into(),
46            provider: None,
47            preset: None,
48            model: None,
49            reasoning_effort: None,
50            base_url: None,
51            working_directory: None,
52            envs: Vec::new(),
53        }
54    }
55
56    /// Use a specific binary instead of `muse` from `PATH`.
57    pub fn binary(mut self, path: impl Into<String>) -> Self {
58        self.binary = path.into();
59        self
60    }
61
62    pub fn provider(mut self, provider: Provider) -> Self {
63        self.provider = Some(provider);
64        self
65    }
66
67    /// Built-in preset (`native-basic`, `miniswe`).
68    pub fn preset(mut self, preset: impl Into<String>) -> Self {
69        self.preset = Some(preset.into());
70        self
71    }
72
73    pub fn model(mut self, model: impl Into<String>) -> Self {
74        self.model = Some(model.into());
75        self
76    }
77
78    /// Meta reasoning effort (`none|minimal|low|medium|high|xhigh|ultra`).
79    /// Not supported with [`Provider::Echo`].
80    pub fn reasoning_effort(mut self, effort: impl Into<String>) -> Self {
81        self.reasoning_effort = Some(effort.into());
82        self
83    }
84
85    pub fn base_url(mut self, url: impl Into<String>) -> Self {
86        self.base_url = Some(url.into());
87        self
88    }
89
90    pub fn working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
91        self.working_directory = Some(dir.into());
92        self
93    }
94
95    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
96        self.envs.push((key.into(), value.into()));
97        self
98    }
99
100    /// Resolve the binary and assemble the command with piped stdio.
101    pub fn build_command(&self) -> Result<tokio::process::Command> {
102        let program = which::which(&self.binary).map_err(|_| Error::BinaryNotFound {
103            name: self.binary.clone(),
104        })?;
105        let mut cmd = tokio::process::Command::new(program);
106        cmd.arg("exec").arg("--json");
107        if let Some(p) = self.provider {
108            cmd.args(["--provider", p.as_str()]);
109        }
110        if let Some(p) = &self.preset {
111            cmd.args(["--preset", p]);
112        }
113        if let Some(m) = &self.model {
114            cmd.args(["--model", m]);
115        }
116        if let Some(e) = &self.reasoning_effort {
117            cmd.args(["--reasoning-effort", e]);
118        }
119        if let Some(u) = &self.base_url {
120            cmd.args(["--base-url", u]);
121        }
122        cmd.arg(&self.prompt)
123            .stdin(Stdio::null())
124            .stdout(Stdio::piped())
125            .stderr(Stdio::piped())
126            .kill_on_drop(true);
127        if let Some(dir) = &self.working_directory {
128            cmd.current_dir(dir);
129        }
130        for (k, v) in &self.envs {
131            cmd.env(k, v);
132        }
133        Ok(cmd)
134    }
135
136    /// Spawn the run.
137    pub async fn spawn(&self) -> Result<tokio::process::Child> {
138        Ok(self.build_command()?.spawn()?)
139    }
140}