feat: support bots (#39)

This commit is contained in:
sigoden
2024-06-08 20:46:27 +08:00
committed by GitHub
parent 82d7a7de8a
commit 213e949fc8
12 changed files with 757 additions and 9 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
/tmp
functions.txt
bots.txt
tools.txt
bots.txt
functions.json
/bin
/cache
+231 -6
View File
@@ -21,7 +21,7 @@ run-tool() {
ext=".cmd"
fi
if [[ -z "$argc_json" ]]; then
declaration="$(jq --arg name "$argc_cmd" '.[] | select(.name == $name)' functions.json)"
declaration="$(cat functions.json | jq --arg name "$argc_cmd" '.[] | select(.name == $name)')"
if [[ -n "$declaration" ]]; then
_ask_json_data "$declaration"
fi
@@ -32,9 +32,33 @@ run-tool() {
"$BIN_DIR/$argc_cmd$ext" "$argc_json"
}
# @cmd Run the tool
# @arg cmd![`_choice_bot`] The bot command
# @arg action![`_choice_bot_action`] The bot action
# @arg json The json data
run-bot() {
if _is_win; then
ext=".cmd"
fi
if [[ -z "$argc_json" ]]; then
functions_path="bots/$argc_cmd/functions.json"
if [[ -f "$functions_path" ]]; then
declaration="$(jq --arg name "$argc_action" '.[] | select(.name == $name)' "$functions_path")"
if [[ -n "$declaration" ]]; then
_ask_json_data "$declaration"
fi
fi
fi
if [[ -z "$argc_json" ]]; then
_die "error: no JSON data"
fi
"$BIN_DIR/$argc_cmd$ext" "$argc_action" "$argc_json"
}
# @cmd Build the project
build() {
argc build-tools
argc build-bots
}
# @cmd Build tools
@@ -81,7 +105,7 @@ build-tools-bin() {
if [[ -f "$tool_path" ]]; then
if _is_win; then
bin_file="$BIN_DIR/$basename.cmd"
_build_win_shim_tool $lang > "$bin_file"
_build_win_shim tool $lang > "$bin_file"
else
bin_file="$BIN_DIR/$basename"
ln -s -f "$PWD/scripts/run-tool.$lang" "$bin_file"
@@ -143,16 +167,147 @@ build-tool-declaration() {
"$cmd" "scripts/build-declarations.$lang" "tools/$1" | jq '.[0]'
}
# @cmd List tools that can be put into tools.txt
# @cmd Build bots
# @option --names-file=bots.txt Path to a file containing bot filenames, one per line.
# Example:
# hackernews
# spotify
# @arg bots*[`_choice_bot`] The bot filenames
build-bots() {
if [[ "${#argc_bots[@]}" -gt 0 ]]; then
mkdir -p "$TMP_DIR"
argc_names_file="$TMP_DIR/bots.txt"
printf "%s\n" "${argc_bots[@]}" > "$argc_names_file"
else
argc clean-bots
fi
argc build-bots-json --names-file "${argc_names_file}"
argc build-bots-bin --names-file "${argc_names_file}"
}
# @cmd Build tools to bin
# @option --names-file=bots.txt Path to a file containing bot filenames, one per line.
# @arg bots*[`_choice_bot`] The bot names
build-bots-bin() {
mkdir -p "$BIN_DIR"
if [[ "${#argc_bots[@]}" -gt 0 ]]; then
names=("${argc_bots[@]}" )
elif [[ -f "$argc_names_file" ]]; then
names=($(cat "$argc_names_file"))
if [[ "${#names[@]}" -gt 0 ]]; then
(cd "$BIN_DIR" && rm -rf "${names[@]}")
fi
fi
if [[ -z "$names" ]]; then
_die "error: not input bots, not found '$argc_names_file', please create it add some tools."
fi
not_found_bots=()
for name in "${names[@]}"; do
bot_dir="bots/$name"
found=false
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
bot_tools_file="$bot_dir/tools.$lang"
if [[ -f "$bot_tools_file" ]]; then
found=true
if _is_win; then
bin_file="$BIN_DIR/$name.cmd"
_build_win_shim bot $lang > "$bin_file"
else
bin_file="$BIN_DIR/$name"
ln -s -f "$PWD/scripts/run-bot.$lang" "$bin_file"
fi
echo "Build bot $name"
fi
done
if [[ "$found" = "false" ]]; then
not_found_bots+=("$name")
fi
done
if [[ -n "$not_found_bots" ]]; then
_die "error: not found bots: ${not_found_bots[*]}"
fi
}
# @cmd Build bots functions.json
# @option --names-file=bots.txt Path to a file containing bot filenames, one per line.
# @arg tools*[`_choice_tool`] The tool filenames
build-bots-json() {
if [[ "${#argc_bots[@]}" -gt 0 ]]; then
names=("${argc_bots[@]}" )
elif [[ -f "$argc_names_file" ]]; then
names=($(cat "$argc_names_file"))
fi
if [[ -z "$names" ]]; then
_die "error: not input bots, not found '$argc_names_file', please create it add some tools."
fi
not_found_bots=()
build_failed_bots=()
for name in "${names[@]}"; do
bot_dir="bots/$name"
build_ok=false
found=false
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
bot_tools_file="$bot_dir/tools.$lang"
if [[ -f "$bot_tools_file" ]]; then
found=true
json_data="$(build-bot-declarations "$name")" || {
build_failed_bots+=("$name")
}
declarations_file="$bot_dir/functions.json"
echo "Build $declarations_file"
echo "$json_data" > "$declarations_file"
fi
done
if [[ "$found" == "false" ]]; then
not_found_bots+=("$name")
fi
done
if [[ -n "$not_found_bots" ]]; then
_die "error: not found bots: ${not_found_bots[*]}"
fi
if [[ -n "$build_failed_bots" ]]; then
_die "error: invalid bots: ${build_failed_bots[*]}"
fi
}
# @cmd Build function declarations for an bot
# @flag --oneline Summary JSON in one line
# @arg bot![`_choice_bot`] The bot name
build-bot-declarations() {
tools_path="$(_get_bot_tools_path "$1")"
if [[ -z "$tools_path" ]]; then
_die "error: no found entry file at bots/$1/tools.<lang>"
fi
lang="${tools_path##*.}"
cmd="$(_lang_to_cmd "$lang")"
json="$("$cmd" "scripts/build-declarations.$lang" "$tools_path")"
if [[ -n "$argc_oneline" ]]; then
echo "$json" | jq -r '.[] | .name + ": " + (.description | split("\n"))[0]'
else
echo "$json"
fi
}
# @cmd List tools that can be put into functions.txt
# Examples:
# argc list-tools > tools.txt
list-tools() {
_choice_tool
}
# @cmd List bots that can be put into bots.txt
# Examples:
# argc list-bots > bots.txt
list-bots() {
_choice_bot
}
# @cmd Test the project
test() {
test-tools
test-bots
}
# @cmd Test tools
@@ -218,10 +373,55 @@ test-tools-demo() {
done
}
# @cmd Test bots
test-bots() {
tmp_dir="cache/tmp"
mkdir -p "$tmp_dir"
names_file="$tmp_dir/bots.txt"
argc list-bots > "$names_file"
argc build-bots --names-file "$names_file"
test-bots-todo-lang
}
# @cmd Test todo-* bots
test-bots-todo-lang() {
if _is_win; then
ext=".cmd"
fi
test_cases=( \
'add_todo#{"desc":"Add a todo item"}' \
'add_todo#{"desc":"Add another todo item"}' \
'del_todo#{"id":1}' \
'list_todos#{}' \
'clear_todos#{}' \
)
for item in "${LANG_CMDS[@]}"; do
cmd="${item#*:}"
if command -v "$cmd" &> /dev/null; then
lang="${item%:*}"
bot_name="todo-$lang"
rm -rf "cache/$bot_name/todos.json"
for test_case in "${test_cases[@]}"; do
IFS='#' read -r action data <<<"${test_case}"
cmd_path="$BIN_DIR/$bot_name$ext"
echo "Test $cmd_path: "
"$cmd_path" "$action" "$data"
done
fi
done
}
# @cmd Clean tools
clean-tools() {
_choice_tool | sed 's/\.\([a-z]\+\)$//' | xargs -I{} rm -rf "$BIN_DIR/{}"
rm -rf functions.json
}
# @cmd Clean bots
clean-bots() {
_choice_bot | xargs -I{} rm -rf "$BIN_DIR/{}"
_choice_bot | xargs -I{} rm -rf bots/{}/functions.json
}
# @cmd Install this repo to aichat functions_dir
@@ -269,8 +469,20 @@ _lang_to_cmd() {
done
}
_build_win_shim_tool() {
lang="$1"
_get_bot_tools_path() {
name="$1"
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
entry_file="bots/$name/tools.$lang"
if [[ -f "bots/$name/tools.$lang" ]]; then
echo "$entry_file"
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"
@@ -285,7 +497,7 @@ 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-tool.$lang" "%script_name%.$lang" %*
$run "%script_dir%scripts\run-$kind.$lang" "%script_name%" %*
EOF
}
@@ -338,6 +550,19 @@ _choice_tool() {
done
}
_choice_bot() {
ls -1 bots
}
_choice_bot_action() {
if [[ "$ARGC_COMPGEN" -eq 1 ]]; then
expr="s/: /\t/"
else
expr="s/:.*//"
fi
argc build-bot-declarations "$1" --oneline | sed "$expr"
}
_choice_cmd() {
ls -1 "$BIN_DIR" | sed -e 's/\.cmd$//'
}
+1
View File
@@ -0,0 +1 @@
../todo-sh/index.yaml
+73
View File
@@ -0,0 +1,73 @@
const fs = require('fs');
const path = require('path');
/**
* Add a new todo item
* @typedef {Object} AddTodoArgs
* @property {string} desc - The task description
* @param {AddTodoArgs} args
*/
exports.add_todo = function addTodo(args) {
const todosFile = _getTodosFile();
if (fs.existsSync(todosFile)) {
const num = JSON.parse(fs.readFileSync(todosFile)).reduce((max, item) => Math.max(max, item.id), 0) + 1;
const data = fs.readFileSync(todosFile);
fs.writeFileSync(todosFile, JSON.stringify([...JSON.parse(data), { id: num, desc: args.desc }]));
console.log(`Successfully added todo id=${num}`);
} else {
fs.writeFileSync(todosFile, JSON.stringify([{ id: 1, desc: args.desc }]));
console.log('Successfully added todo id=1');
}
}
/**
* Delete an existing todo item
* @typedef {Object} DelTodoArgs
* @property {number} id - The task id
* @param {DelTodoArgs} args
*/
exports.del_todo = function delTodo(args) {
const todosFile = _getTodosFile();
if (fs.existsSync(todosFile)) {
const data = fs.readFileSync(todosFile);
const newData = JSON.parse(data).filter(item => item.id !== args.id);
fs.writeFileSync(todosFile, JSON.stringify(newData));
console.log(`Successfully deleted todo id=${args.id}`);
} else {
_die('Empty todo list');
}
}
/**
* Display the current todo list in json format.
*/
exports.list_todos = function listTodos() {
const todosFile = _getTodosFile();
if (fs.existsSync(todosFile)) {
console.log(fs.readFileSync(todosFile, "utf8"));
} else {
_die('Empty todo list');
}
}
/**
* Delete the entire todo list.
*/
exports.clear_todos = function clearTodos() {
const todosFile = _getTodosFile();
fs.unlinkSync(todosFile)
console.log("Successfully deleted entry todo list");
}
function _getTodosFile() {
const cacheDir = process.env.LLM_BOT_CACHE_DIR || '/tmp';
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
return path.join(cacheDir, 'todos.json');
}
function _die(message) {
console.error(message);
process.exit(1);
}
+1
View File
@@ -0,0 +1 @@
../todo-sh/index.yaml
+65
View File
@@ -0,0 +1,65 @@
import json
import sys
import os
from json import JSONDecodeError
def add_todo(desc: str):
"""Add a new todo item
Args:
desc: The task description
"""
todos_file = _get_todos_file()
try:
with open(todos_file, "r") as f:
data = json.load(f)
except (FileNotFoundError, JSONDecodeError):
data = []
num = max([item["id"] for item in data] + [0]) + 1
data.append({"id": num, "desc": desc})
with open(todos_file, "w") as f:
json.dump(data, f)
print(f"Successfully added todo id={num}")
def del_todo(id: int):
"""Delete an existing todo item
Args:
id: The task id
"""
todos_file = _get_todos_file()
try:
with open(todos_file, "r") as f:
data = json.load(f)
except (FileNotFoundError, JSONDecodeError):
_die("Empty todo list")
data = [item for item in data if item["id"] != id]
with open(todos_file, "w") as f:
json.dump(data, f)
print(f"Successfully deleted todo id={id}")
def list_todos():
"""Display the current todo list in json format."""
todos_file = _get_todos_file()
try:
with open(todos_file, "r") as f:
print(f.read())
except FileNotFoundError:
_die("Empty todo list")
def clear_todos():
"""Delete the entire todo list."""
os.remove(_get_todos_file())
def _get_todos_file() -> str:
cache_dir=os.environ.get("LLM_BOT_CACHE_DIR", "/tmp")
if not os.path.exists(cache_dir):
os.makedirs(cache_dir, exist_ok=True)
return os.path.join(cache_dir, "todos.json")
def _die(msg: str):
print(msg, file=sys.stderr)
exit(1)
+19
View File
@@ -0,0 +1,19 @@
name: Todo List
description: Your name is TodoBot and you are a helpful chatbot that manages a todo list.
instructions: |
You will be provided with a list of todos.
Users can interact with you using the following commands:
* add_todo: Add a todo to the list.
* rm_todo: Remove a todo from the list.
* list_todos: Display the current todo list.
* clear_todos: Delete the entire todo list.
Based on the interaction, ensure that you provide appropriate confirmations or errors for the requested operation. For example:
- Confirmations: "Todo item added successfully!", "Todo item removed successfully!", "All todo items deleted!"
- Errors: "Cannot add todo item, missing description.", "Todo item with id {id} not found.", "No todo items to delete."
Make sure you understand the user request properly before performing any action. If unsure, ask clarifying questions like "Do you want to remove all todos or just a specific one?"
conversation_starters:
- "Add a new todo item 'Finish report'."
- "Remove the todo item with id=2."
- "Delete all my todos."
- "What todos do I have pending?"
- "How can I remove a specific todo item?"
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -e
# @cmd Add a new todo item
# @option --desc! The task description
add_todo() {
todos_file="$(_get_todos_file)"
if [[ -f "$todos_file" ]]; then
num="$(cat "$todos_file" | jq '[.[].id] | max + 1')"
data="$(cat "$todos_file")"
else
num=1
data="[]"
fi
echo "$data" | \
jq --arg new_id $num \
--arg new_desc "$argc_desc" \
'. += [{"id": $new_id | tonumber, "desc": $new_desc}]' \
> "$todos_file"
echo "Successfully added todo id=$num"
}
# @cmd Delete an existing todo item
# @option --id! <INT> The task id
del_todo() {
todos_file="$(_get_todos_file)"
if [[ -f "$todos_file" ]]; then
data="$(cat "$todos_file")"
echo "$data" | \
jq --arg id $argc_id '[.[] | select(.id != ($id | tonumber))]' \
> "$todos_file"
echo "Successfully deleted todo id=$argc_id"
else
_die "Empty todo list"
fi
}
# @cmd Display the current todo list in json format.
list_todos() {
todos_file="$(_get_todos_file)"
if [[ -f "$todos_file" ]]; then
cat "$todos_file"
else
_die "Empty todo list"
fi
}
# @cmd Delete the entire todo list.
clear_todos() {
todos_file="$(_get_todos_file)"
if [[ -f "$todos_file" ]]; then
rm -rf "$todos_file"
fi
echo "Successfully deleted entry todo list"
}
_argc_before() {
todos_file="$(_get_todos_file)"
mkdir -p "$(dirname "$todos_file")"
}
_get_todos_file() {
echo "${LLM_BOT_CACHE_DIR:-/tmp}/todos.json"
}
_die() {
echo "$*" >&2
exit 1
}
# See more details at https://github.com/sigoden/argc
eval "$(argc --argc-eval "$0" "$@")"
+2 -2
View File
@@ -15,8 +15,8 @@ pretty-print() {
# @cmd Generate placeholder json according to declarations
# Examples:
# ./scripts/declarations.sh generate-json-data functions.json
# cat functions.json | ./scripts/declarations.sh generate-json-data functions.json
# ./scripts/declarations.sh generate-json functions.json
# cat functions.json | ./scripts/declarations.sh generate-json functions.json
# @arg json-file The json file, Read stdin if omitted
generate-json() {
_run _generate_json
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env node
const path = require("path");
const fs = require("fs");
const os = require("os");
async function main() {
const [botName, botFunc, rawData] = parseArgv("run-bot.js");
const botData = parseRawData(rawData);
const rootDir = path.resolve(__dirname, "..");
setupEnv(rootDir, botName);
const botToolsPath = path.resolve(rootDir, `bots/${botName}/tools.js`);
await run(botToolsPath, botFunc, botData);
}
function parseArgv(thisFileName) {
let botName = process.argv[1];
let botFunc = "";
let botData = null;
if (botName.endsWith(thisFileName)) {
botName = process.argv[2];
botFunc = process.argv[3];
botData = process.argv[4];
} else {
botName = path.basename(botName);
botFunc = process.argv[2];
botData = process.argv[3];
}
if (botName.endsWith(".js")) {
botName = botName.slice(0, -3);
}
return [botName, botFunc, botData];
}
function parseRawData(data) {
if (!data) {
throw new Error("No JSON data");
}
try {
return JSON.parse(data);
} catch {
throw new Error("Invalid JSON data");
}
}
function setupEnv(rootDir, botName) {
process.env["LLM_ROOT_DIR"] = rootDir;
loadEnv(path.resolve(rootDir, ".env"));
process.env["LLM_BOT_NAME"] = botName;
process.env["LLM_BOT_ROOT_DIR"] = path.resolve(rootDir, "bots", botName);
process.env["LLM_BOT_CACHE_DIR"] = path.resolve(rootDir, "cache", botName);
}
function loadEnv(filePath) {
try {
const data = fs.readFileSync(filePath, "utf-8");
const lines = data.split("\n");
lines.forEach((line) => {
if (line.trim().startsWith("#") || line.trim() === "") return;
const [key, ...value] = line.split("=");
process.env[key.trim()] = value.join("=").trim();
});
} catch {}
}
async function run(botPath, botFunc, botData) {
let mod;
if (os.platform() === "win32") {
botPath = `file://${botPath}`;
}
try {
mod = await import(botPath);
} catch {
throw new Error(`Unable to load bot tools at '${botPath}'`);
}
if (!mod || !mod[botFunc]) {
throw new Error(`Not module function '${botFunc}' at '${botPath}'`);
}
const value = await mod[botFunc](botData);
dumpValue(value);
}
function dumpValue(value) {
if (value === null || value === undefined) {
return;
}
const type = typeof value;
if (type === "string" || type === "number" || type === "boolean") {
console.log(value);
} else if (type === "object") {
const proto = Object.prototype.toString.call(value);
if (proto === "[object Object]" || proto === "[object Array]") {
const valueStr = JSON.stringify(value, null, 2);
require("assert").deepStrictEqual(value, JSON.parse(valueStr));
console.log(valueStr);
}
}
}
main();
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python
import os
import json
import sys
import importlib.util
def main():
(bot_name, bot_func, raw_data) = parse_argv("run-bot.py")
bot_data = parse_raw_data(raw_data)
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
setup_env(root_dir, bot_name)
bot_tools_path = os.path.join(root_dir, f"bots/{bot_name}/tools.py")
run(bot_tools_path, bot_func, bot_data)
def parse_raw_data(data):
if not data:
raise ValueError("No JSON data")
try:
return json.loads(data)
except Exception:
raise ValueError("Invalid JSON data")
def parse_argv(this_file_name):
argv = sys.argv[:] + [None] * max(0, 4 - len(sys.argv))
bot_name = argv[0]
bot_func = ""
bot_data = None
if bot_name.endswith(this_file_name):
bot_name = sys.argv[1]
bot_func = sys.argv[2]
bot_data = sys.argv[3]
else:
bot_name = os.path.basename(bot_name)
bot_func = sys.argv[1]
bot_data = sys.argv[2]
if bot_name.endswith(".py"):
bot_name = bot_name[:-3]
return bot_name, bot_func, bot_data
def setup_env(root_dir, bot_name):
os.environ["LLM_ROOT_DIR"] = root_dir
load_env(os.path.join(root_dir, ".env"))
os.environ["LLM_BOT_NAME"] = bot_name
os.environ["LLM_BOT_ROOT_DIR"] = os.path.join(root_dir, "bots", bot_name)
os.environ["LLM_BOT_CACHE_DIR"] = os.path.join(root_dir, "cache", bot_name)
def load_env(file_path):
try:
with open(file_path, "r") as f:
for line in f:
line = line.strip()
if line.startswith("#") or line == "":
continue
key, *value = line.split("=")
os.environ[key.strip()] = "=".join(value).strip()
except FileNotFoundError:
pass
def run(bot_path, bot_func, bot_data):
try:
spec = importlib.util.spec_from_file_location(
os.path.basename(bot_path), bot_path
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
except:
raise Exception(f"Unable to load bot tools at '{bot_path}'")
if not hasattr(mod, bot_func):
raise Exception(f"Not module function '{bot_func}' at '{bot_path}'")
value = getattr(mod, bot_func)(**bot_data)
dump_value(value)
def dump_value(value):
if value is None:
return
value_type = type(value).__name__
if value_type in ("str", "int", "float", "bool"):
print(value)
elif value_type == "dict" or value_type == "list":
value_str = json.dumps(value, indent=2)
assert value == json.loads(value_str)
print(value_str)
if __name__ == "__main__":
main()
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -e
main() {
this_file_name=run-bot.sh
parse_argv "$@"
root_dir="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)"
setup_env
bot_tools_path="$root_dir/bots/$bot_name/tools.sh"
run
}
parse_argv() {
if [[ "$0" == *"$this_file_name" ]]; then
bot_name="$1"
bot_func="$2"
bot_data="$3"
else
bot_name="$(basename "$0")"
bot_func="$1"
bot_data="$2"
fi
if [[ "$bot_name" == *.sh ]]; then
bot_name="${bot_name:0:$((${#bot_name}-3))}"
fi
}
setup_env() {
export LLM_ROOT_DIR="$root_dir"
if [[ -f "$LLM_ROOT_DIR/.env" ]]; then
source "$LLM_ROOT_DIR/.env"
fi
export LLM_BOT_NAME="$bot_name"
export LLM_BOT_ROOT_DIR="$LLM_ROOT_DIR/bots/$bot_name"
export LLM_BOT_CACHE_DIR="$LLM_ROOT_DIR/cache/$bot_name"
}
run() {
if [[ -z "$bot_data" ]]; then
die "No JSON data"
fi
_jq=jq
if [[ "$OS" == "Windows_NT" ]]; then
_jq="jq -b"
bot_tools_path="$(cygpath -w "$bot_tools_path")"
fi
data="$(
echo "$bot_data" | \
$_jq -r '
to_entries | .[] |
(.key | split("_") | join("-")) as $key |
if .value | type == "array" then
.value | .[] | "--\($key)\n\(. | @json)"
elif .value | type == "boolean" then
if .value then "--\($key)" else "" end
else
"--\($key)\n\(.value | @json)"
end'
)" || {
die "Invalid JSON data"
}
while IFS= read -r line; do
if [[ "$line" == '--'* ]]; then
args+=("$line")
else
args+=("$(echo "$line" | $_jq -r '.')")
fi
done <<< "$data"
"$bot_tools_path" "$bot_func" "${args[@]}"
}
die() {
echo "$*" >&2
exit 1
}
main "$@"