diff --git a/assets/functions/scripts/run-tool.ts b/assets/functions/scripts/run-tool.ts index 5ffd32b..228166a 100644 --- a/assets/functions/scripts/run-tool.ts +++ b/assets/functions/scripts/run-tool.ts @@ -2,8 +2,8 @@ // Usage: ./{function_name}.ts -import { readFileSync, writeFileSync, existsSync, statSync } from "fs"; -import { join, basename } from "path"; +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { join } from "path"; import { pathToFileURL } from "url"; async function main(): Promise { diff --git a/src/function/mod.rs b/src/function/mod.rs index ca777c9..96ffdb4 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -89,6 +89,26 @@ impl Language { } } +fn extract_shebang_runtime(path: &Path) -> Option { + let file = File::open(path).ok()?; + let reader = io::BufReader::new(file); + let first_line = io::BufRead::lines(reader).next()?.ok()?; + let shebang = first_line.strip_prefix("#!")?; + let cmd = shebang.trim(); + if cmd.is_empty() { + return None; + } + if let Some(after_env) = cmd.strip_prefix("/usr/bin/env ") { + let runtime = after_env.trim(); + if runtime.is_empty() { + return None; + } + Some(runtime.to_string()) + } else { + Some(cmd.to_string()) + } +} + pub async fn eval_tool_calls( config: &GlobalConfig, mut calls: Vec, @@ -522,7 +542,14 @@ impl Functions { bail!("Unsupported tool file extension: {}", language.as_ref()); } - Self::build_binaries(binary_name, language, BinaryType::Tool(agent_name))?; + let tool_path = Config::global_tools_dir().join(tool); + let custom_runtime = extract_shebang_runtime(&tool_path); + Self::build_binaries( + binary_name, + language, + BinaryType::Tool(agent_name), + custom_runtime.as_deref(), + )?; } Ok(()) @@ -563,8 +590,9 @@ impl Functions { } fn build_agent_tool_binaries(name: &str) -> Result<()> { + let tools_file = Config::agent_functions_file(name)?; let language = Language::from( - &Config::agent_functions_file(name)? + &tools_file .extension() .and_then(OsStr::to_str) .map(|s| s.to_lowercase()) @@ -577,7 +605,8 @@ impl Functions { bail!("Unsupported tool file extension: {}", language.as_ref()); } - Self::build_binaries(name, language, BinaryType::Agent) + let custom_runtime = extract_shebang_runtime(&tools_file); + Self::build_binaries(name, language, BinaryType::Agent, custom_runtime.as_deref()) } #[cfg(windows)] @@ -585,6 +614,7 @@ impl Functions { binary_name: &str, language: Language, binary_type: BinaryType, + custom_runtime: Option<&str>, ) -> Result<()> { use native::runtime; let (binary_file, binary_script_file) = match binary_type { @@ -669,38 +699,42 @@ impl Functions { binary_file.display() ); - let run = match language { - Language::Bash => { - let shell = runtime::bash_path().ok_or_else(|| anyhow!("Shell not found"))?; - format!("{shell} --noprofile --norc") + let run = if let Some(rt) = custom_runtime { + rt.to_string() + } else { + match language { + Language::Bash => { + let shell = runtime::bash_path().ok_or_else(|| anyhow!("Shell not found"))?; + format!("{shell} --noprofile --norc") + } + Language::Python if Path::new(".venv").exists() => { + let executable_path = env::current_dir()? + .join(".venv") + .join("Scripts") + .join("activate.bat"); + let canonicalized_path = dunce::canonicalize(&executable_path)?; + format!( + "call \"{}\" && {}", + canonicalized_path.to_string_lossy(), + language.to_cmd() + ) + } + Language::Python => { + let executable_path = which::which("python") + .or_else(|_| which::which("python3")) + .map_err(|_| anyhow!("Python executable not found in PATH"))?; + let canonicalized_path = dunce::canonicalize(&executable_path)?; + canonicalized_path.to_string_lossy().into_owned() + } + Language::TypeScript => { + let npx_path = which::which("npx").map_err(|_| { + anyhow!("npx executable not found in PATH (required for TypeScript tools)") + })?; + let canonicalized_path = dunce::canonicalize(&npx_path)?; + format!("{} tsx", canonicalized_path.to_string_lossy()) + } + _ => bail!("Unsupported language: {}", language.as_ref()), } - Language::Python if Path::new(".venv").exists() => { - let executable_path = env::current_dir()? - .join(".venv") - .join("Scripts") - .join("activate.bat"); - let canonicalized_path = dunce::canonicalize(&executable_path)?; - format!( - "call \"{}\" && {}", - canonicalized_path.to_string_lossy(), - language.to_cmd() - ) - } - Language::Python => { - let executable_path = which::which("python") - .or_else(|_| which::which("python3")) - .map_err(|_| anyhow!("Python executable not found in PATH"))?; - let canonicalized_path = dunce::canonicalize(&executable_path)?; - canonicalized_path.to_string_lossy().into_owned() - } - Language::TypeScript => { - let npx_path = which::which("npx").map_err(|_| { - anyhow!("npx executable not found in PATH (required for TypeScript tools)") - })?; - let canonicalized_path = dunce::canonicalize(&npx_path)?; - format!("{} tsx", canonicalized_path.to_string_lossy()) - } - _ => bail!("Unsupported language: {}", language.as_ref()), }; let bin_dir = binary_file .parent() @@ -730,6 +764,7 @@ impl Functions { binary_name: &str, language: Language, binary_type: BinaryType, + custom_runtime: Option<&str>, ) -> Result<()> { use std::os::unix::prelude::PermissionsExt; @@ -758,7 +793,7 @@ impl Functions { ) })?; let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) }; - let content = match binary_type { + let mut content = match binary_type { BinaryType::Tool(None) => { let root_dir = Config::functions_dir(); let tool_path = format!( @@ -790,6 +825,12 @@ impl Functions { &Config::bash_prompt_utils_file().to_string_lossy(), ); + if let Some(rt) = custom_runtime + && let Some(newline_pos) = content.find('\n') + { + content = format!("#!/usr/bin/env {rt}{}", &content[newline_pos..]); + } + if language == Language::TypeScript { let bin_dir = binary_file .parent() @@ -802,7 +843,11 @@ impl Functions { sf.write_all(content.as_bytes())?; fs::set_permissions(&script_file, fs::Permissions::from_mode(0o755))?; - let wrapper = format!("#!/bin/sh\nexec tsx \"{}\" \"$@\"\n", script_file.display()); + let ts_runtime = custom_runtime.unwrap_or("tsx"); + let wrapper = format!( + "#!/bin/sh\nexec {ts_runtime} \"{}\" \"$@\"\n", + script_file.display() + ); if binary_file.exists() { fs::remove_file(&binary_file)?; } diff --git a/src/parsers/common.rs b/src/parsers/common.rs index b0d80d6..58e4a9a 100644 --- a/src/parsers/common.rs +++ b/src/parsers/common.rs @@ -17,15 +17,9 @@ pub(crate) struct Param { pub(crate) trait ScriptedLanguage { fn ts_language(&self) -> tree_sitter::Language; - fn default_runtime(&self) -> &str; - fn lang_name(&self) -> &str; - fn find_functions<'a>( - &self, - root: Node<'a>, - src: &str, - ) -> Vec<(Node<'a>, Node<'a>)>; + fn find_functions<'a>(&self, root: Node<'a>, src: &str) -> Vec<(Node<'a>, Node<'a>)>; fn function_name<'a>(&self, func_node: Node<'a>, src: &'a str) -> Result<&'a str>; @@ -175,31 +169,6 @@ pub(crate) fn named_child(node: Node<'_>, index: usize) -> Option> { node.named_children(&mut cursor).nth(index) } -pub(crate) fn extract_runtime(tree: &tree_sitter::Tree, src: &str, default: &str) -> String { - let root = tree.root_node(); - let mut cursor = root.walk(); - for child in root.named_children(&mut cursor) { - let text = match child.kind() { - "hash_bang_line" | "comment" => match child.utf8_text(src.as_bytes()) { - Ok(t) => t, - Err(_) => continue, - }, - _ => break, - }; - - if let Some(cmd) = text.strip_prefix("#!") { - let cmd = cmd.trim(); - if let Some(after_env) = cmd.strip_prefix("/usr/bin/env ") { - return after_env.trim().to_string(); - } - return cmd.to_string(); - } - - break; - } - default.to_string() -} - pub(crate) fn generate_declarations( lang: &L, src: &str, @@ -226,8 +195,6 @@ pub(crate) fn generate_declarations( ); } - let _runtime = extract_runtime(&tree, src, lang.default_runtime()); - let mut out = Vec::new(); for (wrapper, func) in lang.find_functions(tree.root_node(), src) { let func_name = lang.function_name(func, src)?; diff --git a/src/parsers/python.rs b/src/parsers/python.rs index d8eb169..de9a089 100644 --- a/src/parsers/python.rs +++ b/src/parsers/python.rs @@ -15,10 +15,6 @@ impl ScriptedLanguage for PythonLanguage { tree_sitter_python::LANGUAGE.into() } - fn default_runtime(&self) -> &str { - "python" - } - fn lang_name(&self) -> &str { "python" } diff --git a/src/parsers/typescript.rs b/src/parsers/typescript.rs index abaf878..4fbea59 100644 --- a/src/parsers/typescript.rs +++ b/src/parsers/typescript.rs @@ -15,10 +15,6 @@ impl ScriptedLanguage for TypeScriptLanguage { tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into() } - fn default_runtime(&self) -> &str { - "npx tsx" - } - fn lang_name(&self) -> &str { "typescript" }