From 882942385bf256ec3dfb2869d7fcfebadafd0f18 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 10 Oct 2025 09:58:20 -0600 Subject: [PATCH] feat: Embedded baseline MCP config and global tools --- assets/functions/mcp.json | 35 + assets/functions/tools.txt | 24 + assets/functions/tools/demo_py.py | 38 + assets/functions/tools/demo_sh.sh | 29 + assets/functions/tools/execute_command.sh | 204 +++++ assets/functions/tools/execute_py_code.py | 33 + assets/functions/tools/execute_sql_code.sh | 22 + assets/functions/tools/fetch_url_via_curl.sh | 18 + assets/functions/tools/fetch_url_via_jina.sh | 17 + assets/functions/tools/fs_cat.sh | 202 +++++ assets/functions/tools/fs_ls.sh | 200 +++++ assets/functions/tools/fs_mkdir.sh | 201 +++++ assets/functions/tools/fs_patch.sh | 229 ++++++ assets/functions/tools/fs_rm.sh | 210 +++++ assets/functions/tools/fs_write.sh | 224 ++++++ assets/functions/tools/get_current_time.sh | 10 + assets/functions/tools/get_current_weather.py | 35 + assets/functions/tools/get_current_weather.sh | 199 +++++ assets/functions/tools/query_jira_issues.sh | 198 +++++ assets/functions/tools/search_arxiv.sh | 16 + assets/functions/tools/search_wikipedia.sh | 29 + assets/functions/tools/search_wolframalpha.sh | 19 + assets/functions/tools/send_mail.sh | 30 + assets/functions/tools/send_twilio.sh | 50 ++ assets/functions/tools/web_search_loki.sh | 254 +++++++ .../functions/tools/web_search_perplexity.sh | 32 + assets/functions/tools/web_search_tavily.sh | 24 + assets/functions/utils/prompt-utils.sh | 716 ++++++++++++++++++ src/function.rs | 44 +- 29 files changed, 3339 insertions(+), 3 deletions(-) create mode 100644 assets/functions/mcp.json create mode 100644 assets/functions/tools.txt create mode 100644 assets/functions/tools/demo_py.py create mode 100755 assets/functions/tools/demo_sh.sh create mode 100755 assets/functions/tools/execute_command.sh create mode 100644 assets/functions/tools/execute_py_code.py create mode 100755 assets/functions/tools/execute_sql_code.sh create mode 100755 assets/functions/tools/fetch_url_via_curl.sh create mode 100755 assets/functions/tools/fetch_url_via_jina.sh create mode 100755 assets/functions/tools/fs_cat.sh create mode 100755 assets/functions/tools/fs_ls.sh create mode 100755 assets/functions/tools/fs_mkdir.sh create mode 100755 assets/functions/tools/fs_patch.sh create mode 100755 assets/functions/tools/fs_rm.sh create mode 100755 assets/functions/tools/fs_write.sh create mode 100755 assets/functions/tools/get_current_time.sh create mode 100644 assets/functions/tools/get_current_weather.py create mode 100755 assets/functions/tools/get_current_weather.sh create mode 100755 assets/functions/tools/query_jira_issues.sh create mode 100755 assets/functions/tools/search_arxiv.sh create mode 100755 assets/functions/tools/search_wikipedia.sh create mode 100755 assets/functions/tools/search_wolframalpha.sh create mode 100755 assets/functions/tools/send_mail.sh create mode 100755 assets/functions/tools/send_twilio.sh create mode 100755 assets/functions/tools/web_search_loki.sh create mode 100755 assets/functions/tools/web_search_perplexity.sh create mode 100755 assets/functions/tools/web_search_tavily.sh create mode 100755 assets/functions/utils/prompt-utils.sh diff --git a/assets/functions/mcp.json b/assets/functions/mcp.json new file mode 100644 index 0000000..34cf36f --- /dev/null +++ b/assets/functions/mcp.json @@ -0,0 +1,35 @@ +{ + "mcpServers": { + "github": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "GITHUB_PERSONAL_ACCESS_TOKEN", + "ghcr.io/github/github-mcp-server" + ], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "{{GITHUB_PERSONAL_ACCESS_TOKEN}}" + } + }, + "gitmcp": { + "command": "npx", + "args": ["mcp-remote", "https://gitmcp.io/docs"] + }, + "docker": { + "command": "uvx", + "args": ["docker-mcp"] + }, + "slack": { + "command": "npx", + "args": ["-y", "slack-mcp-server@latest", "--transport", "stdio"], + "env": { + "SLACK_MCP_XOXC_TOKEN": "{{SLACK_MCP_XOXC_TOKEN}}", + "SLACK_MCP_XOXD_TOKEN": "{{SLACK_MCP_XOXD_TOKEN}}", + "SLACK_MCP_ADD_MESSAGE_TOOL": true + } + } + } +} diff --git a/assets/functions/tools.txt b/assets/functions/tools.txt new file mode 100644 index 0000000..f09e7ad --- /dev/null +++ b/assets/functions/tools.txt @@ -0,0 +1,24 @@ +# demo_py.py +# demo_sh.sh +execute_command.sh +# execute_py_code.py +# execute_sql_code.sh +# fetch_url_via_curl.sh +# fetch_url_via_jina.sh +fs_cat.sh +fs_ls.sh +# fs_mkdir.sh +# fs_patch.sh +# fs_write.sh +get_current_time.sh +# get_current_weather.py +get_current_weather.sh +query_jira_issues.sh +# search_arxiv.sh +# search_wikipedia.sh +# search_wolframalpha.sh +# send_mail.sh +# send_twilio.sh +# web_search_loki.sh +# web_search_perplexity.sh +# web_search_tavily.sh diff --git a/assets/functions/tools/demo_py.py b/assets/functions/tools/demo_py.py new file mode 100644 index 0000000..8b0ca0a --- /dev/null +++ b/assets/functions/tools/demo_py.py @@ -0,0 +1,38 @@ +import os +from typing import List, Literal, Optional + +def run( + string: str, + string_enum: Literal["foo", "bar"], + boolean: bool, + integer: int, + number: float, + array: List[str], + string_optional: Optional[str] = None, + array_optional: Optional[List[str]] = None, +): + """Demonstrates how to create a tool using Python and how to use comments. + Args: + string: Define a required string property + string_enum: Define a required string property with enum + boolean: Define a required boolean property + integer: Define a required integer property + number: Define a required number property + array: Define a required string array property + string_optional: Define an optional string property + array_optional: Define an optional string array property + """ + output = f"""string: {string} +string_enum: {string_enum} +string_optional: {string_optional} +boolean: {boolean} +integer: {integer} +number: {number} +array: {array} +array_optional: {array_optional}""" + + for key, value in os.environ.items(): + if key.startswith("LLM_"): + output = f"{output}\n{key}: {value}" + + return output diff --git a/assets/functions/tools/demo_sh.sh b/assets/functions/tools/demo_sh.sh new file mode 100755 index 0000000..6e10754 --- /dev/null +++ b/assets/functions/tools/demo_sh.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -e + +# @describe Demonstrate how to create a tool using Bash and how to use comment tags. +# @option --string! Define a required string property +# @option --string-enum![foo|bar] Define a required string property with enum +# @option --string-optional Define a optional string property +# @flag --boolean Define a boolean property +# @option --integer! Define a required integer property +# @option --number! Define a required number property +# @option --array+ Define a required string array property +# @option --array-optional* Define a optional string array property + +# @env LLM_OUTPUT=/dev/stdout The output path + +# shellcheck disable=SC2154 +main() { + cat <> "$LLM_OUTPUT" +string: ${argc_string} +string_enum: ${argc_string_enum} +string_optional: ${argc_string_optional} +boolean: ${argc_boolean} +integer: ${argc_integer} +number: ${argc_number} +array: ${argc_array[@]} +array_optional: ${argc_array_optional[@]} +$(printenv | grep '^LLM_') +EOF +} diff --git a/assets/functions/tools/execute_command.sh b/assets/functions/tools/execute_command.sh new file mode 100755 index 0000000..57ac528 --- /dev/null +++ b/assets/functions/tools/execute_command.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +set -e + +# @describe Execute the shell command. +# @option --command! The command to execute. + +# @env LLM_OUTPUT=/dev/stdout The output path + +PROMPT_UTILS="${LLM_ROOT_DIR:-$(dirname "${BASH_SOURCE[0]}")/..}/utils/prompt-utils.sh" +# shellcheck disable=SC1090 +source "$PROMPT_UTILS" + +main() { + guard_operation + # shellcheck disable=SC2154 + eval "$argc_command" >> "$LLM_OUTPUT" +} + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +Execute the shell command. + +USAGE: --command + +OPTIONS: + --command The command to execute. + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + LLM_OUTPUT The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --command) + _argc_take_args "--command " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_command:-}" ]]; then + argc_command="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--command\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_command:--command ' + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } diff --git a/assets/functions/tools/execute_py_code.py b/assets/functions/tools/execute_py_code.py new file mode 100644 index 0000000..71d66e1 --- /dev/null +++ b/assets/functions/tools/execute_py_code.py @@ -0,0 +1,33 @@ +import ast +import io +from contextlib import redirect_stdout + + +def run(code: str): + """Execute the python code. + Args: + code: Python code to execute, such as `print("hello world")` + """ + output = io.StringIO() + with redirect_stdout(output): + value = exec_with_return(code, {}, {}) + + if value is not None: + output.write(str(value)) + + return output.getvalue() + + +def exec_with_return(code: str, globals: dict, locals: dict): + a = ast.parse(code) + last_expression = None + if a.body: + if isinstance(a_last := a.body[-1], ast.Expr): + last_expression = ast.unparse(a.body.pop()) + elif isinstance(a_last, ast.Assign): + last_expression = ast.unparse(a_last.targets[0]) + elif isinstance(a_last, (ast.AnnAssign, ast.AugAssign)): + last_expression = ast.unparse(a_last.target) + exec(ast.unparse(a), globals, locals) + if last_expression: + return eval(last_expression, globals, locals) diff --git a/assets/functions/tools/execute_sql_code.sh b/assets/functions/tools/execute_sql_code.sh new file mode 100755 index 0000000..3b03cd7 --- /dev/null +++ b/assets/functions/tools/execute_sql_code.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -e + +# @describe Execute sql code. +# @option --code! The code to execute. + +# @meta require-tools usql + +# @env USQL_DSN! The database connection url. e.g. pgsql://user:pass@host:port +# @env LLM_OUTPUT=/dev/stdout The output path + +PROMPT_UTILS="${LLM_ROOT_DIR:-$(dirname "${BASH_SOURCE[0]}")/..}/utils/prompt-utils.sh" +# shellcheck disable=SC1090 +source "$PROMPT_UTILS" + +# shellcheck disable=SC2154 +main() { + if ! grep -qi '^select' <<<"$argc_code"; then + guard_operation "" + fi + usql -c "$argc_code" "$USQL_DSN" >> "$LLM_OUTPUT" +} diff --git a/assets/functions/tools/fetch_url_via_curl.sh b/assets/functions/tools/fetch_url_via_curl.sh new file mode 100755 index 0000000..307d089 --- /dev/null +++ b/assets/functions/tools/fetch_url_via_curl.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -e + +# @describe Extract the content from a given URL. +# @option --url! The URL to scrape. + +# @meta require-tools pandoc + +# @env LLM_OUTPUT=/dev/stdout The output path + +# shellcheck disable=SC2154 +main() { + # span and div tags are dropped from the HTML https://pandoc.org/MANUAL.html#raw-htmltex and sed removes any inline SVG images in image tags from the Markdown content. + curl -fsSL "$argc_url" | \ + pandoc -f html-native_divs-native_spans -t gfm-raw_html --wrap=none | \ + sed -E 's/!\[[^]]*\]\([^)]*\)//g' \ + >> "$LLM_OUTPUT" +} diff --git a/assets/functions/tools/fetch_url_via_jina.sh b/assets/functions/tools/fetch_url_via_jina.sh new file mode 100755 index 0000000..a0b6dd4 --- /dev/null +++ b/assets/functions/tools/fetch_url_via_jina.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -e + +# @describe Extract the content from a given URL. +# @option --url! The URL to scrape. + +# @env JINA_API_KEY Your Jina API key +# @env LLM_OUTPUT=/dev/stdout The output path + +# shellcheck disable=SC2154 +main() { + curl_args=() + if [[ -n "$JINA_API_KEY" ]]; then + curl_args+=("-H" "Authorization: Bearer $JINA_API_KEY") + fi + curl -fsSL "${curl_args[@]}" "https://r.jina.ai/$argc_url" >> "$LLM_OUTPUT" +} diff --git a/assets/functions/tools/fs_cat.sh b/assets/functions/tools/fs_cat.sh new file mode 100755 index 0000000..9143082 --- /dev/null +++ b/assets/functions/tools/fs_cat.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +set -e + +# @describe Read the contents of a file at the specified path. +# Use this when you need to examine the contents of an existing file. + +# @option --path! The path of the file to read + +# @env LLM_OUTPUT=/dev/stdout The output path + +main() { + # shellcheck disable=SC2154 + cat "$argc_path" >> "$LLM_OUTPUT" +} + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +Read the contents of a file at the specified path. +Use this when you need to examine the contents of an existing file. + +USAGE: --path + +OPTIONS: + --path The path of the file to read + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + LLM_OUTPUT The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --path) + _argc_take_args "--path " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_path:-}" ]]; then + argc_path="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--path\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_path:--path ' + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } \ No newline at end of file diff --git a/assets/functions/tools/fs_ls.sh b/assets/functions/tools/fs_ls.sh new file mode 100755 index 0000000..7ac19d5 --- /dev/null +++ b/assets/functions/tools/fs_ls.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +set -e + +# @describe List all files and directories at the specified path. + +# @option --path! The path of the directory to list + +# @env LLM_OUTPUT=/dev/stdout The output path + +main() { + # shellcheck disable=SC2154 + ls -1 "$argc_path" >> "$LLM_OUTPUT" +} + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +List all files and directories at the specified path. + +USAGE: --path + +OPTIONS: + --path The path of the directory to list + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + LLM_OUTPUT The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --path) + _argc_take_args "--path " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_path:-}" ]]; then + argc_path="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--path\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_path:--path ' + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } diff --git a/assets/functions/tools/fs_mkdir.sh b/assets/functions/tools/fs_mkdir.sh new file mode 100755 index 0000000..8017eb5 --- /dev/null +++ b/assets/functions/tools/fs_mkdir.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +set -e + +# @describe Create a new directory at the specified path. + +# @option --path! The path of the directory to create + +# @env LLM_OUTPUT=/dev/stdout The output path + +main() { + # shellcheck disable=SC2154 + mkdir -p "$argc_path" + echo "Directory created: $argc_path" >> "$LLM_OUTPUT" +} + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +Create a new directory at the specified path. + +USAGE: --path + +OPTIONS: + --path The path of the directory to create + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + LLM_OUTPUT The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --path) + _argc_take_args "--path " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_path:-}" ]]; then + argc_path="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--path\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_path:--path ' + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } \ No newline at end of file diff --git a/assets/functions/tools/fs_patch.sh b/assets/functions/tools/fs_patch.sh new file mode 100755 index 0000000..7450560 --- /dev/null +++ b/assets/functions/tools/fs_patch.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +set -e + +# @describe Apply a patch to a file at the specified path. +# This can be used to edit the file without having to rewrite the whole file. + +# @option --path! The path of the file to apply the patch to +# @option --contents! The patch to apply to the file + +# @env LLM_OUTPUT=/dev/stdout The output path + +PROMPT_UTILS="${LLM_ROOT_DIR:-$(dirname "${BASH_SOURCE[0]}")/..}/utils/prompt-utils.sh" +# shellcheck disable=SC1090 +source "$PROMPT_UTILS" + +# shellcheck disable=SC2154 +main() { + if [[ ! -f "$argc_path" ]]; then + error "Unable to find the specified file: $argc_path" + exit 1 + fi + + new_contents="$(patch_file "$argc_path" <(printf "%s" "$argc_contents"))" + printf "%s" "$new_contents" | git diff --no-index "$argc_path" - || true + + guard_operation "Apply changes?" + + printf "%s" "$new_contents" > "$argc_path" + + info "Applied the patch to: $argc_path" >> "$LLM_OUTPUT" +} + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +Apply a patch to a file at the specified path. +This can be used to edit the file without having to rewrite the whole file. + +USAGE: --path --contents + +OPTIONS: + --path The path of the file to apply the patch to + --contents The patch to apply to the file + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + LLM_OUTPUT The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --path) + _argc_take_args "--path " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_path:-}" ]]; then + argc_path="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--path\` cannot be used multiple times" + fi + ;; + --contents) + _argc_take_args "--contents " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_contents:-}" ]]; then + argc_contents="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--contents\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_path:--path ' 'argc_contents:--contents ' + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } diff --git a/assets/functions/tools/fs_rm.sh b/assets/functions/tools/fs_rm.sh new file mode 100755 index 0000000..e181c2d --- /dev/null +++ b/assets/functions/tools/fs_rm.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +set -e + +# @describe Remove the file or directory at the specified path. + +# @option --path! The path of the file or directory to remove + +# @env LLM_OUTPUT=/dev/stdout The output path + +PROMPT_UTILS="${LLM_ROOT_DIR:-$(dirname "${BASH_SOURCE[0]}")/..}/utils/prompt-utils.sh" +# shellcheck disable=SC1090 +source "$PROMPT_UTILS" + +# shellcheck disable=SC2154 +main() { + if [[ -f "$argc_path" ]]; then + guard_path "$argc_path" "Remove '$argc_path'?" + rm -rf "$argc_path" + fi + + echo "Path removed: $argc_path" >> "$LLM_OUTPUT" +} + + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +Remove the file or directory at the specified path. + +USAGE: --path + +OPTIONS: + --path The path of the file or directory to remove + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + LLM_OUTPUT The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --path) + _argc_take_args "--path " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_path:-}" ]]; then + argc_path="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--path\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_path:--path ' + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } diff --git a/assets/functions/tools/fs_write.sh b/assets/functions/tools/fs_write.sh new file mode 100755 index 0000000..8e69108 --- /dev/null +++ b/assets/functions/tools/fs_write.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +set -e + +# @describe Write the full file contents to a file at the specified path. + +# @option --path! The path of the file to write to +# @option --contents! The full contents to write to the file + +# @env LLM_OUTPUT=/dev/stdout The output path + +PROMPT_UTILS="${LLM_ROOT_DIR:-$(dirname "${BASH_SOURCE[0]}")/..}/utils/prompt-utils.sh" +# shellcheck disable=SC1090 +source "$PROMPT_UTILS" + +# shellcheck disable=SC2154 +main() { + if [[ -f "$argc_path" ]]; then + printf "%s" "$argc_contents" | git diff --no-index "$argc_path" - || true + guard_operation "Apply changes?" + else + guard_path "$argc_path" "Write '$argc_path'?" + mkdir -p "$(dirname "$argc_path")" + fi + + printf "%s" "$argc_contents" > "$argc_path" + echo "The File contents were written to: $argc_path" >> "$LLM_OUTPUT" +} + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +Write the full file contents to a file at the specified path. + +USAGE: --path --contents + +OPTIONS: + --path The path of the file to write to + --contents The full contents to write to the file + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + LLM_OUTPUT The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --path) + _argc_take_args "--path " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_path:-}" ]]; then + argc_path="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--path\` cannot be used multiple times" + fi + ;; + --contents) + _argc_take_args "--contents " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_contents:-}" ]]; then + argc_contents="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--contents\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_path:--path ' 'argc_contents:--contents ' + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } diff --git a/assets/functions/tools/get_current_time.sh b/assets/functions/tools/get_current_time.sh new file mode 100755 index 0000000..d1b4d51 --- /dev/null +++ b/assets/functions/tools/get_current_time.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +# @describe Get the current time. + +# @env LLM_OUTPUT=/dev/stdout The output path + +main() { + date >> "$LLM_OUTPUT" +} diff --git a/assets/functions/tools/get_current_weather.py b/assets/functions/tools/get_current_weather.py new file mode 100644 index 0000000..6a78460 --- /dev/null +++ b/assets/functions/tools/get_current_weather.py @@ -0,0 +1,35 @@ +import os +from pathlib import Path +from typing import Optional +from urllib.parse import quote_plus +from urllib.request import urlopen + + +def run( + location: str, + llm_output: Optional[str] = None, +) -> str: + """Get the current weather in a given location + + Args: + location (str): The city and optionally the state or country (e.g., "London", "San Francisco, CA"). + + Returns: + str: A single-line formatted weather string from wttr.in (``format=4`` with metric units). + """ + url = f"https://wttr.in/{quote_plus(location)}?format=4&M" + + with urlopen(url, timeout=10) as resp: + weather = resp.read().decode("utf-8", errors="replace") + + dest = llm_output if llm_output is not None else os.environ.get("LLM_OUTPUT", "/dev/stdout") + + if dest not in {"-", "/dev/stdout"}: + path = Path(dest) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as fh: + fh.write(weather) + else: + pass + + return weather diff --git a/assets/functions/tools/get_current_weather.sh b/assets/functions/tools/get_current_weather.sh new file mode 100755 index 0000000..2ee88c3 --- /dev/null +++ b/assets/functions/tools/get_current_weather.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +set -e + +# @describe Get the current weather in a given location. +# @option --location! The city and optionally the state or country, e.g., "London", "San Francisco, CA". + +# @env LLM_OUTPUT=/dev/stdout The output path + +main() { + curl -fsSL "https://wttr.in/$(echo "$argc_location" | sed 's/ /+/g')?format=4&M" \ + >> "$LLM_OUTPUT" +} + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +Get the current weather in a given location. + +USAGE: --location + +OPTIONS: + --location The city and optionally the state or country, e.g., "London", "San Francisco, CA". + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + LLM_OUTPUT The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --location) + _argc_take_args "--location " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_location:-}" ]]; then + argc_location="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--location\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_location:--location ' + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } \ No newline at end of file diff --git a/assets/functions/tools/query_jira_issues.sh b/assets/functions/tools/query_jira_issues.sh new file mode 100755 index 0000000..9bc7f86 --- /dev/null +++ b/assets/functions/tools/query_jira_issues.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +set -e + +# @meta require-tools jira +# @describe Query for jira issues using a Jira Query Language (JQL) query +# @option --jql-query! The Jira Query Language query to execute +# @env LLM_OUTPUT=/dev/stdout The output path + +main() { + jira issue ls -q "$argc_jql_query" --plain >> "$LLM_OUTPUT" +} + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +Query for jira issues using a Jira Query Language (JQL) query + +USAGE: --jql-query + +OPTIONS: + --jql-query The Jira Query Language query to execute + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + LLM_OUTPUT The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --jql-query) + _argc_take_args "--jql-query " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_jql_query:-}" ]]; then + argc_jql_query="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--jql-query\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_jql_query:--jql-query ' + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } diff --git a/assets/functions/tools/search_arxiv.sh b/assets/functions/tools/search_arxiv.sh new file mode 100755 index 0000000..c499188 --- /dev/null +++ b/assets/functions/tools/search_arxiv.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -e + +# @describe Search arXiv using the given search query and return the top papers. + +# @option --query! The search query. + +# @env ARXIV_MAX_RESULTS=3 The max results to return. +# @env LLM_OUTPUT=/dev/stdout The output path + +main() { + # shellcheck disable=SC2154 + encoded_query="$(jq -nr --arg q "$argc_query" '$q|@uri')" + url="http://export.arxiv.org/api/query?search_query=all:$encoded_query&max_results=$ARXIV_MAX_RESULTS" + curl -fsSL "$url" >> "$LLM_OUTPUT" +} diff --git a/assets/functions/tools/search_wikipedia.sh b/assets/functions/tools/search_wikipedia.sh new file mode 100755 index 0000000..49e2819 --- /dev/null +++ b/assets/functions/tools/search_wikipedia.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -e + +# @describe Search Wikipedia using the given search query. +# Use it to get detailed information about a public figure, interpretation of a complex scientific concept or in-depth connectivity of a significant historical event, etc. + +# @option --query! The search query. + +# @env LLM_OUTPUT=/dev/stdout The output path + +# shellcheck disable=SC2154 +main() { + encoded_query="$(jq -nr --arg q "$argc_query" '$q|@uri')" + base_url="https://en.wikipedia.org/w/api.php" + url="$base_url?action=query&list=search&srprop=&srlimit=1&limit=1&srsearch=$encoded_query&srinfo=suggestion&format=json" + json="$(curl -fsSL "$url")" + # suggestion="$(echo "$json" | jq -r '.query.searchinfo.suggestion // empty')" + title="$(echo "$json" | jq -r '.query.search[0].title // empty')" + pageid="$(echo "$json" | jq -r '.query.search[0].pageid // empty')" + + if [[ -z "$title" || -z "$pageid" ]]; then + echo "error: no results for '$argc_query'" >&2 + exit 1 + fi + + title="$(echo "$title" | tr ' ' '_')" + url="$base_url?action=query&prop=extracts&explaintext=&titles=$title&exintro=&format=json" + curl -fsSL "$url" | jq -r '.query.pages["'"$pageid"'"].extract' >> "$LLM_OUTPUT" +} diff --git a/assets/functions/tools/search_wolframalpha.sh b/assets/functions/tools/search_wolframalpha.sh new file mode 100755 index 0000000..dcd5f05 --- /dev/null +++ b/assets/functions/tools/search_wolframalpha.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e + +# @describe Get an answer to a question using Wolfram Alpha. The input query should be in English. +# Use it to answer user questions that require computation, detailed facts, data analysis, or complex queries. + +# @option --query! The search/computation query to pass to Wolfram Alpha + +# @env WOLFRAM_API_ID! Your Wolfram Alpha API ID +# @env LLM_OUTPUT=/dev/stdout The output path + +# shellcheck disable=SC2154 +main() { + encoded_query="$(jq -nr --arg q "$argc_query" '$q|@uri')" + url="https://api.wolframalpha.com/v2/query?appid=${WOLFRAM_API_ID}&input=$encoded_query&output=json&format=plaintext" + + curl -fsSL "$url" | jq '[.queryresult | .pods[] | {title:.title, values:[.subpods[].plaintext | select(. != "")]}]' \ + >> "$LLM_OUTPUT" +} diff --git a/assets/functions/tools/send_mail.sh b/assets/functions/tools/send_mail.sh new file mode 100755 index 0000000..f90870f --- /dev/null +++ b/assets/functions/tools/send_mail.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e + +# @describe Send an email. +# @option --recipient! The recipient of the email. +# @option --subject! The subject of the email. +# @option --body! The body of the email. + +# @env EMAIL_SMTP_ADDR! The SMTP Address, e.g. smtps://smtp.gmail.com:465 +# @env EMAIL_SMTP_USER! The SMTP User, e.g. alice@gmail.com +# @env EMAIL_SMTP_PASS! The SMTP Password +# @env EMAIL_SENDER_NAME The sender's name +# @env LLM_OUTPUT=/dev/stdout The output path + +# shellcheck disable=SC2154 +main() { + sender_name="${EMAIL_SENDER_NAME:-$(echo "$EMAIL_SMTP_USER" | awk -F'@' '{print $1}')}" + printf "%s\n" "From: $sender_name <$EMAIL_SMTP_USER> +To: $argc_recipient +Subject: $argc_subject + +$argc_body" | \ + curl -fsS --ssl-reqd \ + --url "$EMAIL_SMTP_ADDR" \ + --user "$EMAIL_SMTP_USER:$EMAIL_SMTP_PASS" \ + --mail-from "$EMAIL_SMTP_USER" \ + --mail-rcpt "$argc_recipient" \ + --upload-file - + echo "Email sent successfully" >> "$LLM_OUTPUT" +} diff --git a/assets/functions/tools/send_twilio.sh b/assets/functions/tools/send_twilio.sh new file mode 100755 index 0000000..88d02ff --- /dev/null +++ b/assets/functions/tools/send_twilio.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -e + +# @describe Send SMS or Twilio Messaging Channels messages using the Twilio API. +# @option --to-number! The recipient's phone number. Prefix it with 'whatsapp:' for WhatsApp messages, e.g. whatsapp:+1234567890 +# @option --message! The content of the message to be sent + +# @env TWILIO_ACCOUNT_SID! The twilio account sid +# @env TWILIO_AUTH_TOKEN! The twilio auth token +# @env TWILIO_FROM_NUMBER! The twilio from number +# @env LLM_OUTPUT=/dev/stdout The output path + +# shellcheck disable=SC2154 +main() { + from_number="$TWILIO_FROM_NUMBER" + to_number="$argc_to_number" + + if [[ "$to_number" == 'whatsapp:'* ]]; then + from_number="whatsapp:$from_number" + fi + + if [[ "$to_number" != 'whatsapp:'* && "$to_number" != '+'* ]]; then + to_number="+$to_number" + fi + + res="$(curl -s -X POST "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Messages.json" \ + -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \ + -w "\n%{http_code}" \ + --data-urlencode "From=$from_number" \ + --data-urlencode "To=$to_number" \ + --data-urlencode "Body=$argc_message")" + status="$(echo "$res" | tail -n 1)" + body="$(echo "$res" | head -n -1)" + + if [[ "$status" -ge 200 && "$status" -lt 300 ]]; then + if [[ "$(echo "$body" | jq -r 'has("sid")')" == "true" ]]; then + echo "Message sent successfully" >> "$LLM_OUTPUT" + else + _die "error: $body" + fi + else + _die "error: $body" + fi +} + +_die() { + echo "$*" >&2 + exit 1 +} + diff --git a/assets/functions/tools/web_search_loki.sh b/assets/functions/tools/web_search_loki.sh new file mode 100755 index 0000000..7179ad3 --- /dev/null +++ b/assets/functions/tools/web_search_loki.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +set -e + +# @describe Perform a web search to get up-to-date information or additional context. +# Use this when you need current information or feel a search could provide a better answer. + +# @option --query! The search query. + +# @meta require-tools loki + +# @env WEB_SEARCH_MODEL=gemini:gemini-2.5-flash The model for web-searching. +# +# supported loki models: +# - gemini:gemini-2.0-* +# - vertexai:gemini-* +# - perplexity:* +# - ernie:* +# @env LLM_OUTPUT=/dev/stdout The output path + +# shellcheck disable=SC2154 +main() { + client="${WEB_SEARCH_MODEL%%:*}" + + if [[ "$client" == "gemini" ]]; then + export LOKI_PATCH_GEMINI_CHAT_COMPLETIONS='{".*":{"body":{"tools":[{"google_search":{}}]}}}' + elif [[ "$client" == "vertexai" ]]; then + export LOKI_PATCH_VERTEXAI_CHAT_COMPLETIONS='{ + "gemini-1.5-.*":{"body":{"tools":[{"googleSearchRetrieval":{}}]}}, + "gemini-2.0-.*":{"body":{"tools":[{"google_search":{}}]}} +}' + elif [[ "$client" == "ernie" ]]; then + export LOKI_PATCH_ERNIE_CHAT_COMPLETIONS='{".*":{"body":{"web_search":{"enable":true}}}}' + fi + + loki -m "$WEB_SEARCH_MODEL" "$argc_query" >> "$LLM_OUTPUT" +} + + +# ARGC-BUILD { +# This block was generated by argc (https://github.com/sigoden/argc). +# Modifying it manually is not recommended + +_argc_run() { + if [[ "${1:-}" == "___internal___" ]]; then + _argc_die "error: unsupported ___internal___ command" + fi + if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then + set -o igncr + fi + argc__args=("$(basename "$0" .sh)" "$@") + argc__positionals=() + _argc_index=1 + _argc_len="${#argc__args[@]}" + _argc_tools=() + _argc_parse + _argc_require_tools "${_argc_tools[@]}" + if [ -n "${argc__fn:-}" ]; then + $argc__fn "${argc__positionals[@]}" + fi +} + +_argc_usage() { + cat <<-'EOF' +Perform a web search to get up-to-date information or additional context. +Use this when you need current information or feel a search could provide a better answer. + +USAGE: --query + +OPTIONS: + --query The query to search for. + -h, --help Print help + -V, --version Print version + +ENVIRONMENTS: + WEB_SEARCH_MODEL + The model for web-searching. + + supported loki models: + - gemini:gemini-2.0-* + - vertexai:gemini-* + - perplexity:* + - ernie:* + [default: gemini:gemini-2.5-flash] + + LLM_OUTPUT + The output path [default: /dev/stdout] +EOF + exit +} + +_argc_version() { + echo 0.0.0 + exit +} + +_argc_parse() { + local _argc_key _argc_action + local _argc_subcmds="" + while [[ $_argc_index -lt $_argc_len ]]; do + _argc_item="${argc__args[_argc_index]}" + _argc_key="${_argc_item%%=*}" + case "$_argc_key" in + --help | -help | -h) + _argc_usage + ;; + --version | -version | -V) + _argc_version + ;; + --) + _argc_dash="${#argc__positionals[@]}" + argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}") + _argc_index=$_argc_len + break + ;; + --query) + _argc_take_args "--query " 1 1 "-" "" + _argc_index=$((_argc_index + _argc_take_args_len + 1)) + if [[ -z "${argc_query:-}" ]]; then + argc_query="${_argc_take_args_values[0]:-}" + else + _argc_die "error: the argument \`--query\` cannot be used multiple times" + fi + ;; + *) + if _argc_maybe_flag_option "-" "$_argc_item"; then + _argc_die "error: unexpected argument \`$_argc_key\` found" + fi + argc__positionals+=("$_argc_item") + _argc_index=$((_argc_index + 1)) + ;; + esac + done + _argc_require_params "error: the following required arguments were not provided:" \ + 'argc_query:--query ' + _argc_tools=(loki) + if [[ -n "${_argc_action:-}" ]]; then + $_argc_action + else + argc__fn=main + if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then + _argc_usage + fi + if [[ -z "${WEB_SEARCH_MODEL:-}" ]]; then + export WEB_SEARCH_MODEL=gemini:gemini-2.5-flash + fi + if [[ -z "${LLM_OUTPUT:-}" ]]; then + export LLM_OUTPUT=/dev/stdout + fi + fi +} + +_argc_take_args() { + _argc_take_args_values=() + _argc_take_args_len=0 + local param="$1" min="$2" max="$3" signs="$4" delimiter="$5" + if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then + return + fi + local _argc_take_index=$((_argc_index + 1)) _argc_take_value + if [[ "$_argc_item" == *=* ]]; then + _argc_take_args_values=("${_argc_item##*=}") + else + while [[ $_argc_take_index -lt $_argc_len ]]; do + _argc_take_value="${argc__args[_argc_take_index]}" + if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then + if [[ "${#_argc_take_value}" -gt 1 ]]; then + break + fi + fi + _argc_take_args_values+=("$_argc_take_value") + _argc_take_args_len=$((_argc_take_args_len + 1)) + if [[ "$_argc_take_args_len" -ge "$max" ]]; then + break + fi + _argc_take_index=$((_argc_take_index + 1)) + done + fi + if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then + _argc_die "error: incorrect number of values for \`$param\`" + fi + if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then + local item values arr=() + for item in "${_argc_take_args_values[@]}"; do + IFS="$delimiter" read -r -a values <<<"$item" + arr+=("${values[@]}") + done + _argc_take_args_values=("${arr[@]}") + fi +} + +_argc_require_params() { + local message="$1" missed_envs="" item name render_name + for item in "${@:2}"; do + name="${item%%:*}" + render_name="${item##*:}" + if [[ -z "${!name:-}" ]]; then + missed_envs="$missed_envs"$'\n'" $render_name" + fi + done + if [[ -n "${missed_envs}" ]]; then + _argc_die "$message$missed_envs" + fi +} + +_argc_maybe_flag_option() { + local signs="$1" arg="$2" + if [[ -z "$signs" ]]; then + return 1 + fi + local cond=false + if [[ "$signs" == *"+"* ]]; then + if [[ "$arg" =~ ^\+[^+].* ]]; then + cond=true + fi + elif [[ "$arg" == -* ]]; then + if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then + cond=true + fi + fi + if [[ "$cond" == "false" ]]; then + return 1 + fi + local value="${arg%%=*}" + if [[ "$value" =~ [[:space:]] ]]; then + return 1 + fi + return 0 +} + +_argc_require_tools() { + local tool missing_tools=() + for tool in "$@"; do + if ! command -v "$tool" >/dev/null 2>&1; then + missing_tools+=("$tool") + fi + done + if [[ "${#missing_tools[@]}" -gt 0 ]]; then + echo "error: missing tools: ${missing_tools[*]}" >&2 + exit 1 + fi +} + +_argc_die() { + if [[ $# -eq 0 ]]; then + cat + else + echo "$*" >&2 + fi + exit 1 +} + +_argc_run "$@" + +# ARGC-BUILD } diff --git a/assets/functions/tools/web_search_perplexity.sh b/assets/functions/tools/web_search_perplexity.sh new file mode 100755 index 0000000..28ce01e --- /dev/null +++ b/assets/functions/tools/web_search_perplexity.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -e + +# @describe Perform a web search using the Perplexity API to get up-to-date information or additional context. +# Use this when you need current information or feel a search could provide a better answer. + +# @option --query! The search query. + +# @env PERPLEXITY_API_KEY! Your Perplexity API key +# @env PERPLEXITY_WEB_SEARCH_MODEL=llama-3.1-sonar-small-128k-online The LLM model to use for the search +# @env LLM_OUTPUT=/dev/stdout The output path + +# shellcheck disable=SC2154 +main() { + curl -fsS -X POST https://api.perplexity.ai/chat/completions \ + -H "authorization: Bearer $PERPLEXITY_API_KEY" \ + -H "accept: application/json" \ + -H "content-type: application/json" \ + --data ' +{ + "model": "'"$PERPLEXITY_WEB_SEARCH_MODEL"'", + "messages": [ + { + "role": "user", + "content": "'"$argc_query"'" + } + ] +} +' | \ + jq -r '.choices[0].message.content' \ + >> "$LLM_OUTPUT" +} diff --git a/assets/functions/tools/web_search_tavily.sh b/assets/functions/tools/web_search_tavily.sh new file mode 100755 index 0000000..79ba724 --- /dev/null +++ b/assets/functions/tools/web_search_tavily.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -e + +# @describe Perform a web search using the Tavily API to get up-to-date information or additional context. +# Use this when you need current information or feel a search could provide a better answer. + +# @option --query! The search query. + +# @env TAVILY_API_KEY! Your Tavile API key +# @env LLM_OUTPUT=/dev/stdout The output path The output path + +# shellcheck disable=SC2154 +main() { + curl -fsSL -X POST https://api.tavily.com/search \ + -H "content-type: application/json" \ + -d ' +{ + "api_key": "'"$TAVILY_API_KEY"'", + "query": "'"$argc_query"'", + "include_answer": true +}' | \ + jq -r '.answer' >> "$LLM_OUTPUT" +} + diff --git a/assets/functions/utils/prompt-utils.sh b/assets/functions/utils/prompt-utils.sh new file mode 100755 index 0000000..2e0d4eb --- /dev/null +++ b/assets/functions/utils/prompt-utils.sh @@ -0,0 +1,716 @@ +#!/usr/bin/env bash + +# Bash helper utilities for prompting users. +# This is a modified version of the excellent Bash TUI toolkit +# (https://github.com/timo-reymann/bash-tui-toolkit) +# +# It includes the following functions for you to use in your +# bash tool commands: +# +# - password Password prompt +# - checked Checkbox +# - text Text input with validation +# - list Select an option from a given list +# - range Prompt the user for a value within a range +# - confirm Confirmation prompt +# - editor Open the user's preferred editor for input +# - detect_os Detect the current OS +# - get_opener Get the file opener for the current OS +# - open_link Open the given link in the default browser +# See https://github.com/timo-reymann/bash-tui-toolkit/blob/main/test.sh +# for examples on how to use these commands +# +# - guard_operation Prompt for permission to run an operation +# - guard_path Prompt for permission to perform path operations +# - patch_file Patch a file +# - error Log an error +# - warn Log a warning +# - info Log info +# - debug Log a debug message +# - trace Log a trace message +# - red Output given text in red +# - green Output given text in green +# - gold Output given text in gold +# - blue Output given text in blue +# - magenta Output given text in magenta +# - cyan Output given text in cyan +# - white Output given text in white + +# shellcheck disable=SC2034 +red=$(tput setaf 1) +green=$(tput setaf 2) +gold=$(tput setaf 3) +blue=$(tput setaf 4) +magenta=$(tput setaf 5) +cyan=$(tput setaf 6) +white=$(tput setaf 7) + +default=$(tput sgr0) +gray=$(tput setaf 243) + +bold=$(tput bold) +underlined=$(tput smul) + +error() { + echo -e "${red}${bold}ERROR:${default}${red} $1${default}" +} + +warn() { + echo -e "${gold}${bold}WARN:${default}${gold} $1${default}" +} + +info() { + echo -e "${cyan}${bold}INFO:${default}${cyan} $1${default}" +} + +debug() { + echo -e "${blue}${bold}DEBUG:${default}${blue} $1${default}" +} + +trace() { + echo -e "${gray}${bold}TRACE:${default}${gray} $1${default}" +} + +success() { + echo -e "${green}${bold}SUCCESS:${default}${green} $1${default}" +} + +red() { + echo -e "${red}$1${default}" +} + +green() { + echo -e "${green}$1${default}" +} + +gold() { + echo -e "${gold}$1${default}" +} + +blue() { + echo -e "${blue}$1${default}" +} + +magenta() { + echo -e "${magenta}$1${default}" +} + +cyan() { + echo -e "${cyan}$1${default}" +} + +white() { + echo -e "${white}$1${default}" +} + +_read_stdin() { + read -r "$@" &2 +} +_cursor_blink_off() { + echo -en "\033[?25l" >&2 +} + +_cursor_to() { + echo -en "\033[$1;$2H" >&2 +} + +# shellcheck disable=SC2154 +_key_input() { + declare ESC=$'\033' + declare IFS='' + _read_stdin -rsn1 a + if [[ "$ESC" == "$a" ]]; then + _read_stdin -rsn2 b + fi + + declare input="${a}${b}" + case "$input" in + "${ESC}[A" | "k") echo up ;; + "${ESC}[B" | "j") echo down ;; + "${ESC}[C" | "l") echo right ;; + "${ESC}[D" | "h") echo left ;; + '') echo enter ;; + ' ') echo space ;; + esac +} + +_new_line_foreach_item() { + count=0 + while [[ $count -lt $1 ]]; do + echo "" >&2 + ((count++)) + done +} + +_prompt_text() { + echo -en "\033[32m?\033[0m\033[1m ${1}\033[0m " >&2 +} + +_decrement_selected() { + declare selected=$1 + ((selected--)) + if [[ "${selected}" -lt 0 ]]; then + selected=$(($2 - 1)) + fi + + echo -n $selected +} + +_increment_selected() { + declare selected=$1 + ((selected++)) + + if [[ "${selected}" -ge "${opts_count}" ]]; then + selected=0 + fi + + echo -n $selected +} + +# shellcheck disable=SC2154 +input() { + _prompt_text "$1" + echo -en "\033[36m\c" >&2 + _read_stdin -r text + echo -n "${text}" +} + +confirm() { + trap "stty echo; exit" EXIT + _prompt_text "$1 (y/N)" + echo -en "\033[36m\c " >&2 + + declare first_row + first_row=$(_get_cursor_row) + declare current_row + current_row=$((first_row - 1)) + declare result="" + echo -n " " >&2 + + while true; do + echo -e "\033[1D\c " >&2 + _read_stdin -n1 result + + case "$result" in + y | Y) + echo -n 1 + break + ;; + n | N | *) + echo -n 0 + break + ;; + esac + done + + echo -en "\033[0m" >&2 + echo "" >&2 +} + +list() { + _prompt_text "$1 " + declare opts=("${@:2}") + declare opts_count=$(($# - 1)) + + _new_line_foreach_item "${#opts[@]}" + + declare last_row + last_row=$(_get_cursor_row) + declare first_row + first_row=$((last_row - opts_count + 1)) + + trap "_cursor_blink_on; stty echo; exit" 2 + + _cursor_blink_off + + declare selected=0 + while true; do + declare idx=0 + for opt in "${opts[@]}"; do + _cursor_to $((first_row + idx)) + + if [[ $idx -eq "$selected" ]]; then + printf "\033[0m\033[36m❯\033[0m \033[36m%s\033[0m" "$opt" >&2 + else + printf " %s" "$opt" >&2 + fi + + ((idx++)) + done + + case $(_key_input) in + enter) break ;; + up) selected=$(_decrement_selected "${selected}" "${opts_count}") ;; + down) selected=$(_increment_selected "${selected}" "${opts_count}") ;; + esac + done + + echo -en "\n" >&2 + _cursor_to "${last_row}" + _cursor_blink_on + echo -n "${selected}" +} + +checkbox() { + _prompt_text "$1" + declare opts + opts=("${@:2}") + declare opts_count + opts_count=$(($# - 1)) + + _new_line_foreach_item "${#opts[@]}" + + declare last_row + last_row=$(_get_cursor_row) + declare first_row + first_row=$((last_row - opts_count + 1)) + + trap "_cursor_blink_on; stty echo; exit" 2 + + _cursor_blink_off + + declare selected=0 + declare checked=() + while true; do + declare idx=0 + for opt in "${opts[@]}"; do + _cursor_to $((first_row + idx)) + declare icon="◯" + + for item in "${checked[@]}"; do + if [[ "$item" == "$idx" ]]; then + icon="◉" + break + fi + done + + if [[ $idx -eq "$selected" ]]; then + printf "%s \e[0m\e[36m❯\e[0m \e[36m%-50s\e[0m" "$icon" "$opt" >&2 + else + printf "%s %-50s" "$icon" "$opt" >&2 + fi + + ((idx++)) + done + + case $(_key_input) in + enter) break ;; + space) + declare found=0 + for item in "${checked[@]}"; do + if [[ "$item" == "$selected" ]]; then + found=1 + break + fi + done + + if [ $found -eq 1 ]; then + checked=("${checked[@]/$selected/}") + else + checked+=("${selected}") + fi + ;; + up) selected=$(_decrement_selected "${selected}" "${opts_count}") ;; + down) selected=$(_increment_selected "${selected}" "${opts_count}") ;; + esac + done + + _cursor_to "${last_row}" + _cursor_blink_on + IFS="" echo -n "${checked[@]}" +} + +password() { + _prompt_text "$1" + echo -en "\033[36m" >&2 + declare password='' + declare IFS= + + while _read_stdin -r -s -n1 char; do + [[ -z "${char}" ]] && { + printf '\n' >&2 + break + } + + if [[ "${char}" == $'\x7f' ]]; then + if [[ "${#password}" -gt 0 ]]; then + password="${password%?}" + echo -en '\b \b' >&2 + fi + else + password+=$char + echo -en '*' >&2 + fi + done + + echo -en "\e[0m" >&2 + echo -n "${password}" +} + +editor() { + tmpfile=$(mktemp) + _prompt_text "$1" + echo "" >&2 + "${EDITOR:-vi}" "${tmpfile}" >/dev/tty + echo -en "\033[36m" >&2 + sed -e 's/^/ /' "${tmpfile}" >&2 + echo -en "\033[0m" >&2 + cat "${tmpfile}" +} + +with_validate() { + while true; do + declare val + val="$(eval "$1")" + + if ($2 "$val" >/dev/null); then + echo "$val" + break + else + show_error "$($2 "$val")" + fi + done +} + +range() { + declare min="$2" + declare current="$3" + declare max="$4" + declare selected="${current}" + declare max_len_current + max_len_current=0 + + if [[ "${#min}" -gt "${#max}" ]]; then + max_len_current="${#min}" + else + max_len_current="${#max}" + fi + + declare padding + padding="$(printf "%-${max_len_current}s" "")" + declare first_row + first_row=$(_get_cursor_row) + declare current_row + current_row=$((first_row - 1)) + + trap "_cursor_blink_on; stty echo; exit" 2 + + _cursor_blink_off + + _check_range() { + val=$1 + + if [[ "$val" -gt "$max" ]]; then + val=$min + elif [[ "$val" -lt "$min" ]]; then + val=$max + fi + + echo "$val" + } + + while true; do + _prompt_text "$1" + printf "\033[37m%s\033[0m \033[1;90m❮\033[0m \033[36m%s%s\033[0m \033[1;90m❯\033[0m \033[37m%s\033[0m\n" "$min" "${padding:${#selected}}" "$selected" "$max" >&2 + + case $(_key_input) in + enter) + break + ;; + left) + selected="$(_check_range $((selected - 1)))" + ;; + right) + selected="$(_check_range $((selected + 1)))" + ;; + esac + + _cursor_to "$current_row" + done + + echo "$selected" +} + +validate_present() { + if [ "$1" != "" ]; then + return 0 + else + error "Please specify the value" + return 1 + fi +} + +show_error() { + echo -e "\033[91;1m✘ $1\033[0m" >&2 +} + +show_success() { + echo -e "\033[92;1m✔ $1\033[0m" >&2 +} + +detect_os() { + case "$OSTYPE" in + solaris*) echo "solaris" ;; + darwin*) echo "macos" ;; + linux*) echo "linux" ;; + bsd*) echo "bsd" ;; + msys*) echo "windows" ;; + cygwin*) echo "windows" ;; + *) echo "unknown" ;; + esac +} + +get_opener() { + declare cmd + + case "$(detect_os)" in + macos) cmd="open" ;; + linux) cmd="xdg-open" ;; + windows) cmd="start" ;; + *) cmd="" ;; + esac + + echo "$cmd" +} + +open_link() { + cmd="$(get_opener)" + + if [[ "$cmd" == "" ]]; then + error "Your platform is not supported for opening links." + red "Please open the following URL in your preferred browser:" + red " ${1}" + return 1 + fi + + $cmd "$1" + + if [[ $? -eq 1 ]]; then + error "Failed to open your browser." + red "Please open the following URL in your browser:" + red "${1}" + return 1 + fi + + return 0 +} + +guard_operation() { + if [[ -t 1 ]]; then + ans="$(confirm "${1:-Are you sure you want to continue?}")" + + if [[ "$ans" == 0 ]]; then + error "Operation aborted!" 2>&1 + exit 1 + fi + fi +} + +# Here is an example of a patch block that can be applied to modify the file to request the user's name: +# --- a/hello.py +# +++ b/hello.py +# \@@ ... @@ +# def hello(): +# - print("Hello World") +# + name = input("What is your name? ") +# + print(f"Hello {name}") +patch_file() { + awk ' + FNR == NR { + lines[FNR] = $0 + next; + } + + { + patchLines[FNR] = $0 + } + + END { + totalPatchLines=length(patchLines) + totalLines = length(lines) + patchLineIndex = 1 + + mode = "none" + + while (patchLineIndex <= totalPatchLines) { + line = patchLines[patchLineIndex] + + if (line ~ /^--- / || line ~ /^\+\+\+ /) { + patchLineIndex++ + continue + } + + if (line ~ /^@@ /) { + mode = "hunk" + hunkIndex++ + patchLineIndex++ + continue + } + + if (mode == "hunk") { + while (patchLineIndex <= totalPatchLines && line ~ /^[-+ ]|^\s*$/ && line !~ /^--- /) { + sanitizedLine = substr(line, 2) + + if (line !~ /^\+/) { + hunkTotalOriginalLines[hunkIndex]++; + hunkOriginalLines[hunkIndex,hunkTotalOriginalLines[hunkIndex]] = sanitizedLine + } + + if (line !~ /^-/) { + hunkTotalUpdatedLines[hunkIndex]++; + hunkUpdatedLines[hunkIndex,hunkTotalUpdatedLines[hunkIndex]] = sanitizedLine + } + + patchLineIndex++ + line = patchLines[patchLineIndex] + } + + mode = "none" + } else { + patchLineIndex++ + } + } + + if (hunkIndex == 0) { + print "error: no patch" > "/dev/stderr" + exit 1 + } + + totalHunks = hunkIndex + hunkIndex = 1 + + for (lineIndex = 1; lineIndex <= totalLines; lineIndex++) { + line = lines[lineIndex] + nextLineIndex = 0 + + if (hunkIndex <= totalHunks && line == hunkOriginalLines[hunkIndex,1]) { + nextLineIndex = lineIndex + 1 + + for (i = 2; i <= hunkTotalOriginalLines[hunkIndex]; i++) { + if (lines[nextLineIndex] != hunkOriginalLines[hunkIndex,i]) { + nextLineIndex = 0 + break + } + + nextLineIndex++ + } + } + + if (nextLineIndex > 0) { + for (i = 1; i <= hunkTotalUpdatedLines[hunkIndex]; i++) { + print hunkUpdatedLines[hunkIndex,i] + } + + hunkIndex++ + lineIndex = nextLineIndex - 1; + } else { + print line + } + } + + if (hunkIndex != totalHunks + 1) { + print "error: unable to apply patch" > "/dev/stderr" + exit 1 + } + } + + function inspectHunks() { + print "/* Begin inspecting hunks" + + for (i = 1; i <= totalHunks; i++) { + print ">>>>>> Original" + + for (j = 1; j <= hunkTotalOriginalLines[i]; j++) { + print hunkOriginalLines[i,j] + } + + print "======" + + for (j = 1; j <= hunkTotalUpdatedLines[i]; j++) { + print hunkUpdatedLines[i,j] + } + + print "<<<<<< Updated" + } + + print "End inspecting hunks */\n" + }' "$1" "$2" +} + +guard_path() { + if [[ "$#" -ne 2 ]]; then + echo "Usage: guard_path " >&2 + exit 1 + fi + + if [[ -t 1 ]]; then + path="$(_to_real_path "$1")" + confirmation_prompt="$2" + + if [[ ! "$path" == "$(pwd)"* ]]; then + ans="$(confirm "$confirmation_prompt")" + + if [[ "$ans" == 0 ]]; then + error "Operation aborted!" >&2 + exit 1 + fi + fi + fi +} + +_to_real_path() { + path="$1" + + if [[ $OS == "Windows_NT" ]]; then + path="$(cygpath -u "$path")" + fi + + awk -v path="$path" -v pwd="$PWD" ' + BEGIN { + if (path !~ /^\//) { + path = pwd "/" path + } + + if (path ~ /\/\.{1,2}?$/) { + isDir = 1 + } + + split(path, parts, "/") + newPartsLength = 0 + + for (i = 1; i <= length(parts); i++) { + part = parts[i] + if (part == "..") { + if (newPartsLength > 0) { + delete newParts[newPartsLength--] + } + } else if (part != "." && part != "") { + newParts[++newPartsLength] = part + } + } + + if (isDir == 1 || newPartsLength == 0) { + newParts[++newPartsLength] = "" + } + + printf "/" + + for (i = 1; i <= newPartsLength; i++) { + newPart = newParts[i] + printf newPart + if (i < newPartsLength) { + printf "/" + } + } + }' +} diff --git a/src/function.rs b/src/function.rs index 0d61fb0..f780b1b 100644 --- a/src/function.rs +++ b/src/function.rs @@ -20,10 +20,11 @@ use std::{ path::{Path, PathBuf}, }; use strum_macros::AsRefStr; +use crate::config::ensure_parent_exists; #[derive(Embed)] #[folder = "assets/functions/"] -struct FunctionAsset; +struct FunctionAssets; #[cfg(windows)] const PATH_SEP: &str = ";"; @@ -122,7 +123,43 @@ pub struct Functions { } impl Functions { + fn install_global_tools() -> Result<()> { + ensure_parent_exists(&Config::global_tools_file())?; + + info!("Installing global built-in functions in {}", Config::functions_dir().display()); + + for file in FunctionAssets::iter() { + debug!("Processing function file: {}", file.as_ref()); + if file.as_ref().starts_with("scripts/") { + debug!("Skipping script file: {}", file.as_ref()); + continue; + } + + let embedded_file = FunctionAssets::get(&file).ok_or_else(|| { + anyhow!( + "Failed to load embedded function file: {}", + file.as_ref() + ) + })?; + let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) }; + let file_path = Config::functions_dir().join(file.as_ref()); + + if file_path.exists() { + debug!("Function file already exists, skipping: {}", file_path.display()); + continue; + } + + ensure_parent_exists(&file_path)?; + info!("Creating function file: {}", file_path.display()); + let mut function_file = File::create(&file_path)?; + function_file.write_all(content.as_bytes())?; + } + + Ok(()) + } + pub fn init() -> Result { + Self::install_global_tools()?; info!( "Initializing global functions from {}", Config::global_tools_file().display() @@ -144,6 +181,7 @@ impl Functions { } pub fn init_agent(name: &str, global_tools: &[String]) -> Result { + Self::install_global_tools()?; let global_tools_declarations = if !global_tools.is_empty() { let enabled_tools = global_tools.join("\n"); info!("Loading global tools for agent: {name}: {enabled_tools}"); @@ -448,7 +486,7 @@ impl Functions { binary_name, binary_script_file.display(), ); - let embedded_file = FunctionAsset::get(&format!( + let embedded_file = FunctionAssets::get(&format!( "scripts/run-{}.{}", binary_type.as_ref().to_lowercase(), language.to_extension() @@ -542,7 +580,7 @@ impl Functions { binary_name, binary_file.display() ); - let embedded_file = FunctionAsset::get(&format!( + let embedded_file = FunctionAssets::get(&format!( "scripts/run-{}.{}", binary_type.as_ref().to_lowercase(), language.to_extension()