feat: Automatic runtime customization using shebangs
This commit is contained in:
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
// Usage: ./{function_name}.ts <tool-data>
|
// Usage: ./{function_name}.ts <tool-data>
|
||||||
|
|
||||||
import { readFileSync, writeFileSync, existsSync, statSync } from "fs";
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||||
import { join, basename } from "path";
|
import { join } from "path";
|
||||||
import { pathToFileURL } from "url";
|
import { pathToFileURL } from "url";
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
|
|||||||
+81
-36
@@ -89,6 +89,26 @@ impl Language {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_shebang_runtime(path: &Path) -> Option<String> {
|
||||||
|
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(
|
pub async fn eval_tool_calls(
|
||||||
config: &GlobalConfig,
|
config: &GlobalConfig,
|
||||||
mut calls: Vec<ToolCall>,
|
mut calls: Vec<ToolCall>,
|
||||||
@@ -522,7 +542,14 @@ impl Functions {
|
|||||||
bail!("Unsupported tool file extension: {}", language.as_ref());
|
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(())
|
Ok(())
|
||||||
@@ -563,8 +590,9 @@ impl Functions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_agent_tool_binaries(name: &str) -> Result<()> {
|
fn build_agent_tool_binaries(name: &str) -> Result<()> {
|
||||||
|
let tools_file = Config::agent_functions_file(name)?;
|
||||||
let language = Language::from(
|
let language = Language::from(
|
||||||
&Config::agent_functions_file(name)?
|
&tools_file
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(OsStr::to_str)
|
.and_then(OsStr::to_str)
|
||||||
.map(|s| s.to_lowercase())
|
.map(|s| s.to_lowercase())
|
||||||
@@ -577,7 +605,8 @@ impl Functions {
|
|||||||
bail!("Unsupported tool file extension: {}", language.as_ref());
|
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)]
|
#[cfg(windows)]
|
||||||
@@ -585,6 +614,7 @@ impl Functions {
|
|||||||
binary_name: &str,
|
binary_name: &str,
|
||||||
language: Language,
|
language: Language,
|
||||||
binary_type: BinaryType,
|
binary_type: BinaryType,
|
||||||
|
custom_runtime: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use native::runtime;
|
use native::runtime;
|
||||||
let (binary_file, binary_script_file) = match binary_type {
|
let (binary_file, binary_script_file) = match binary_type {
|
||||||
@@ -669,38 +699,42 @@ impl Functions {
|
|||||||
binary_file.display()
|
binary_file.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
let run = match language {
|
let run = if let Some(rt) = custom_runtime {
|
||||||
Language::Bash => {
|
rt.to_string()
|
||||||
let shell = runtime::bash_path().ok_or_else(|| anyhow!("Shell not found"))?;
|
} else {
|
||||||
format!("{shell} --noprofile --norc")
|
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
|
let bin_dir = binary_file
|
||||||
.parent()
|
.parent()
|
||||||
@@ -730,6 +764,7 @@ impl Functions {
|
|||||||
binary_name: &str,
|
binary_name: &str,
|
||||||
language: Language,
|
language: Language,
|
||||||
binary_type: BinaryType,
|
binary_type: BinaryType,
|
||||||
|
custom_runtime: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use std::os::unix::prelude::PermissionsExt;
|
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_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||||
let content = match binary_type {
|
let mut content = match binary_type {
|
||||||
BinaryType::Tool(None) => {
|
BinaryType::Tool(None) => {
|
||||||
let root_dir = Config::functions_dir();
|
let root_dir = Config::functions_dir();
|
||||||
let tool_path = format!(
|
let tool_path = format!(
|
||||||
@@ -790,6 +825,12 @@ impl Functions {
|
|||||||
&Config::bash_prompt_utils_file().to_string_lossy(),
|
&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 {
|
if language == Language::TypeScript {
|
||||||
let bin_dir = binary_file
|
let bin_dir = binary_file
|
||||||
.parent()
|
.parent()
|
||||||
@@ -802,7 +843,11 @@ impl Functions {
|
|||||||
sf.write_all(content.as_bytes())?;
|
sf.write_all(content.as_bytes())?;
|
||||||
fs::set_permissions(&script_file, fs::Permissions::from_mode(0o755))?;
|
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() {
|
if binary_file.exists() {
|
||||||
fs::remove_file(&binary_file)?;
|
fs::remove_file(&binary_file)?;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-34
@@ -17,15 +17,9 @@ pub(crate) struct Param {
|
|||||||
pub(crate) trait ScriptedLanguage {
|
pub(crate) trait ScriptedLanguage {
|
||||||
fn ts_language(&self) -> tree_sitter::Language;
|
fn ts_language(&self) -> tree_sitter::Language;
|
||||||
|
|
||||||
fn default_runtime(&self) -> &str;
|
|
||||||
|
|
||||||
fn lang_name(&self) -> &str;
|
fn lang_name(&self) -> &str;
|
||||||
|
|
||||||
fn find_functions<'a>(
|
fn find_functions<'a>(&self, root: Node<'a>, src: &str) -> Vec<(Node<'a>, Node<'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>;
|
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<'_>> {
|
|||||||
node.named_children(&mut cursor).nth(index)
|
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<L: ScriptedLanguage>(
|
pub(crate) fn generate_declarations<L: ScriptedLanguage>(
|
||||||
lang: &L,
|
lang: &L,
|
||||||
src: &str,
|
src: &str,
|
||||||
@@ -226,8 +195,6 @@ pub(crate) fn generate_declarations<L: ScriptedLanguage>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _runtime = extract_runtime(&tree, src, lang.default_runtime());
|
|
||||||
|
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for (wrapper, func) in lang.find_functions(tree.root_node(), src) {
|
for (wrapper, func) in lang.find_functions(tree.root_node(), src) {
|
||||||
let func_name = lang.function_name(func, src)?;
|
let func_name = lang.function_name(func, src)?;
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ impl ScriptedLanguage for PythonLanguage {
|
|||||||
tree_sitter_python::LANGUAGE.into()
|
tree_sitter_python::LANGUAGE.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_runtime(&self) -> &str {
|
|
||||||
"python"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lang_name(&self) -> &str {
|
fn lang_name(&self) -> &str {
|
||||||
"python"
|
"python"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ impl ScriptedLanguage for TypeScriptLanguage {
|
|||||||
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
|
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_runtime(&self) -> &str {
|
|
||||||
"npx tsx"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lang_name(&self) -> &str {
|
fn lang_name(&self) -> &str {
|
||||||
"typescript"
|
"typescript"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user