1use serde_json::Value;
8use std::path::PathBuf;
9use std::time::{Duration, Instant};
10
11const BUDGET: Duration = Duration::from_secs(5);
13
14pub fn scripts_dir() -> PathBuf {
15 crate::settings::Settings::dir().join("scripts")
16}
17
18pub fn list() -> Vec<PathBuf> {
21 let dir = scripts_dir();
22 if !dir.exists() {
23 let _ = std::fs::create_dir_all(&dir);
24 let _ = std::fs::write(dir.join("example.luau"), EXAMPLE);
25 }
26 let mut out: Vec<PathBuf> = std::fs::read_dir(&dir)
27 .into_iter()
28 .flatten()
29 .flatten()
30 .map(|e| e.path())
31 .filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("luau")))
32 .collect();
33 out.sort();
34 out
35}
36
37const EXAMPLE: &str = r#"-- Simple Editor script. `editor.tool(name, args)` calls the same tools the MCP server exposes;
38-- `editor.tools()` lists them; `editor.log(text)` shows a toast.
39local s = editor.tool("project.summary", {})
40editor.log("Project: " .. tostring(s.duration or "?") .. " s, " .. tostring(#(s.tracks or {})) .. " tracks")
41"#;
42
43pub fn run(
46 src: &str,
47 chunk_name: &str,
48 call: &mut dyn FnMut(&str, &Value) -> Result<Value, String>,
49 logs: &mut Vec<String>,
50) -> Result<(), String> {
51 let lua = mlua::Lua::new();
52 lua.sandbox(true).map_err(|e| e.to_string())?;
53 let start = Instant::now();
54 lua.set_interrupt(move |_| {
55 if start.elapsed() > BUDGET {
56 Err(mlua::Error::runtime("script took too long (5 s budget)"))
57 } else {
58 Ok(mlua::VmState::Continue)
59 }
60 });
61 let call = std::cell::RefCell::new(call);
62 let logs = std::cell::RefCell::new(logs);
63 lua.scope(|scope| {
64 let editor = lua.create_table()?;
65 editor.set(
66 "tool",
67 scope.create_function(|lua, (name, args): (String, Option<mlua::Table>)| {
68 let args = match args {
69 Some(t) => lua_to_json(mlua::Value::Table(t))?,
70 None => Value::Object(Default::default()),
71 };
72 let r = (call.borrow_mut())(&name, &args).map_err(mlua::Error::runtime)?;
73 json_to_lua(lua, &r)
74 })?,
75 )?;
76 editor.set(
77 "tools",
78 scope.create_function(|lua, ()| {
79 let t = lua.create_table()?;
80 for (i, (name, desc, _)) in crate::mcp::tools::TOOLS.iter().enumerate() {
81 let row = lua.create_table()?;
82 row.set("name", *name)?;
83 row.set("description", *desc)?;
84 t.set(i + 1, row)?;
85 }
86 Ok(t)
87 })?,
88 )?;
89 editor.set(
90 "log",
91 scope.create_function(|_, s: String| {
92 logs.borrow_mut().push(s);
93 Ok(())
94 })?,
95 )?;
96 lua.globals().set("editor", editor)?;
97 lua.load(src).set_name(chunk_name).exec()
98 })
99 .map_err(|e| e.to_string())
100}
101
102fn lua_to_json(v: mlua::Value) -> mlua::Result<Value> {
104 Ok(match v {
105 mlua::Value::Nil => Value::Null,
106 mlua::Value::Boolean(b) => Value::Bool(b),
107 mlua::Value::Integer(i) => Value::from(i),
108 mlua::Value::Number(n) => serde_json::Number::from_f64(n).map(Value::Number).unwrap_or(Value::Null),
109 mlua::Value::String(s) => Value::String(s.to_str()?.to_string()),
110 mlua::Value::Table(t) => {
111 let len = t.raw_len();
112 let arrayish = len > 0
113 && t.pairs::<mlua::Value, mlua::Value>().all(|p| {
114 p.map(|(k, _)| matches!(k, mlua::Value::Integer(i) if i >= 1 && i as usize <= len)).unwrap_or(false)
115 });
116 if arrayish {
117 let mut a = Vec::with_capacity(len);
118 for i in 1..=len {
119 a.push(lua_to_json(t.raw_get(i)?)?);
120 }
121 Value::Array(a)
122 } else {
123 let mut m = serde_json::Map::new();
124 for p in t.pairs::<mlua::Value, mlua::Value>() {
125 let (k, val) = p?;
126 let key = match k {
127 mlua::Value::String(s) => s.to_str()?.to_string(),
128 mlua::Value::Integer(i) => i.to_string(),
129 mlua::Value::Number(n) => n.to_string(),
130 _ => continue, };
132 m.insert(key, lua_to_json(val)?);
133 }
134 Value::Object(m)
135 }
136 }
137 _ => Value::Null, })
139}
140
141fn json_to_lua(lua: &mlua::Lua, v: &Value) -> mlua::Result<mlua::Value> {
143 Ok(match v {
144 Value::Null => mlua::Value::Nil,
145 Value::Bool(b) => mlua::Value::Boolean(*b),
146 Value::Number(n) => {
147 match n.as_i64().and_then(|i| i32::try_from(i).ok()) {
149 Some(i) => mlua::Value::Integer(i),
150 None => mlua::Value::Number(n.as_f64().unwrap_or(0.0)),
151 }
152 }
153 Value::String(s) => mlua::Value::String(lua.create_string(s)?),
154 Value::Array(a) => {
155 let t = lua.create_table_with_capacity(a.len(), 0)?;
156 for (i, v) in a.iter().enumerate() {
157 t.set(i + 1, json_to_lua(lua, v)?)?;
158 }
159 mlua::Value::Table(t)
160 }
161 Value::Object(m) => {
162 let t = lua.create_table_with_capacity(0, m.len())?;
163 for (k, v) in m {
164 t.set(k.as_str(), json_to_lua(lua, v)?)?;
165 }
166 mlua::Value::Table(t)
167 }
168 })
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use serde_json::json;
175
176 fn run_src(src: &str) -> (Result<(), String>, Vec<(String, Value)>, Vec<String>) {
177 let mut calls = Vec::new();
178 let mut logs = Vec::new();
179 let r = {
180 let mut call = |name: &str, args: &Value| {
181 calls.push((name.to_string(), args.clone()));
182 Ok(json!({"ok": true, "echo": args, "n": 3, "list": [1, 2, 3]}))
183 };
184 run(src, "test", &mut call, &mut logs)
185 };
186 (r, calls, logs)
187 }
188
189 #[test]
191 fn tool_call_round_trips() {
192 let (r, calls, logs) = run_src(
193 r#"
194 local r = editor.tool("clip.set", { id = 7, speed = 2.0, tags = {"a", "b"} })
195 assert(r.ok == true)
196 assert(r.n == 3)
197 assert(r.list[2] == 2)
198 assert(r.echo.id == 7)
199 assert(r.echo.tags[1] == "a")
200 editor.log("done " .. tostring(r.n))
201 "#,
202 );
203 assert_eq!(r, Ok(()));
204 assert_eq!(calls.len(), 1);
205 assert_eq!(calls[0].0, "clip.set");
206 assert_eq!(calls[0].1["speed"].as_f64(), Some(2.0));
208 assert_eq!(calls[0].1["tags"], json!(["a", "b"]));
209 assert_eq!(logs, vec!["done 3"]);
210 }
211
212 #[test]
214 fn errors_and_budget() {
215 let mut logs = Vec::new();
216 let mut fail = |_: &str, _: &Value| -> Result<Value, String> { Err("no such clip".into()) };
217 let e = run(r#"editor.tool("clip.set", {})"#, "t", &mut fail, &mut logs).unwrap_err();
218 assert!(e.contains("no such clip"), "{e}");
219 let (r, _, _) = run_src(r#"assert(io == nil and os.exit == nil)"#);
221 assert_eq!(r, Ok(()));
222 let (r, _, _) = run_src(r#"assert(#editor.tools() > 10)"#);
225 assert_eq!(r, Ok(()));
226 }
227}