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    session_id: Option<String>,
36    reasoning_effort: Option<String>,
37    base_url: Option<String>,
38    working_directory: Option<PathBuf>,
39    envs: Vec<(String, String)>,
40}
41
42impl MuseExecBuilder {
43    pub fn new(prompt: impl Into<String>) -> Self {
44        Self {
45            binary: "muse".to_string(),
46            prompt: prompt.into(),
47            provider: None,
48            preset: None,
49            model: None,
50            session_id: None,
51            reasoning_effort: None,
52            base_url: None,
53            working_directory: None,
54            envs: Vec::new(),
55        }
56    }
57
58    /// Use a specific binary instead of `muse` from `PATH`.
59    pub fn binary(mut self, path: impl Into<String>) -> Self {
60        self.binary = path.into();
61        self
62    }
63
64    pub fn provider(mut self, provider: Provider) -> Self {
65        self.provider = Some(provider);
66        self
67    }
68
69    /// Built-in preset (`native-basic`, `miniswe`).
70    pub fn preset(mut self, preset: impl Into<String>) -> Self {
71        self.preset = Some(preset.into());
72        self
73    }
74
75    pub fn model(mut self, model: impl Into<String>) -> Self {
76        self.model = Some(model.into());
77        self
78    }
79
80    /// Run under a caller-supplied session id (`--session-id`), the basis of
81    /// multi-turn continuity: each turn is its own process, and passing the
82    /// same id makes the CLI continue that session rather than start a new
83    /// one. The id is adopted verbatim as the `stream.id` on every emitted
84    /// record.
85    ///
86    /// Supplying your own id is also what makes
87    /// [`MuseRecord`](crate::MuseRecord) identity safe to key on: record
88    /// `id`s are UUID-shaped counters that repeat across sessions, so the
89    /// only unique handle is the composite `(stream.id, id)` — and that is
90    /// trustworthy precisely because `stream.id` is yours. (When omitted,
91    /// the CLI mints a random v4 of its own.)
92    pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
93        self.session_id = Some(session_id.into());
94        self
95    }
96
97    /// Meta reasoning effort (`none|minimal|low|medium|high|xhigh|ultra`).
98    /// Not supported with [`Provider::Echo`].
99    pub fn reasoning_effort(mut self, effort: impl Into<String>) -> Self {
100        self.reasoning_effort = Some(effort.into());
101        self
102    }
103
104    pub fn base_url(mut self, url: impl Into<String>) -> Self {
105        self.base_url = Some(url.into());
106        self
107    }
108
109    pub fn working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
110        self.working_directory = Some(dir.into());
111        self
112    }
113
114    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
115        self.envs.push((key.into(), value.into()));
116        self
117    }
118
119    /// Resolve the binary and assemble the command with piped stdio.
120    pub fn build_command(&self) -> Result<tokio::process::Command> {
121        let program = which::which(&self.binary).map_err(|_| Error::BinaryNotFound {
122            name: self.binary.clone(),
123        })?;
124        let mut cmd = tokio::process::Command::new(program);
125        cmd.arg("exec").arg("--json");
126        if let Some(p) = self.provider {
127            cmd.args(["--provider", p.as_str()]);
128        }
129        if let Some(p) = &self.preset {
130            cmd.args(["--preset", p]);
131        }
132        if let Some(m) = &self.model {
133            cmd.args(["--model", m]);
134        }
135        if let Some(s) = &self.session_id {
136            cmd.args(["--session-id", s]);
137        }
138        if let Some(e) = &self.reasoning_effort {
139            cmd.args(["--reasoning-effort", e]);
140        }
141        if let Some(u) = &self.base_url {
142            cmd.args(["--base-url", u]);
143        }
144        cmd.arg(&self.prompt)
145            .stdin(Stdio::null())
146            .stdout(Stdio::piped())
147            .stderr(Stdio::piped())
148            .kill_on_drop(true);
149        if let Some(dir) = &self.working_directory {
150            cmd.current_dir(dir);
151        }
152        for (k, v) in &self.envs {
153            cmd.env(k, v);
154        }
155        Ok(cmd)
156    }
157
158    /// Spawn the run.
159    pub async fn spawn(&self) -> Result<tokio::process::Child> {
160        Ok(self.build_command()?.spawn()?)
161    }
162}