diff --git a/Cargo.lock b/Cargo.lock index a52948f..1166795 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3137,6 +3137,7 @@ dependencies = [ "crossterm 0.28.1", "dirs", "duct", + "dunce", "fancy-regex", "futures-util", "fuzzy-matcher", diff --git a/Cargo.toml b/Cargo.toml index 1683cb0..1662ec2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ anyhow = "1.0.69" bytes = "1.4.0" clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] } dirs = "6.0.0" +dunce = "1.0.5" futures-util = "0.3.29" inquire = "0.9.4" is-terminal = "0.4.9" diff --git a/assets/functions/scripts/run-agent.py b/assets/functions/scripts/run-agent.py index 6125c5a..e22368b 100755 --- a/assets/functions/scripts/run-agent.py +++ b/assets/functions/scripts/run-agent.py @@ -50,7 +50,13 @@ def parse_raw_data(data): def parse_argv(): agent_func = sys.argv[1] - agent_data = sys.argv[2] + + tool_data_file = os.environ.get("LLM_TOOL_DATA_FILE") + if tool_data_file and os.path.isfile(tool_data_file): + with open(tool_data_file, "r", encoding="utf-8") as f: + agent_data = f.read() + else: + agent_data = sys.argv[2] if (not agent_data) or (not agent_func): print("Usage: ./{agent_name}.py ", file=sys.stderr) diff --git a/assets/functions/scripts/run-agent.sh b/assets/functions/scripts/run-agent.sh index 5dd5179..acdfa94 100644 --- a/assets/functions/scripts/run-agent.sh +++ b/assets/functions/scripts/run-agent.sh @@ -14,7 +14,11 @@ main() { parse_argv() { agent_func="$1" - agent_data="$2" + if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then + agent_data="$(cat "$LLM_TOOL_DATA_FILE")" + else + agent_data="$2" + fi if [[ -z "$agent_data" ]] || [[ -z "$agent_func" ]]; then die "usage: ./{agent_name}.sh " fi @@ -57,7 +61,6 @@ run() { if [[ "$OS" == "Windows_NT" ]]; then set -o igncr tools_path="$(cygpath -w "$tools_path")" - tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')" fi jq_script="$(cat <<-'EOF' diff --git a/assets/functions/scripts/run-tool.py b/assets/functions/scripts/run-tool.py index d8c90b1..9ac3c07 100644 --- a/assets/functions/scripts/run-tool.py +++ b/assets/functions/scripts/run-tool.py @@ -49,6 +49,11 @@ def parse_raw_data(data): def parse_argv(): + tool_data_file = os.environ.get("LLM_TOOL_DATA_FILE") + if tool_data_file and os.path.isfile(tool_data_file): + with open(tool_data_file, "r", encoding="utf-8") as f: + return f.read() + argv = sys.argv[:] + [None] * max(0, 2 - len(sys.argv)) tool_data = argv[1] diff --git a/assets/functions/scripts/run-tool.sh b/assets/functions/scripts/run-tool.sh index ead83f6..75b72b9 100644 --- a/assets/functions/scripts/run-tool.sh +++ b/assets/functions/scripts/run-tool.sh @@ -13,7 +13,11 @@ main() { } parse_argv() { - tool_data="$1" + if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then + tool_data="$(cat "$LLM_TOOL_DATA_FILE")" + else + tool_data="$1" + fi if [[ -z "$tool_data" ]]; then die "usage: ./{function_name}.sh " fi @@ -54,7 +58,6 @@ run() { if [[ "$OS" == "Windows_NT" ]]; then set -o igncr tool_path="$(cygpath -w "$tool_path")" - tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')" fi jq_script="$(cat <<-'EOF' diff --git a/docs/ENVIRONMENT-VARIABLES.md b/docs/ENVIRONMENT-VARIABLES.md index 3bb52e0..a9a0a3f 100644 --- a/docs/ENVIRONMENT-VARIABLES.md +++ b/docs/ENVIRONMENT-VARIABLES.md @@ -107,6 +107,7 @@ The following variables can be used to change the log level of Loki or the locat can also pass the `--disable-log-colors` flag as well. ## Miscellaneous Variables -| Environment Variable | Description | Default Value | -|----------------------|--------------------------------------------------------------------------------------------------|---------------| -| `AUTO_CONFIRM` | Bypass all `guard_*` checks in the bash prompt helpers; useful for agent composition and routing | | \ No newline at end of file +| Environment Variable | Description | Default Value | +|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| +| `AUTO_CONFIRM` | Bypass all `guard_*` checks in the bash prompt helpers; useful for agent composition and routing | | +| `LLM_TOOL_DATA_FILE` | Set automatically by Loki on Windows. Points to a temporary file containing the JSON tool call data.
Tool scripts (`run-tool.sh`, `run-agent.sh`, etc.) read from this file instead of command-line args
to avoid JSON escaping issues when data passes through `cmd.exe` → bash. **Not intended to be set by users.** | | \ No newline at end of file diff --git a/src/function/mod.rs b/src/function/mod.rs index 4384982..b0698ac 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -613,6 +613,7 @@ impl Functions { ) })?; let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) }; + let to_script_path = |p: &str| -> String { p.replace('\\', "/") }; let content = match binary_type { BinaryType::Tool(None) => { let root_dir = Config::functions_dir(); @@ -622,8 +623,8 @@ impl Functions { ); content_template .replace("{function_name}", binary_name) - .replace("{root_dir}", &root_dir.to_string_lossy()) - .replace("{tool_path}", &tool_path) + .replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy())) + .replace("{tool_path}", &to_script_path(&tool_path)) } BinaryType::Tool(Some(agent_name)) => { let root_dir = Config::agent_data_dir(agent_name); @@ -633,16 +634,19 @@ impl Functions { ); content_template .replace("{function_name}", binary_name) - .replace("{root_dir}", &root_dir.to_string_lossy()) - .replace("{tool_path}", &tool_path) + .replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy())) + .replace("{tool_path}", &to_script_path(&tool_path)) } BinaryType::Agent => content_template .replace("{agent_name}", binary_name) - .replace("{config_dir}", &Config::config_dir().to_string_lossy()), + .replace( + "{config_dir}", + &to_script_path(&Config::config_dir().to_string_lossy()), + ), } .replace( "{prompt_utils_file}", - &Config::bash_prompt_utils_file().to_string_lossy(), + &to_script_path(&Config::bash_prompt_utils_file().to_string_lossy()), ); if binary_script_file.exists() { fs::remove_file(&binary_script_file)?; @@ -666,7 +670,7 @@ impl Functions { .join(".venv") .join("Scripts") .join("activate.bat"); - let canonicalized_path = fs::canonicalize(&executable_path)?; + let canonicalized_path = dunce::canonicalize(&executable_path)?; format!( "call \"{}\" && {}", canonicalized_path.to_string_lossy(), @@ -677,19 +681,16 @@ impl Functions { let executable_path = which::which("python") .or_else(|_| which::which("python3")) .map_err(|_| anyhow!("Python executable not found in PATH"))?; - let canonicalized_path = fs::canonicalize(&executable_path)?; + let canonicalized_path = dunce::canonicalize(&executable_path)?; canonicalized_path.to_string_lossy().into_owned() } _ => bail!("Unsupported language: {}", language.as_ref()), }; let bin_dir = binary_file .parent() - .expect("Failed to get parent directory of binary file") - .canonicalize()? - .to_string_lossy() - .into_owned(); - let wrapper_binary = binary_script_file - .canonicalize()? + .expect("Failed to get parent directory of binary file"); + let bin_dir = dunce::canonicalize(bin_dir)?.to_string_lossy().into_owned(); + let wrapper_binary = dunce::canonicalize(&binary_script_file)? .to_string_lossy() .into_owned(); let content = formatdoc!( @@ -1117,6 +1118,20 @@ pub fn run_llm_function( #[cfg(windows)] let cmd_name = polyfill_cmd_name(&cmd_name, &bin_dirs); + #[cfg(windows)] + let cmd_args = { + let mut args = cmd_args; + if let Some(json_data) = args.pop() { + let tool_data_file = temp_file("-tool-data-", ".json"); + fs::write(&tool_data_file, &json_data)?; + envs.insert( + "LLM_TOOL_DATA_FILE".into(), + tool_data_file.display().to_string(), + ); + } + args + }; + envs.insert("CLICOLOR_FORCE".into(), "1".into()); envs.insert("FORCE_COLOR".into(), "1".into());