#!/usr/bin/env bash
set -e
# @meta dotenv
BIN_DIR=bin
TMP_DIR="cache/__tmp__"
VENV_DIR=".venv"
LANG_CMDS=( \
"sh:bash" \
"js:node" \
"py:python" \
)
# @cmd Run the tool
# @option -C --cwd
Change the current working directory
# @alias tool:run
# @arg tool![`_choice_tool`] The tool name
# @arg json The json data
run@tool() {
if [[ -z "$argc_json" ]]; then
declaration="$(generate-declarations@tool "$argc_tool" | jq -r '.[0]')"
if [[ -n "$declaration" ]]; then
_ask_json_data "$declaration"
fi
fi
if [[ -z "$argc_json" ]]; then
_die "error: no JSON data"
fi
lang="${argc_tool##*.}"
cmd="$(_lang_to_cmd "$lang")"
run_tool_script="scripts/run-tool.$lang"
[[ -n "$argc_cwd" ]] && cd "$argc_cwd"
exec "$cmd" "$run_tool_script" "$argc_tool" "$argc_json"
}
# @cmd Run the agent
# @alias agent:run
# @option -C --cwd Change the current working directory
# @arg agent![`_choice_agent`] The agent name
# @arg action![?`_choice_agent_action`] The agent action
# @arg json The json data
run@agent() {
if [[ -z "$argc_json" ]]; then
declaration="$(generate-declarations@agent "$argc_agent" | jq --arg name "$argc_action" '.[] | select(.name == $name)')"
if [[ -n "$declaration" ]]; then
_ask_json_data "$declaration"
fi
fi
if [[ -z "$argc_json" ]]; then
_die "error: no JSON data"
fi
tools_path="$(_get_agent_tools_path "$argc_agent")"
lang="${tools_path##*.}"
cmd="$(_lang_to_cmd "$lang")"
run_agent_script="scripts/run-agent.$lang"
[[ -n "$argc_cwd" ]] && cd "$argc_cwd"
exec "$cmd" "$run_agent_script" "$argc_agent" "$argc_action" "$argc_json"
}
# @cmd Build the project
build() {
if [[ -f tools.txt ]]; then
argc build@tool
else
echo 'Skipped building tools since tools.txt is missing'
fi
if [[ -f agents.txt ]]; then
argc build@agent
else
echo 'Skipped building agents since agents.txt is missing'
fi
if [[ -f mcp.json ]]; then
argc mcp merge-functions -S
fi
}
# @cmd Build tools
# @alias tool:build
# @option --names-file=tools.txt Path to a file containing tool filenames, one per line.
# This file specifies which tools will be used.
# @option --declarations-file=functions.json Path to a json file to save function declarations
# @arg tools*[`_choice_tool`] The tool filenames
build@tool() {
if [[ "${#argc_tools[@]}" -gt 0 ]]; then
mkdir -p "$TMP_DIR"
argc_names_file="$TMP_DIR/tools.txt"
printf "%s\n" "${argc_tools[@]}" > "$argc_names_file"
elif [[ "$argc_declarations_file" == "functions.json" ]]; then
argc clean@tool
fi
argc build-declarations@tool --names-file "${argc_names_file}" --declarations-file "${argc_declarations_file}"
argc build-bin@tool --names-file "${argc_names_file}"
}
# @cmd Build tools to bin
# @alias tool:build-bin
# @option --names-file=tools.txt Path to a file containing tool filenames, one per line.
# @arg tools*[`_choice_tool`] The tool filenames
build-bin@tool() {
mkdir -p "$BIN_DIR"
if [[ "${#argc_tools[@]}" -gt 0 ]]; then
names=("${argc_tools[@]}" )
elif [[ -f "$argc_names_file" ]]; then
names=($(cat "$argc_names_file" | grep -v '^#'))
if [[ "${#names[@]}" -gt 0 ]]; then
(cd "$BIN_DIR" && rm -rf "${names[@]}")
fi
fi
if [[ -z "$names" ]]; then
_die "error: no tools provided. '$argc_names_file' is missing. please create it and add some tools."
fi
not_found_tools=()
for name in "${names[@]}"; do
basename="${name%.*}"
lang="${name##*.}"
tool_path="tools/$name"
if [[ -f "$tool_path" ]]; then
if _is_win; then
bin_file="$BIN_DIR/$basename.cmd"
_build_win_shim tool $lang > "$bin_file"
else
bin_file="$BIN_DIR/$basename"
if [[ "$lang" == "py" && -d "$VENV_DIR" ]]; then
rm -rf "$bin_file"
_build_py_shim tool $lang > "$bin_file"
chmod +x "$bin_file"
else
ln -s -f "$PWD/scripts/run-tool.$lang" "$bin_file"
fi
fi
echo "Build bin/$basename"
else
not_found_tools+=("$name")
fi
done
if [[ -n "$not_found_tools" ]]; then
_die "error: not found tools: ${not_found_tools[*]}"
fi
}
# @cmd Build tools function declarations file
# @alias tool:build-declarations
# @option --names-file=tools.txt Path to a file containing tool filenames, one per line.
# @option --declarations-file=functions.json Path to a json file to save function declarations
# @arg tools*[`_choice_tool`] The tool filenames
build-declarations@tool() {
if [[ "${#argc_tools[@]}" -gt 0 ]]; then
names=("${argc_tools[@]}" )
elif [[ -f "$argc_names_file" ]]; then
names=($(cat "$argc_names_file" | grep -v '^#'))
fi
if [[ -z "$names" ]]; then
_die "error: no tools provided. '$argc_names_file' is missing. please create it and add some tools."
fi
json_list=()
not_found_tools=()
build_failed_tools=()
for name in "${names[@]}"; do
lang="${name##*.}"
tool_path="tools/$name"
if [[ ! -f "$tool_path" ]]; then
not_found_tools+=("$name")
continue;
fi
json_data="$(generate-declarations@tool "$name" | jq -r '.[0]')" || {
build_failed_tools+=("$name")
}
if [[ "$json_data" == "null" ]]; then
_die "error: failed to build declarations for tool $name"
fi
json_list+=("$json_data")
done
if [[ -n "$not_found_tools" ]]; then
_die "error: not found tools: ${not_found_tools[*]}"
fi
if [[ -n "$build_failed_tools" ]]; then
_die "error: invalid tools: ${build_failed_tools[*]}"
fi
json_data="$(echo "${json_list[@]}" | jq -s '.')"
if [[ "$argc_declarations_file" == "-" ]]; then
echo "$json_data"
else
echo "Build $argc_declarations_file"
echo "$json_data" > "$argc_declarations_file"
fi
}
# @cmd Generate function declaration for the tool
# @alias tool:generate-declarations
# @arg tool![`_choice_tool`] The tool name
generate-declarations@tool() {
lang="${1##*.}"
cmd="$(_lang_to_cmd "$lang")"
"$cmd" "scripts/build-declarations.$lang" "tools/$1"
}
# @cmd Build agents
# @alias agent:build
# @option --names-file=agents.txt Path to a file containing agent filenames, one per line.
# @arg agents*[`_choice_agent`] The agent filenames
build@agent() {
if [[ "${#argc_agents[@]}" -gt 0 ]]; then
mkdir -p "$TMP_DIR"
argc_names_file="$TMP_DIR/agents.txt"
printf "%s\n" "${argc_agents[@]}" > "$argc_names_file"
else
argc clean@agent
fi
argc build-declarations@agent --names-file "${argc_names_file}"
argc build-bin@agent --names-file "${argc_names_file}"
}
# @cmd Build agents to bin
# @alias agent:build-bin
# @option --names-file=agents.txt Path to a file containing agent dirs, one per line.
# @arg agents*[`_choice_agent`] The agent names
build-bin@agent() {
mkdir -p "$BIN_DIR"
if [[ "${#argc_agents[@]}" -gt 0 ]]; then
names=("${argc_agents[@]}" )
elif [[ -f "$argc_names_file" ]]; then
names=($(cat "$argc_names_file" | grep -v '^#'))
if [[ "${#names[@]}" -gt 0 ]]; then
(cd "$BIN_DIR" && rm -rf "${names[@]}")
fi
fi
if [[ -z "$names" ]]; then
_die "error: no agents provided. '$argc_names_file' is missing. please create it and add some agents."
fi
not_found_agents=()
for name in "${names[@]}"; do
agent_dir="agents/$name"
found=false
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
agent_tools_path="$agent_dir/tools.$lang"
if [[ -f "$agent_tools_path" ]]; then
found=true
if _is_win; then
bin_file="$BIN_DIR/$name.cmd"
_build_win_shim agent $lang > "$bin_file"
else
bin_file="$BIN_DIR/$name"
if [[ "$lang" == "py" && -d "$VENV_DIR" ]]; then
rm -rf "$bin_file"
_build_py_shim tool $lang > "$bin_file"
chmod +x "$bin_file"
else
ln -s -f "$PWD/scripts/run-agent.$lang" "$bin_file"
fi
fi
echo "Build bin/$name"
tool_names_file="$agent_dir/tools.txt"
if [[ -f "$tool_names_file" ]]; then
argc build-bin@tool --names-file "${tool_names_file}"
fi
break
fi
done
if [[ "$found" == "false" ]] && [[ ! -d "$agent_dir" ]]; then
not_found_agents+=("$name")
fi
done
if [[ -n "$not_found_agents" ]]; then
_die "error: not found agents: ${not_found_agents[*]}"
fi
}
# @cmd Build agents function declarations file
# @alias agent:build-declarations
# @option --names-file=agents.txt Path to a file containing agent dirs, one per line.
# @arg agents*[`_choice_agent`] The tool filenames
build-declarations@agent() {
if [[ "${#argc_agents[@]}" -gt 0 ]]; then
names=("${argc_agents[@]}" )
elif [[ -f "$argc_names_file" ]]; then
names=($(cat "$argc_names_file" | grep -v '^#'))
fi
if [[ -z "$names" ]]; then
_die "error: no agents provided. '$argc_names_file' is missing. please create it and add some agents."
fi
not_found_agents=()
build_failed_agents=()
exist_tools="$(ls -1 tools)"
for name in "${names[@]}"; do
agent_dir="agents/$name"
declarations_file="$agent_dir/functions.json"
tool_names_file="$agent_dir/tools.txt"
found=false
if [[ -d "$agent_dir" ]]; then
found=true
ok=true
json_data=""
agent_json_data=""
tools_json_data=""
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
agent_tools_path="$agent_dir/tools.$lang"
if [[ -f "$agent_tools_path" ]]; then
agent_json_data="$(generate-declarations@agent "$name")" || {
ok=false
build_failed_agents+=("$name")
}
break
fi
done
if [[ -f "$tool_names_file" ]]; then
if grep -q '^web_search\.' "$tool_names_file" && ! grep -q '^web_search\.' <<<"$exist_tools"; then
echo "WARNING: no found web_search tool, please run \`argc link-web-search \` to set one."
fi
if grep -q '^code_interpreter\.' "$tool_names_file" && ! grep -q '^code_interpreter\.' <<<"$exist_tools"; then
echo "WARNING: no found code_interpreter tool, please run \`argc link-code-interpreter \` to set one."
fi
tools_json_data="$(argc build-declarations@tool --names-file="$tool_names_file" --declarations-file=-)" || {
ok=false
build_failed_agents+=("$name")
}
fi
if [[ "$ok" == "true" ]]; then
if [[ -n "$agent_json_data" ]] && [[ -n "$tools_json_data" ]]; then
json_data="$(echo "[$agent_json_data,$tools_json_data]" | jq 'flatten')"
elif [[ -n "$agent_json_data" ]]; then
json_data="$agent_json_data"
elif [[ -n "$tools_json_data" ]]; then
json_data="$tools_json_data"
fi
if [[ -n "$json_data" ]]; then
echo "Build $declarations_file"
echo "$json_data" > "$declarations_file"
fi
fi
fi
if [[ "$found" == "false" ]]; then
not_found_agents+=("$name")
fi
done
if [[ -n "$not_found_agents" ]]; then
_die "error: not found agents: ${not_found_agents[*]}"
fi
if [[ -n "$build_failed_agents" ]]; then
_die "error: invalid agents: ${build_failed_agents[*]}"
fi
}
# @cmd Generate function declarations for the agent
# @alias agent:generate-declarations
# @flag --oneline Summary JSON in one line
# @arg agent![`_choice_agent`] The agent name
generate-declarations@agent() {
tools_path="$(_get_agent_tools_path "$1")"
if [[ -z "$tools_path" ]]; then
_die "error: no found entry file at agents/$1/tools."
fi
lang="${tools_path##*.}"
cmd="$(_lang_to_cmd "$lang")"
json="$("$cmd" "scripts/build-declarations.$lang" "$tools_path" | jq 'map(. + {agent: true})')"
if [[ -n "$argc_oneline" ]]; then
echo "$json" | jq -r '.[] | .name + ": " + (.description | split("\n"))[0]'
else
echo "$json"
fi
}
# @cmd Check environment variables, Node/Python dependencies, MCP-Bridge-Server status
check() {
argc check@tool
argc check@agent
argc mcp check
}
# @cmd Check dependencies and environment variables for a specific tool
# @alias tool:check
# @arg tools*[`_choice_tool`] The tool name
check@tool() {
if [[ "${#argc_tools[@]}" -gt 0 ]]; then
tool_names=("${argc_tools[@]}")
else
tool_names=($(cat tools.txt | grep -v '^#'))
fi
for name in "${tool_names[@]}"; do
tool_path="tools/$name"
echo "Check $tool_path"
if [[ -f "$tool_path" ]]; then
_check_bin "${name%.*}"
_check_envs "$tool_path"
./scripts/check-deps.sh "$tool_path"
else
echo "✗ not found tool file"
fi
done
}
# @cmd Check dependencies and environment variables for a specific agent
# @alias agent:check
# @arg agents*[`_choice_agent`] The agent name
check@agent() {
if [[ "${#argc_agents[@]}" -gt 0 ]]; then
agent_names=("${argc_agents[@]}")
else
agent_names=($(cat agents.txt | grep -v '^#'))
fi
for name in "${agent_names[@]}"; do
agent_dir="agents/$name"
echo "Check $agent_dir"
if [[ -d "$agent_dir" ]]; then
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
agent_tools_path="$agent_dir/tools.$lang"
if [[ -f "$agent_tools_path" ]]; then
_check_bin "$name"
_check_envs "$agent_tools_path"
./scripts/check-deps.sh "$agent_tools_path"
break
fi
done
else
echo "✗ not found agent dir"
fi
done
}
# @cmd List tools which can be put into functions.txt
# @alias tool:list
# Examples:
# argc list-tools > tools.txt
list@tool() {
_choice_tool
}
# @cmd List agents which can be put into agents.txt
# @alias agent:list
# Examples:
# argc list-agents > agents.txt
list@agent() {
_choice_agent
}
# @cmd Test the project
test() {
test@tool
test@agent
}
# @cmd Test tools
# @alias tool:test
test@tool() {
mkdir -p "$TMP_DIR"
names_file="$TMP_DIR/tools.txt"
declarations_file="$TMP_DIR/functions.json"
argc list@tool > "$names_file"
argc build@tool --names-file "$names_file" --declarations-file "$declarations_file"
test-demo@tool
}
# @cmd Test demo tools
# @alias tool:test-demo
test-demo@tool() {
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
tool="demo_$lang.$lang"
echo "---- Test $tool ---"
argc build-bin@tool "$tool"
argc run@tool $tool '{
"boolean": true,
"string": "Hello",
"string_enum": "foo",
"integer": 123,
"number": 3.14,
"array": [
"a",
"b",
"c"
],
"string_optional": "OptionalValue",
"array_optional": [
"x",
"y"
]
}'
echo
done
}
# @cmd Test agents
# @alias agent:test
test@agent() {
mkdir -p "$TMP_DIR"
names_file="$TMP_DIR/agents.txt"
argc list@agent > "$names_file"
argc build@agent --names-file "$names_file"
test-demo@agent
}
# @cmd Test demo agents
# @alias agent:test-demo
test-demo@agent() {
echo "---- Test demo agent ---"
args=(demo get_ipinfo '{}')
argc run@agent "${args[@]}"
for item in "${LANG_CMDS[@]}"; do
cmd="${item#*:}"
lang="${item%:*}"
echo "---- Test agents/demo/tools.$lang ---"
if [[ "$cmd" == "sh" ]]; then
"$(argc --argc-shell-path)" ./scripts/run-agent.sh "${args[@]}"
elif command -v "$cmd" &> /dev/null; then
$cmd ./scripts/run-agent.$lang "${args[@]}"
echo
fi
done
}
# @cmd Clean the project
clean() {
clean@tool
clean@agent
rm -rf "$BIN_DIR/"*
}
# @cmd Clean tools
# @alias tool:clean
clean@tool() {
_choice_tool | sed -E 's/\.([a-z]+)$//' | xargs -I{} rm -rf "$BIN_DIR/{}"
rm -rf functions.json
}
# @cmd Clean agents
# @alias agent:clean
clean@agent() {
_choice_agent | xargs -I{} rm -rf "$BIN_DIR/{}"
_choice_agent | xargs -I{} rm -rf agents/{}/functions.json
}
# @cmd Link a tool as web_search tool
#
# Example:
# argc link-web-search web_search_perplexity.sh
# @arg tool![`_choice_web_search`] The tool work as web_search
link-web-search() {
_link_tool $1 web_search
}
# @cmd Link a tool as code_interpreter tool
#
# Example:
# argc link-code-interpreter execute_py_code.py
# @arg tool![`_choice_code_interpreter`] The tool work as code_interpreter
link-code-interpreter() {
_link_tool $1 code_interpreter
}
# @cmd Link this repo to aichat functions_dir
link-to-aichat() {
functions_dir="$(aichat --info | grep -w functions_dir | awk '{$1=""; print substr($0,2)}')"
if [[ -z "$functions_dir" ]]; then
_die "error: your aichat version don't support function calling"
fi
if [[ ! -e "$functions_dir" ]]; then
if _is_win; then
current_dir="$(cygpath -w "$(pwd)")"
cmd <<< "mklink /D \"${functions_dir%/}\" \"${current_dir%/}\"" > /dev/null
else
ln -s "$(pwd)" "$functions_dir"
fi
echo "$functions_dir symlinked"
else
echo "$functions_dir already exists"
fi
}
# @cmd Run mcp command
# @arg args~[?`_choice_mcp_args`] The mcp command and arguments
mcp() {
bash ./scripts/mcp.sh "$@"
}
# @cmd Create a boilplate tool script
# @alias tool:create
# @arg args~
create@tool() {
./scripts/create-tool.sh "$@"
}
# @cmd Displays version information for required tools
version() {
uname -a
if command -v aichat &> /dev/null; then
aichat --version
fi
argc --argc-version
jq --version
ls --version 2>&1 | head -n 1
for item in "${LANG_CMDS[@]}"; do
cmd="${item#*:}"
if [[ "$cmd" == "bash" ]]; then
echo "$(argc --argc-shell-path) $("$(argc --argc-shell-path)" --version | head -n 1)"
elif command -v "$cmd" &> /dev/null; then
echo "$(_normalize_path "$(which $cmd)") $($cmd --version)"
fi
done
}
_lang_to_cmd() {
match_lang="$1"
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
if [[ "$lang" == "$match_lang" ]]; then
echo "${item#*:}"
fi
done
}
_get_agent_tools_path() {
name="$1"
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
entry_file="agents/$name/tools.$lang"
if [[ -f "agents/$name/tools.$lang" ]]; then
echo "$entry_file"
break
fi
done
}
_build_win_shim() {
kind="$1"
lang="$2"
cmd="$(_lang_to_cmd "$lang")"
if [[ "$lang" == "sh" ]]; then
run="\"$(argc --argc-shell-path)\" --noprofile --norc"
else
if [[ "$cmd" == "python" && -d "$VENV_DIR" ]]; then
run="call \"$(_normalize_path "$PWD/$VENV_DIR/Scripts/activate.bat")\" && python"
else
run="\"$(_normalize_path "$(which $cmd)")\""
fi
fi
cat <<-EOF
@echo off
setlocal
set "bin_dir=%~dp0"
for %%i in ("%bin_dir:~0,-1%") do set "script_dir=%%~dpi"
set "script_name=%~n0"
$run "%script_dir%scripts\run-$kind.$lang" "%script_name%" %*
EOF
}
_build_py_shim() {
kind="$1"
lang="$2"
cat <<-'EOF' | sed -e "s|__ROOT_DIR__|$PWD|g" -e "s|__VENV_DIR__|$VENV_DIR|g" -e "s/__KIND__/$kind/g"
#!/usr/bin/env bash
set -e
if [[ -f "__ROOT_DIR__/__VENV_DIR__/bin/activate" ]]; then
source "__ROOT_DIR__/__VENV_DIR__/bin/activate"
fi
python "__ROOT_DIR__/scripts/run-__KIND__.py" "$(basename "$0")" "$@"
EOF
}
_check_bin() {
bin_name="$1"
if _is_win; then
bin_name+=".cmd"
fi
if [[ ! -f "$BIN_DIR/$bin_name" ]]; then
echo "✗ missing bin/$bin_name"
fi
}
_check_envs() {
script_path="$1"
envs=( $(sed -E -n 's/.* @env ([A-Z0-9_]+)!.*/\1/p' $script_path) )
missing_envs=()
for env in $envs; do
if [[ -z "${!env}" ]]; then
missing_envs+=("$env")
fi
done
if [[ -n "$missing_envs" ]]; then
echo "✗ missing envs ${missing_envs[*]}"
fi
}
_link_tool() {
from="$1"
to="$2.${1##*.}"
rm -rf tools/$to
if _is_win; then
(cd tools && cp -f $from $to)
else
(cd tools && ln -s $from $to)
fi
(cd tools && ls -l $to)
}
_ask_json_data() {
declaration="$1"
echo 'Missing the JSON data but here are its properties:'
echo "$declaration" | ./scripts/declarations-util.sh pretty-print | sed -n '2,$s/^/>/p'
echo 'Generate placeholder data:'
data="$(echo "$declaration" | _declarations_json_data)"
echo "> $data"
read -e -r -p 'JSON data (Press ENTER to use placeholder): ' res
if [[ -z "$res" ]]; then
argc_json="$data"
else
argc_json="$res"
fi
}
_declarations_json_data() {
./scripts/declarations-util.sh generate-json | tail -n +2
}
_normalize_path() {
if _is_win; then
cygpath -w "$1"
else
echo "$1"
fi
}
_is_win() {
if [[ "$OS" == "Windows_NT" ]]; then
return 0
else
return 1
fi
}
_argc_before() {
if [[ -d ".venv/bin/activate" ]]; then
source .venv/bin/activate
fi
}
_choice_tool() {
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
cmd="${item#*:}"
if command -v "$cmd" &> /dev/null; then
ls -1 tools | grep "\.$lang$"
fi
done
}
_choice_web_search() {
_choice_tool | grep '^web_search_'
}
_choice_code_interpreter() {
_choice_tool | grep '^execute_.*_code'
}
_choice_agent() {
ls -1 agents
}
_choice_agent_action() {
if [[ "$ARGC_COMPGEN" -eq 1 ]]; then
expr="s/: /\t/"
else
expr="s/:.*//"
fi
argc generate-declarations@agent "$1" --oneline | sed "$expr"
}
_choice_mcp_args() {
if [[ "$ARGC_COMPGEN" -eq 1 ]]; then
args=( "${argc__positionals[@]}" )
args[-1]="$ARGC_LAST_ARG"
argc --argc-compgen generic scripts/mcp.sh mcp "${args[@]}"
else
:;
fi
}
_die() {
echo "$*" >&2
exit 1
}
if _is_win; then set -o igncr; fi
# See more details at https://github.com/sigoden/argc
eval "$(argc --argc-eval "$0" "$@")"