Skip to main content

soma_codemode/state/
provider.rs

1use serde_json::{json, Value};
2
3use crate::ToolError;
4
5use super::path::state_root;
6use super::path::VirtualPath;
7use super::workspace::{FileEdit, StateWorkspace};
8
9#[derive(Debug, Clone)]
10pub struct StateProvider {
11    workspace: StateWorkspace,
12}
13
14impl Default for StateProvider {
15    fn default() -> Self {
16        Self {
17            workspace: StateWorkspace::new(state_root()),
18        }
19    }
20}
21
22impl StateProvider {
23    pub fn new(workspace: StateWorkspace) -> Self {
24        Self { workspace }
25    }
26
27    pub async fn dispatch(&self, method: &str, params: Value) -> Result<Value, ToolError> {
28        match method {
29            "write_file" => {
30                let path = VirtualPath::parse(string_param(&params, "path")?)?;
31                let content = string_param(&params, "content")?;
32                self.workspace.write_file(&path, content).await?;
33                Ok(json!({"ok": true}))
34            }
35            "append_file" => {
36                let path = VirtualPath::parse(string_param(&params, "path")?)?;
37                Ok(json!(
38                    self.workspace
39                        .append_file(&path, string_param(&params, "content")?)
40                        .await?
41                ))
42            }
43            "read_file" => {
44                let path = VirtualPath::parse(string_param(&params, "path")?)?;
45                Ok(json!(self.workspace.read_file(&path).await?))
46            }
47            "read_json" => {
48                let path = VirtualPath::parse(string_param(&params, "path")?)?;
49                Ok(json!(self.workspace.read_json(&path).await?))
50            }
51            "write_json" => {
52                let path = VirtualPath::parse(string_param(&params, "path")?)?;
53                self.workspace
54                    .write_json(
55                        &path,
56                        value_param(&params, "value")?,
57                        bool_param(&params, "pretty", false),
58                    )
59                    .await?;
60                Ok(json!({"ok": true}))
61            }
62            "hash_file" => {
63                let path = VirtualPath::parse(string_param(&params, "path")?)?;
64                Ok(json!(
65                    self.workspace
66                        .hash_file(&path, string_param_default(&params, "algorithm", "sha256"))
67                        .await?
68                ))
69            }
70            "detect_file" => {
71                let path = VirtualPath::parse(string_param(&params, "path")?)?;
72                Ok(json!(self.workspace.detect_file(&path).await?))
73            }
74            "exists" => {
75                let path =
76                    VirtualPath::parse_read_scope(string_param_default(&params, "path", ""))?;
77                Ok(json!(self.workspace.exists(&path).await?))
78            }
79            "stat" => {
80                let path =
81                    VirtualPath::parse_read_scope(string_param_default(&params, "path", ""))?;
82                Ok(json!(self.workspace.stat(&path).await?))
83            }
84            "mkdir" => {
85                let path = VirtualPath::parse(string_param(&params, "path")?)?;
86                Ok(json!(self.workspace.mkdir(&path).await?))
87            }
88            "remove" => {
89                let path = VirtualPath::parse(string_param(&params, "path")?)?;
90                Ok(json!(
91                    self.workspace
92                        .remove(&path, bool_param(&params, "recursive", false))
93                        .await?
94                ))
95            }
96            "copy" => {
97                let from = VirtualPath::parse(string_param(&params, "from")?)?;
98                let to = VirtualPath::parse(string_param(&params, "to")?)?;
99                Ok(json!(self.workspace.copy(&from, &to).await?))
100            }
101            "move" => {
102                let from = VirtualPath::parse(string_param(&params, "from")?)?;
103                let to = VirtualPath::parse(string_param(&params, "to")?)?;
104                Ok(json!(self.workspace.move_path(&from, &to).await?))
105            }
106            "walk_tree" => {
107                let path =
108                    VirtualPath::parse_read_scope(string_param_default(&params, "path", ""))?;
109                Ok(json!(
110                    self.workspace
111                        .walk_tree(&path, usize_param(&params, "limit", 200))
112                        .await?
113                ))
114            }
115            "list" => {
116                let path =
117                    VirtualPath::parse_read_scope(string_param_default(&params, "path", ""))?;
118                Ok(json!(self.workspace.list(&path).await?))
119            }
120            "glob" => Ok(json!(
121                self.workspace
122                    .glob(
123                        string_param(&params, "pattern")?,
124                        usize_param(&params, "limit", 200)
125                    )
126                    .await?
127            )),
128            "search_files" => Ok(json!(
129                self.workspace
130                    .search_files(
131                        string_param(&params, "pattern")?,
132                        string_param(&params, "query")?,
133                        usize_param(&params, "limit", 200)
134                    )
135                    .await?
136            )),
137            "replace_in_files" => Ok(json!(
138                self.workspace
139                    .replace_in_files(
140                        string_param(&params, "pattern")?,
141                        string_param(&params, "search")?,
142                        string_param(&params, "replace")?,
143                        bool_param(&params, "dry_run", true)
144                    )
145                    .await?
146            )),
147            "plan_edits" => Ok(json!(
148                self.workspace
149                    .plan_edits(edits_param(&params, "edits")?)
150                    .await?
151            )),
152            "apply_edit_plan" => Ok(json!(
153                self.workspace
154                    .apply_edit_plan(string_param(&params, "plan_id")?)
155                    .await?
156            )),
157            "status" => Ok(json!({"root": self.workspace.root().display().to_string()})),
158            _ => Err(ToolError::UnknownAction {
159                message: format!("unknown state method `{method}`"),
160                valid: state_methods().iter().map(ToString::to_string).collect(),
161                hint: None,
162            }),
163        }
164    }
165}
166
167fn state_methods() -> &'static [&'static str] {
168    &[
169        "append_file",
170        "apply_edit_plan",
171        "copy",
172        "detect_file",
173        "exists",
174        "glob",
175        "hash_file",
176        "list",
177        "mkdir",
178        "move",
179        "plan_edits",
180        "read_file",
181        "read_json",
182        "remove",
183        "replace_in_files",
184        "search_files",
185        "stat",
186        "status",
187        "walk_tree",
188        "write_file",
189        "write_json",
190    ]
191}
192
193fn string_param<'a>(params: &'a Value, key: &str) -> Result<&'a str, ToolError> {
194    params
195        .get(key)
196        .and_then(Value::as_str)
197        .ok_or_else(|| ToolError::MissingParam {
198            message: format!("missing `{key}`"),
199            param: key.to_string(),
200        })
201}
202
203fn string_param_default<'a>(params: &'a Value, key: &str, default: &'a str) -> &'a str {
204    params.get(key).and_then(Value::as_str).unwrap_or(default)
205}
206
207fn bool_param(params: &Value, key: &str, default: bool) -> bool {
208    params.get(key).and_then(Value::as_bool).unwrap_or(default)
209}
210
211fn usize_param(params: &Value, key: &str, default: usize) -> usize {
212    params
213        .get(key)
214        .and_then(Value::as_u64)
215        .and_then(|value| usize::try_from(value).ok())
216        .unwrap_or(default)
217}
218
219fn value_param<'a>(params: &'a Value, key: &str) -> Result<&'a Value, ToolError> {
220    params.get(key).ok_or_else(|| ToolError::MissingParam {
221        message: format!("missing `{key}`"),
222        param: key.to_string(),
223    })
224}
225
226fn edits_param(params: &Value, key: &str) -> Result<Vec<FileEdit>, ToolError> {
227    let value = value_param(params, key)?;
228    serde_json::from_value(value.clone()).map_err(|err| ToolError::InvalidParam {
229        message: format!("invalid state edit list: {err}"),
230        param: key.to_string(),
231    })
232}