Compare commits
4
Commits
c102da5665
...
00a21ee777
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00a21ee777 | ||
|
|
016f2654e4 | ||
|
|
9a1d00f5e6 | ||
|
|
a14c768f9a |
@@ -27,7 +27,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
|
|||||||
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
|
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
|
||||||
* [Installation](#install): Install Coyote
|
* [Installation](#install): Install Coyote
|
||||||
* [Getting Started](#getting-started): Get started with Coyote by doing first-run setup steps.
|
* [Getting Started](#getting-started): Get started with Coyote by doing first-run setup steps.
|
||||||
* [Sharing Configurations](https://github.com/Dark-Alex-17/coyote/wiki/Sharing-Configurations): Install bundles of agents, roles, skills, macros, tools, and MCP servers from any git repo, and share your own. Bundles are Coyote's equivalent of plugins in other CLI agents.
|
* [Bundles](https://github.com/Dark-Alex-17/coyote/wiki/Bundles): Install bundles of agents, roles, skills, macros, tools, and MCP servers from any git repo, and share your own. Bundles are Coyote's equivalent of plugins in other CLI agents.
|
||||||
* [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Coyote.
|
* [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Coyote.
|
||||||
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
|
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
|
||||||
* [Vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
|
* [Vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
|
||||||
@@ -43,8 +43,8 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
|
|||||||
* Models interact with each server through a compact set of capability-gated meta-tools: `mcp_search`/`mcp_describe` for discovery across tools, resources, and prompts, `mcp_invoke` for tool calls, `mcp_read` for paged and regex-filterable resource reads, and `mcp_prompt` for server-defined prompts. Binary content is spilled to disk instead of inlined, and oversized tool results are bounded before they reach the model.
|
* Models interact with each server through a compact set of capability-gated meta-tools: `mcp_search`/`mcp_describe` for discovery across tools, resources, and prompts, `mcp_invoke` for tool calls, `mcp_read` for paged and regex-filterable resource reads, and `mcp_prompt` for server-defined prompts. Binary content is spilled to disk instead of inlined, and oversized tool results are bounded before they reach the model.
|
||||||
* Invoke server prompts yourself with `.prompt <server> <name> [key=value ...]` in the REPL, with live staged tab-completion (servers, then prompt names, then `key=` arguments), and discover them with `.list prompts`.
|
* Invoke server prompts yourself with `.prompt <server> <name> [key=value ...]` in the REPL, with live staged tab-completion (servers, then prompt names, then `key=` arguments), and discover them with `.list prompts`.
|
||||||
* [Macros](https://github.com/Dark-Alex-17/coyote/wiki/Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros). Macros are Coyote's custom commands: invoke any macro directly by name (e.g. `.review main`), with tab-completion, right alongside the built-in REPL commands.
|
* [Macros](https://github.com/Dark-Alex-17/coyote/wiki/Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros). Macros are Coyote's custom commands: invoke any macro directly by name (e.g. `.review main`), with tab-completion, right alongside the built-in REPL commands.
|
||||||
* Give a macro a `description` (shown in `.list macros` and completions) and set `isolated: false` to run its steps on the live session, exactly as if you typed them. Note that non-isolated steps are recorded in the session, and mutating steps (`.role`, `.model`, ...) persist after the macro ends — by design. Steps are fail-fast: an error aborts the remaining steps, but completed steps' effects remain. A non-isolated macro step cannot invoke another macro, and a `.exit` step never exits the REPL.
|
* Give a macro a `description` (shown in `.list macros` and completions) and set `isolated: false` to run its steps on the live session, exactly as if you typed them. Note that non-isolated steps are recorded in the session, and mutating steps (`.role`, `.model`, ...) persist after the macro ends, by design. Steps are fail-fast: an error aborts the remaining steps, but completed steps' effects remain. A non-isolated macro step cannot invoke another macro, and a `.exit` step never exits the REPL.
|
||||||
* Commit project-specific macros to `.coyote/macros/` in your repo — they shadow same-named global macros (opt out with `--no-workspace-macros`).
|
* Commit project-specific macros to `.coyote/macros/` in your repo. They shadow same-named global macros (opt out with `--no-workspace-macros`).
|
||||||
* Pass variables positionally or by name: leading `name=value` args set declared variables directly (letting earlier variables keep their defaults), and remaining args fill the rest in order. Tab completion after a macro name lists each variable with its description and default.
|
* Pass variables positionally or by name: leading `name=value` args set declared variables directly (letting earlier variables keep their defaults), and remaining args fill the rest in order. Tab completion after a macro name lists each variable with its description and default.
|
||||||
* Scope which macros are invocable with `enabled_macros` in the global config, a role, an agent, or a session (most specific wins; an empty list disables all macros), and toggle at runtime with `.macro enable|disable <name>`.
|
* Scope which macros are invocable with `enabled_macros` in the global config, a role, an agent, or a session (most specific wins; an empty list disables all macros), and toggle at runtime with `.macro enable|disable <name>`.
|
||||||
* [RAG](https://github.com/Dark-Alex-17/coyote/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
* [RAG](https://github.com/Dark-Alex-17/coyote/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
||||||
|
|||||||
@@ -27,14 +27,30 @@ def _ensure_cwd_venv():
|
|||||||
_ensure_cwd_venv()
|
_ensure_cwd_venv()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_dir(env_name, default_path):
|
||||||
|
"""Resolve a directory at run time.
|
||||||
|
|
||||||
|
Prefer the override env var when set, otherwise fall back to the default
|
||||||
|
path derived from this script's own location, so the shim keeps working
|
||||||
|
when the config dir moves or is shared across environments with different
|
||||||
|
home directories.
|
||||||
|
"""
|
||||||
|
value = os.environ.get(env_name)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return os.path.normpath(default_path)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
(agent_func, raw_data) = parse_argv()
|
(agent_func, raw_data) = parse_argv()
|
||||||
agent_data = parse_raw_data(raw_data)
|
agent_data = parse_raw_data(raw_data)
|
||||||
|
|
||||||
root_dir = "{config_dir}"
|
self_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
setup_env(root_dir, agent_func, raw_data)
|
agent_dir = os.path.normpath(os.path.join(self_dir, ".."))
|
||||||
|
root_dir = resolve_dir("{root_dir_env}", os.path.join(self_dir, "{root_dir_rel}"))
|
||||||
|
setup_env(root_dir, agent_dir, agent_func, raw_data)
|
||||||
|
|
||||||
agent_tools_path = os.path.join(root_dir, "agents/{agent_name}/tools.py")
|
agent_tools_path = os.path.join(agent_dir, "tools.py")
|
||||||
run(agent_tools_path, agent_func, agent_data)
|
run(agent_tools_path, agent_func, agent_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -65,12 +81,12 @@ def parse_argv():
|
|||||||
return agent_func, agent_data
|
return agent_func, agent_data
|
||||||
|
|
||||||
|
|
||||||
def setup_env(root_dir, agent_func, raw_data):
|
def setup_env(root_dir, agent_dir, agent_func, raw_data):
|
||||||
load_env(os.path.join(root_dir, ".env"))
|
load_env(os.path.join(root_dir, ".env"))
|
||||||
os.environ["LLM_ROOT_DIR"] = root_dir
|
os.environ["LLM_ROOT_DIR"] = root_dir
|
||||||
os.environ["LLM_AGENT_NAME"] = "{agent_name}"
|
os.environ["LLM_AGENT_NAME"] = "{agent_name}"
|
||||||
os.environ["LLM_AGENT_FUNC"] = agent_func
|
os.environ["LLM_AGENT_FUNC"] = agent_func
|
||||||
os.environ["LLM_AGENT_ROOT_DIR"] = os.path.join(root_dir, "agents", "{agent_name}")
|
os.environ["LLM_AGENT_ROOT_DIR"] = agent_dir
|
||||||
os.environ["LLM_AGENT_CACHE_DIR"] = os.path.join(root_dir, "cache", "{agent_name}")
|
os.environ["LLM_AGENT_CACHE_DIR"] = os.path.join(root_dir, "cache", "{agent_name}")
|
||||||
os.environ["LLM_AGENT_RAW_JSON"] = raw_data
|
os.environ["LLM_AGENT_RAW_JSON"] = raw_data
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,30 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
root_dir="{config_dir}"
|
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
agent_dir="$(cd "$self_dir/.." && pwd)"
|
||||||
|
root_dir="$(resolve_dir "{root_dir_env}" "$self_dir/{root_dir_rel}")"
|
||||||
|
functions_dir="$(resolve_dir "{functions_dir_env}" "$self_dir/{functions_dir_rel}")"
|
||||||
parse_argv "$@"
|
parse_argv "$@"
|
||||||
setup_env
|
setup_env
|
||||||
tools_path="$root_dir/agents/{agent_name}/tools.sh"
|
tools_path="$agent_dir/tools.sh"
|
||||||
run
|
run
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Resolve a directory at run time: prefer the override env var ($1) when set,
|
||||||
|
# otherwise fall back to the default path ($2) derived from this script's own
|
||||||
|
# location, so the shim keeps working when the config dir moves or is shared
|
||||||
|
# across environments with different home directories.
|
||||||
|
resolve_dir() {
|
||||||
|
local override
|
||||||
|
override="$(printenv "$1" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$override" ]]; then
|
||||||
|
echo "$override"
|
||||||
|
else
|
||||||
|
(cd "$2" 2>/dev/null && pwd) || echo "$2"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
parse_argv() {
|
parse_argv() {
|
||||||
agent_func="$1"
|
agent_func="$1"
|
||||||
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
|
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
|
||||||
@@ -29,9 +46,9 @@ setup_env() {
|
|||||||
export LLM_ROOT_DIR="$root_dir"
|
export LLM_ROOT_DIR="$root_dir"
|
||||||
export LLM_AGENT_NAME="{agent_name}"
|
export LLM_AGENT_NAME="{agent_name}"
|
||||||
export LLM_AGENT_FUNC="$agent_func"
|
export LLM_AGENT_FUNC="$agent_func"
|
||||||
export LLM_AGENT_ROOT_DIR="$LLM_ROOT_DIR/agents/{agent_name}"
|
export LLM_AGENT_ROOT_DIR="$agent_dir"
|
||||||
export LLM_AGENT_CACHE_DIR="$LLM_ROOT_DIR/cache/{agent_name}"
|
export LLM_AGENT_CACHE_DIR="$LLM_ROOT_DIR/cache/{agent_name}"
|
||||||
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
|
export LLM_PROMPT_UTILS_FILE="$functions_dir/utils/prompt-utils.sh"
|
||||||
export LLM_AGENT_RAW_JSON="$agent_data"
|
export LLM_AGENT_RAW_JSON="$agent_data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +76,10 @@ run() {
|
|||||||
die "error: no JSON data"
|
die "error: no JSON data"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$tools_path" ]]; then
|
||||||
|
die "error: agent tools script not found: $tools_path"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$OS" == "Windows_NT" ]]; then
|
if [[ "$OS" == "Windows_NT" ]]; then
|
||||||
set -o igncr
|
set -o igncr
|
||||||
tools_path="$(cygpath -w "$tools_path")"
|
tools_path="$(cygpath -w "$tools_path")"
|
||||||
@@ -122,4 +143,3 @@ die() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,38 @@
|
|||||||
// Usage: ./{agent_name}.ts <agent-func> <agent-data>
|
// Usage: ./{agent_name}.ts <agent-func> <agent-data>
|
||||||
|
|
||||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||||
import { join } from "path";
|
import { dirname, join, resolve } from "path";
|
||||||
import { pathToFileURL } from "url";
|
import { fileURLToPath, pathToFileURL } from "url";
|
||||||
|
|
||||||
|
function selfDir(): string {
|
||||||
|
if (typeof __dirname !== "undefined") {
|
||||||
|
return __dirname;
|
||||||
|
}
|
||||||
|
return dirname(fileURLToPath(import.meta.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a directory at run time: prefer the override env var when set,
|
||||||
|
// otherwise fall back to the default path derived from this script's own
|
||||||
|
// location, so the shim keeps working when the config dir moves or is shared
|
||||||
|
// across environments with different home directories.
|
||||||
|
function resolveDir(envName: string, defaultPath: string): string {
|
||||||
|
const value = process.env[envName];
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return resolve(defaultPath);
|
||||||
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const { agentFunc, rawData } = parseArgv();
|
const { agentFunc, rawData } = parseArgv();
|
||||||
const agentData = parseRawData(rawData);
|
const agentData = parseRawData(rawData);
|
||||||
|
|
||||||
const configDir = "{config_dir}";
|
const binDir = selfDir();
|
||||||
setupEnv(configDir, agentFunc, rawData);
|
const agentDir = resolve(binDir, "..");
|
||||||
|
const configDir = resolveDir("{root_dir_env}", join(binDir, "{root_dir_rel}"));
|
||||||
|
setupEnv(configDir, agentDir, agentFunc, rawData);
|
||||||
|
|
||||||
const agentToolsPath = join(configDir, "agents", "{agent_name}", "tools.ts");
|
const agentToolsPath = join(agentDir, "tools.ts");
|
||||||
await run(agentToolsPath, agentFunc, agentData);
|
await run(agentToolsPath, agentFunc, agentData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +69,17 @@ function parseArgv(): { agentFunc: string; rawData: string } {
|
|||||||
return { agentFunc, rawData: agentData };
|
return { agentFunc, rawData: agentData };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupEnv(configDir: string, agentFunc: string, rawData: string): void {
|
function setupEnv(
|
||||||
|
configDir: string,
|
||||||
|
agentDir: string,
|
||||||
|
agentFunc: string,
|
||||||
|
rawData: string,
|
||||||
|
): void {
|
||||||
loadEnv(join(configDir, ".env"));
|
loadEnv(join(configDir, ".env"));
|
||||||
process.env["LLM_ROOT_DIR"] = configDir;
|
process.env["LLM_ROOT_DIR"] = configDir;
|
||||||
process.env["LLM_AGENT_NAME"] = "{agent_name}";
|
process.env["LLM_AGENT_NAME"] = "{agent_name}";
|
||||||
process.env["LLM_AGENT_FUNC"] = agentFunc;
|
process.env["LLM_AGENT_FUNC"] = agentFunc;
|
||||||
process.env["LLM_AGENT_ROOT_DIR"] = join(configDir, "agents", "{agent_name}");
|
process.env["LLM_AGENT_ROOT_DIR"] = agentDir;
|
||||||
process.env["LLM_AGENT_CACHE_DIR"] = join(configDir, "cache", "{agent_name}");
|
process.env["LLM_AGENT_CACHE_DIR"] = join(configDir, "cache", "{agent_name}");
|
||||||
process.env["LLM_AGENT_RAW_JSON"] = rawData;
|
process.env["LLM_AGENT_RAW_JSON"] = rawData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,30 @@ def _ensure_cwd_venv():
|
|||||||
_ensure_cwd_venv()
|
_ensure_cwd_venv()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_dir(env_name, default_path):
|
||||||
|
"""Resolve a directory at run time.
|
||||||
|
|
||||||
|
Prefer the override env var when set, otherwise fall back to the default
|
||||||
|
path derived from this script's own location, so the shim keeps working
|
||||||
|
when the config dir moves or is shared across environments with different
|
||||||
|
home directories.
|
||||||
|
"""
|
||||||
|
value = os.environ.get(env_name)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return os.path.normpath(default_path)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
raw_data = parse_argv()
|
raw_data = parse_argv()
|
||||||
tool_data = parse_raw_data(raw_data)
|
tool_data = parse_raw_data(raw_data)
|
||||||
|
|
||||||
root_dir = "{root_dir}"
|
self_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
root_dir = resolve_dir("{root_dir_env}", os.path.join(self_dir, "{root_dir_rel}"))
|
||||||
|
functions_dir = resolve_dir("{functions_dir_env}", os.path.join(self_dir, "{functions_dir_rel}"))
|
||||||
setup_env(root_dir, raw_data)
|
setup_env(root_dir, raw_data)
|
||||||
|
|
||||||
tool_path = "{tool_path}.py"
|
tool_path = os.path.join(functions_dir, "tools", "{function_name}.py")
|
||||||
run(tool_path, "run", tool_data)
|
run(tool_path, "run", tool_data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,29 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
root_dir="{root_dir}"
|
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
root_dir="$(resolve_dir "{root_dir_env}" "$self_dir/{root_dir_rel}")"
|
||||||
|
functions_dir="$(resolve_dir "{functions_dir_env}" "$self_dir/{functions_dir_rel}")"
|
||||||
parse_argv "$@"
|
parse_argv "$@"
|
||||||
setup_env
|
setup_env
|
||||||
tool_path="{tool_path}.sh"
|
tool_path="$functions_dir/tools/{function_name}.sh"
|
||||||
run
|
run
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Resolve a directory at run time: prefer the override env var ($1) when set,
|
||||||
|
# otherwise fall back to the default path ($2) derived from this script's own
|
||||||
|
# location, so the shim keeps working when the config dir moves or is shared
|
||||||
|
# across environments with different home directories.
|
||||||
|
resolve_dir() {
|
||||||
|
local override
|
||||||
|
override="$(printenv "$1" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$override" ]]; then
|
||||||
|
echo "$override"
|
||||||
|
else
|
||||||
|
(cd "$2" 2>/dev/null && pwd) || echo "$2"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
parse_argv() {
|
parse_argv() {
|
||||||
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
|
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
|
||||||
tool_data="$(cat "$LLM_TOOL_DATA_FILE")"
|
tool_data="$(cat "$LLM_TOOL_DATA_FILE")"
|
||||||
@@ -28,7 +44,7 @@ setup_env() {
|
|||||||
export LLM_ROOT_DIR="$root_dir"
|
export LLM_ROOT_DIR="$root_dir"
|
||||||
export LLM_TOOL_NAME="{function_name}"
|
export LLM_TOOL_NAME="{function_name}"
|
||||||
export LLM_TOOL_CACHE_DIR="$LLM_ROOT_DIR/cache/{function_name}"
|
export LLM_TOOL_CACHE_DIR="$LLM_ROOT_DIR/cache/{function_name}"
|
||||||
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
|
export LLM_PROMPT_UTILS_FILE="$functions_dir/utils/prompt-utils.sh"
|
||||||
export LLM_TOOL_RAW_JSON="$tool_data"
|
export LLM_TOOL_RAW_JSON="$tool_data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +72,10 @@ run() {
|
|||||||
die "error: no JSON data"
|
die "error: no JSON data"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$tool_path" ]]; then
|
||||||
|
die "error: tool script not found: $tool_path"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$OS" == "Windows_NT" ]]; then
|
if [[ "$OS" == "Windows_NT" ]]; then
|
||||||
set -o igncr
|
set -o igncr
|
||||||
tool_path="$(cygpath -w "$tool_path")"
|
tool_path="$(cygpath -w "$tool_path")"
|
||||||
|
|||||||
@@ -3,17 +3,41 @@
|
|||||||
// Usage: ./{function_name}.ts <tool-data>
|
// Usage: ./{function_name}.ts <tool-data>
|
||||||
|
|
||||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||||
import { join } from "path";
|
import { dirname, join, resolve } from "path";
|
||||||
import { pathToFileURL } from "url";
|
import { fileURLToPath, pathToFileURL } from "url";
|
||||||
|
|
||||||
|
function selfDir(): string {
|
||||||
|
if (typeof __dirname !== "undefined") {
|
||||||
|
return __dirname;
|
||||||
|
}
|
||||||
|
return dirname(fileURLToPath(import.meta.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a directory at run time: prefer the override env var when set,
|
||||||
|
// otherwise fall back to the default path derived from this script's own
|
||||||
|
// location, so the shim keeps working when the config dir moves or is shared
|
||||||
|
// across environments with different home directories.
|
||||||
|
function resolveDir(envName: string, defaultPath: string): string {
|
||||||
|
const value = process.env[envName];
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return resolve(defaultPath);
|
||||||
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const rawData = parseArgv();
|
const rawData = parseArgv();
|
||||||
const toolData = parseRawData(rawData);
|
const toolData = parseRawData(rawData);
|
||||||
|
|
||||||
const rootDir = "{root_dir}";
|
const binDir = selfDir();
|
||||||
|
const rootDir = resolveDir("{root_dir_env}", join(binDir, "{root_dir_rel}"));
|
||||||
|
const functionsDir = resolveDir(
|
||||||
|
"{functions_dir_env}",
|
||||||
|
join(binDir, "{functions_dir_rel}"),
|
||||||
|
);
|
||||||
setupEnv(rootDir, rawData);
|
setupEnv(rootDir, rawData);
|
||||||
|
|
||||||
const toolPath = "{tool_path}.ts";
|
const toolPath = join(functionsDir, "tools", "{function_name}.ts");
|
||||||
await run(toolPath, "run", toolData);
|
await run(toolPath, "run", toolData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+130
-77
@@ -1175,6 +1175,35 @@ impl Functions {
|
|||||||
Self::build_binaries(name, language, BinaryType::Agent, custom_runtime.as_deref())
|
Self::build_binaries(name, language, BinaryType::Agent, custom_runtime.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_shim_template(
|
||||||
|
content_template: &str,
|
||||||
|
binary_name: &str,
|
||||||
|
binary_type: &BinaryType,
|
||||||
|
) -> String {
|
||||||
|
let functions_dir_env = get_env_name("functions_dir");
|
||||||
|
match binary_type {
|
||||||
|
BinaryType::Tool(None) => content_template
|
||||||
|
.replace("{function_name}", binary_name)
|
||||||
|
.replace("{root_dir_env}", &functions_dir_env)
|
||||||
|
.replace("{root_dir_rel}", "..")
|
||||||
|
.replace("{functions_dir_rel}", ".."),
|
||||||
|
BinaryType::Tool(Some(agent_name)) => content_template
|
||||||
|
.replace("{function_name}", binary_name)
|
||||||
|
.replace(
|
||||||
|
"{root_dir_env}",
|
||||||
|
&format!("{}_DATA_DIR", normalize_env_name(agent_name)),
|
||||||
|
)
|
||||||
|
.replace("{root_dir_rel}", "..")
|
||||||
|
.replace("{functions_dir_rel}", "../../../functions"),
|
||||||
|
BinaryType::Agent => content_template
|
||||||
|
.replace("{agent_name}", binary_name)
|
||||||
|
.replace("{root_dir_env}", &get_env_name("config_dir"))
|
||||||
|
.replace("{root_dir_rel}", "../../..")
|
||||||
|
.replace("{functions_dir_rel}", "../../../functions"),
|
||||||
|
}
|
||||||
|
.replace("{functions_dir_env}", &functions_dir_env)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn build_binaries(
|
fn build_binaries(
|
||||||
binary_name: &str,
|
binary_name: &str,
|
||||||
@@ -1218,41 +1247,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 to_script_path = |p: &str| -> String { p.replace('\\', "/") };
|
let content = Self::render_shim_template(content_template, binary_name, &binary_type);
|
||||||
let content = match binary_type {
|
|
||||||
BinaryType::Tool(None) => {
|
|
||||||
let root_dir = paths::functions_dir();
|
|
||||||
let tool_path = format!(
|
|
||||||
"{}/{binary_name}",
|
|
||||||
paths::global_tools_dir().to_string_lossy()
|
|
||||||
);
|
|
||||||
content_template
|
|
||||||
.replace("{function_name}", binary_name)
|
|
||||||
.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 = paths::agent_data_dir(agent_name);
|
|
||||||
let tool_path = format!(
|
|
||||||
"{}/{binary_name}",
|
|
||||||
paths::global_tools_dir().to_string_lossy()
|
|
||||||
);
|
|
||||||
content_template
|
|
||||||
.replace("{function_name}", binary_name)
|
|
||||||
.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}",
|
|
||||||
&to_script_path(&paths::config_dir().to_string_lossy()),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
.replace(
|
|
||||||
"{prompt_utils_file}",
|
|
||||||
&to_script_path(&paths::bash_prompt_utils_file().to_string_lossy()),
|
|
||||||
);
|
|
||||||
write_file_atomic(&binary_script_file, &content, None)?;
|
write_file_atomic(&binary_script_file, &content, None)?;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
@@ -1298,21 +1293,17 @@ impl Functions {
|
|||||||
_ => bail!("Unsupported language: {}", language.as_ref()),
|
_ => bail!("Unsupported language: {}", language.as_ref()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let bin_dir = binary_file
|
// %~dp0 (the .cmd's own directory) keeps the launcher relocatable: no
|
||||||
.parent()
|
// absolute paths may be baked into it (see render_shim_template).
|
||||||
.expect("Failed to get parent directory of binary file");
|
let script_name = format!("run-{binary_name}.{}", language.to_extension());
|
||||||
let canonical_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!(
|
let content = formatdoc!(
|
||||||
r#"
|
r#"
|
||||||
@echo off
|
@echo off
|
||||||
setlocal
|
setlocal
|
||||||
|
|
||||||
set "bin_dir={canonical_bin_dir}"
|
set "bin_dir=%~dp0"
|
||||||
|
|
||||||
{run} "{wrapper_binary}" %*"#,
|
{run} "%~dp0{script_name}" %*"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
write_file_atomic(&binary_file, &content, None)?;
|
write_file_atomic(&binary_file, &content, None)?;
|
||||||
@@ -1352,37 +1343,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 mut content = match binary_type {
|
let mut content = Self::render_shim_template(content_template, binary_name, &binary_type);
|
||||||
BinaryType::Tool(None) => {
|
|
||||||
let root_dir = paths::functions_dir();
|
|
||||||
let tool_path = format!(
|
|
||||||
"{}/{binary_name}",
|
|
||||||
paths::global_tools_dir().to_string_lossy()
|
|
||||||
);
|
|
||||||
content_template
|
|
||||||
.replace("{function_name}", binary_name)
|
|
||||||
.replace("{root_dir}", &root_dir.to_string_lossy())
|
|
||||||
.replace("{tool_path}", &tool_path)
|
|
||||||
}
|
|
||||||
BinaryType::Tool(Some(agent_name)) => {
|
|
||||||
let root_dir = paths::agent_data_dir(agent_name);
|
|
||||||
let tool_path = format!(
|
|
||||||
"{}/{binary_name}",
|
|
||||||
paths::global_tools_dir().to_string_lossy()
|
|
||||||
);
|
|
||||||
content_template
|
|
||||||
.replace("{function_name}", binary_name)
|
|
||||||
.replace("{root_dir}", &root_dir.to_string_lossy())
|
|
||||||
.replace("{tool_path}", &tool_path)
|
|
||||||
}
|
|
||||||
BinaryType::Agent => content_template
|
|
||||||
.replace("{agent_name}", binary_name)
|
|
||||||
.replace("{config_dir}", &paths::config_dir().to_string_lossy()),
|
|
||||||
}
|
|
||||||
.replace(
|
|
||||||
"{prompt_utils_file}",
|
|
||||||
&paths::bash_prompt_utils_file().to_string_lossy(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(rt) = custom_runtime
|
if let Some(rt) = custom_runtime
|
||||||
&& let Some(newline_pos) = content.find('\n')
|
&& let Some(newline_pos) = content.find('\n')
|
||||||
@@ -1398,9 +1359,10 @@ impl Functions {
|
|||||||
write_file_atomic(&script_file, &content, Some(0o755))?;
|
write_file_atomic(&script_file, &content, Some(0o755))?;
|
||||||
|
|
||||||
let ts_runtime = custom_runtime.unwrap_or("tsx");
|
let ts_runtime = custom_runtime.unwrap_or("tsx");
|
||||||
|
// Locate the script next to the wrapper at run time instead of
|
||||||
|
// baking an absolute path (see render_shim_template).
|
||||||
let wrapper = format!(
|
let wrapper = format!(
|
||||||
"#!/bin/sh\nexec {ts_runtime} \"{}\" \"$@\"\n",
|
"#!/bin/sh\nexec {ts_runtime} \"$(dirname \"$0\")/run-{binary_name}.ts\" \"$@\"\n",
|
||||||
script_file.display()
|
|
||||||
);
|
);
|
||||||
write_file_atomic(&binary_file, &wrapper, Some(0o755))?;
|
write_file_atomic(&binary_file, &wrapper, Some(0o755))?;
|
||||||
} else {
|
} else {
|
||||||
@@ -4613,6 +4575,97 @@ mod tests {
|
|||||||
fs::remove_dir_all(&dir).unwrap();
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn built_shims_resolve_paths_at_runtime_after_relocation() {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let root = temp_file("-shim-relocate-", "");
|
||||||
|
let home_a = root.join("home-a");
|
||||||
|
let config_a = home_a.join("coyote");
|
||||||
|
let tools_dir = config_a.join("functions").join("tools");
|
||||||
|
fs::create_dir_all(&tools_dir).unwrap();
|
||||||
|
fs::create_dir_all(config_a.join("functions").join("bin")).unwrap();
|
||||||
|
let tool_src = tools_dir.join("relocheck.sh");
|
||||||
|
fs::write(
|
||||||
|
&tool_src,
|
||||||
|
"#!/usr/bin/env bash\necho \"relocated-ok\" >> \"$LLM_OUTPUT\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::set_permissions(&tool_src, fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
|
||||||
|
let config_env = get_env_name("config_dir");
|
||||||
|
let functions_env = get_env_name("functions_dir");
|
||||||
|
let saved_config = env::var(&config_env).ok();
|
||||||
|
let saved_functions = env::var(&functions_env).ok();
|
||||||
|
unsafe {
|
||||||
|
env::set_var(&config_env, &config_a);
|
||||||
|
env::remove_var(&functions_env);
|
||||||
|
}
|
||||||
|
|
||||||
|
let build_result = Functions::build_global_function_binary("relocheck.sh", None);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
match saved_config {
|
||||||
|
Some(v) => env::set_var(&config_env, v),
|
||||||
|
None => env::remove_var(&config_env),
|
||||||
|
}
|
||||||
|
match saved_functions {
|
||||||
|
Some(v) => env::set_var(&functions_env, v),
|
||||||
|
None => env::remove_var(&functions_env),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
build_result.unwrap();
|
||||||
|
|
||||||
|
let shim = config_a.join("functions").join("bin").join("relocheck");
|
||||||
|
let content = fs::read_to_string(&shim).unwrap();
|
||||||
|
assert!(
|
||||||
|
!content.contains(config_a.to_str().unwrap()),
|
||||||
|
"shim must not bake the generation-time config path:\n{content}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
content.contains(&functions_env),
|
||||||
|
"shim must consult the functions dir env override at run time:\n{content}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate the config dir landing under a different home directory
|
||||||
|
// (shared/synced config dir, or a sandbox with another $HOME).
|
||||||
|
let home_b = root.join("home-b");
|
||||||
|
fs::rename(&home_a, &home_b).unwrap();
|
||||||
|
let moved_shim = home_b
|
||||||
|
.join("coyote")
|
||||||
|
.join("functions")
|
||||||
|
.join("bin")
|
||||||
|
.join("relocheck");
|
||||||
|
|
||||||
|
if which::which("bash").is_err() || which::which("jq").is_err() {
|
||||||
|
fs::remove_dir_all(&root).unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = process::Command::new(&moved_shim)
|
||||||
|
.arg("{}")
|
||||||
|
.env_remove(&config_env)
|
||||||
|
.env_remove(&functions_env)
|
||||||
|
.env_remove("LLM_OUTPUT")
|
||||||
|
.env_remove("LLM_TOOL_DATA_FILE")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"relocated shim failed: stderr={} stdout={stdout}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
stdout.contains("relocated-ok"),
|
||||||
|
"unexpected output: {stdout}"
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_dir_all(&root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn eval_tool_calls_partitions_mcp_and_sequential_then_resorts() {
|
fn eval_tool_calls_partitions_mcp_and_sequential_then_resorts() {
|
||||||
let mut ctx = RequestContext::new(Arc::new(AppState::test_default()), WorkingMode::Cmd);
|
let mut ctx = RequestContext::new(Arc::new(AppState::test_default()), WorkingMode::Cmd);
|
||||||
|
|||||||
Reference in New Issue
Block a user