Merge pull request 'push-znusyzznlmks' (#3) from push-znusyzznlmks into main
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -29,18 +29,20 @@ Programmeur expérimenté. Connaît parfaitement son projet, ses outils, et son
|
|||||||
|
|
||||||
## Heuristiques de délégation
|
## Heuristiques de délégation
|
||||||
|
|
||||||
Délègue à `qwen3-worker` si la tâche est **atomique** et satisfait TOUS les critères :
|
Délègue à `qwen3-worker` (via MCP `qwen3_task`) si la tâche satisfait TOUS les critères :
|
||||||
- ≤ 3 fichiers concernés
|
- Objectif clair et bien délimité
|
||||||
- Pas de modification d’API publique
|
- Pas de modification d’API publique (traits Rust, interfaces Go exportées)
|
||||||
- Pas de dépendance externe non encore importée
|
- Pas de vision cross-module requise
|
||||||
- Tâches typiques : génération de tests unitaires, reformatage, documentation inline,
|
- Tâches typiques : génération de tests, documentation inline, scaffolding de stubs,
|
||||||
conversion de types simples, scaffolding de stubs
|
reformatage, recherche web, exécution de scripts, création de fichiers
|
||||||
- Pour le bash, éviter les commandes destructrices (voir garde-fous)
|
|
||||||
|
Qwen3 dispose d’outils autonomes (filesystem, shell, web) — inutile de lui passer
|
||||||
|
le contenu des fichiers, il les lit lui-même.
|
||||||
|
|
||||||
Traite toi-même si :
|
Traite toi-même si :
|
||||||
- Refactoring cross-module
|
- Refactoring cross-module
|
||||||
- Conception d’architecture
|
- Conception d’architecture
|
||||||
- Debugging avec contexte multi-fichiers
|
- Debugging avec stacktrace multi-fichiers
|
||||||
- Modification de traits/interfaces publics
|
- Modification de traits/interfaces publics
|
||||||
|
|
||||||
## Garde-fous absolus
|
## Garde-fous absolus
|
||||||
|
|||||||
@@ -1,30 +1,56 @@
|
|||||||
---
|
---
|
||||||
name: qwen3-worker
|
name: qwen3-worker
|
||||||
description: >
|
description: >
|
||||||
Délègue à Qwen3-Coder via LM Studio les tâches atomiques sur 1-3 fichiers :
|
Délègue à Qwen3-Coder via le MCP tool `qwen3_task` les tâches de codage autonomes :
|
||||||
génération de tests unitaires, documentation inline, scaffolding de stubs,
|
génération de tests, documentation, scaffolding, reformatage, recherche web,
|
||||||
reformatage de code. N'utilise PAS pour du refactoring cross-module ou de
|
exécution de scripts. Qwen3 dispose d'outils filesystem, shell et web — il lit et
|
||||||
|
écrit les fichiers lui-même. N'utilise PAS pour du refactoring cross-module ou de
|
||||||
la conception d'architecture.
|
la conception d'architecture.
|
||||||
tools: [Read, Write, Bash]
|
tools: [mcp__qwen3__qwen3_task]
|
||||||
model: inherit
|
model: inherit
|
||||||
---
|
---
|
||||||
|
|
||||||
Tu es un assistant de codage spécialisé exécutant des tâches courtes et précises.
|
Tu es un orchestrateur qui délègue des tâches de codage à Qwen3-Coder via l'outil MCP `qwen3_task`.
|
||||||
|
|
||||||
## Comportement
|
## Comportement
|
||||||
1. Lis les fichiers nécessaires avec l'outil Read (ne reçois pas le contenu en entrée).
|
|
||||||
2. Effectue la transformation demandée.
|
1. Formule une description précise de la tâche.
|
||||||
3. Écris le résultat avec Write.
|
2. Appelle `qwen3_task` avec le champ `task`. Le champ `files` est optionnel — ne l'utilise
|
||||||
4. Exécute le linter approprié (clippy pour Rust, golangci-lint pour Go) et corrige
|
que pour donner un contexte initial ; Qwen3 peut lire les fichiers dont il a besoin lui-même.
|
||||||
les erreurs éventuelles (max 2 tentatives).
|
3. Qwen3 opère de façon autonome : il lit, écrit, exécute des commandes et fait des requêtes
|
||||||
5. Retourne un résumé : fichiers modifiés, changements effectués, résultat du lint.
|
web via ses propres outils (llm-functions). Tu n'as pas à gérer ces étapes.
|
||||||
|
4. Retourne le résultat de `qwen3_task` à l'orchestrateur principal.
|
||||||
|
|
||||||
|
## Outils disponibles pour Qwen3
|
||||||
|
|
||||||
|
Qwen3 dispose des outils suivants (définis dans `.claude/llm-functions/tools.txt`) :
|
||||||
|
|
||||||
|
| Outil | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `fs_cat` | Lit un fichier |
|
||||||
|
| `fs_ls` | Liste un répertoire |
|
||||||
|
| `fs_mkdir` | Crée un répertoire |
|
||||||
|
| `fs_write` | Écrit/crée un fichier |
|
||||||
|
| `fs_patch` | Modifie un fichier par patch |
|
||||||
|
| `execute_command` | Exécute une commande shell |
|
||||||
|
| `fetch_url_via_curl` | Requête HTTP GET |
|
||||||
|
| `web_search_aichat` | Recherche web |
|
||||||
|
| `execute_py_code` | Exécute du code Python |
|
||||||
|
|
||||||
## Format de sortie
|
## Format de sortie
|
||||||
|
|
||||||
|
Qwen3 retourne une réponse en langage naturel. Résume-la ainsi :
|
||||||
|
|
||||||
```
|
```
|
||||||
STATUS: success|partial|failure
|
STATUS: success|partial|failure
|
||||||
FILES_MODIFIED: liste des fichiers
|
FILES_MODIFIED: liste des fichiers (si applicable)
|
||||||
SUMMARY: description des changements
|
SUMMARY: description des changements
|
||||||
LINT: passed|failed (+ détail si failed)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Ne génère pas de fonctions non demandées. Ne modifie pas les signatures publiques.
|
Si Qwen3 retourne une erreur (`ERROR:`), signale-la avec `STATUS: failure`.
|
||||||
|
|
||||||
|
## Contraintes
|
||||||
|
|
||||||
|
- Ne modifie pas les signatures publiques (traits Rust, interfaces Go exportées).
|
||||||
|
- Ne génère pas de fonctions non demandées.
|
||||||
|
- Max 2 re-délégations en cas d'échec partiel ; au-delà, remonte le problème.
|
||||||
|
|||||||
24
.claude/llm-functions/.github/ISSUE_TEMPLATE/add_agent.md
vendored
Normal file
24
.claude/llm-functions/.github/ISSUE_TEMPLATE/add_agent.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Add a New AI Agent
|
||||||
|
about: Propose an idea or submit a new AI agent for inclusion
|
||||||
|
title: '[Agent Request] Add <Agent Name>'
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Your issue may already be reported! Please search for it before creating one. -->
|
||||||
|
|
||||||
|
**Agent Description**
|
||||||
|
<!-- Summarize the purpose and functionality of the proposed agent in a few concise sentences. -->
|
||||||
|
|
||||||
|
**Agent Conversation Starters**
|
||||||
|
<!-- Provide examples of natural language prompts a user may use to interact with the agent. -->
|
||||||
|
|
||||||
|
**Agent Tools**
|
||||||
|
<!-- List and describe the tools (if any) your agent requires or uses. -->
|
||||||
|
<!-- foo: Brief description of tool foo -->
|
||||||
|
<!-- bar: Brief description of tool bar -->
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
<!-- Include details to help reviewers understand the feature request, such as relevant documentation, use cases, or screenshots. -->
|
||||||
28
.claude/llm-functions/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
28
.claude/llm-functions/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Your issue may already be reported! Please search for it before creating one. -->
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
<!-- A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
<!-- Steps to reproduce the behavior, including any relevant code snippets. -->
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
<!-- A clear and concise description of what you expected to happen. -->
|
||||||
|
|
||||||
|
**Screenshots/Logs**
|
||||||
|
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
<!-- Please run `argc version` and paste the output -->
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
<!-- Add any other context about the problem here. -->
|
||||||
22
.claude/llm-functions/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
22
.claude/llm-functions/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Your issue may already be reported! Please search for it before creating one. -->
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
<!-- A clear and concise description of what you want to happen. -->
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
51
.claude/llm-functions/.github/workflows/ci.yaml
vendored
Normal file
51
.claude/llm-functions/.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
all:
|
||||||
|
name: All
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: sigoden/install-binary@v1
|
||||||
|
with:
|
||||||
|
repo: sigoden/argc
|
||||||
|
|
||||||
|
- name: Check versions
|
||||||
|
run: argc version
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Link web-search and code-interpreter
|
||||||
|
run: |
|
||||||
|
argc link-web-search web_search_perplexity.sh
|
||||||
|
argc link-code-interpreter execute_py_code.py
|
||||||
|
|
||||||
|
- name: Run Test
|
||||||
|
run: argc test
|
||||||
|
env:
|
||||||
|
PYTHONIOENCODING: utf-8
|
||||||
18
.claude/llm-functions/.gitignore
vendored
Normal file
18
.claude/llm-functions/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/tmp
|
||||||
|
/tools.txt
|
||||||
|
/agents.txt
|
||||||
|
functions.json
|
||||||
|
/bin
|
||||||
|
/cache
|
||||||
|
/agents/_*
|
||||||
|
/tools/_*
|
||||||
|
/tools/web_search.*
|
||||||
|
/tools/code_interpreter.*
|
||||||
|
/.env
|
||||||
|
__pycache__
|
||||||
|
/.venv
|
||||||
|
node_modules
|
||||||
|
/package.json
|
||||||
|
package-lock.json
|
||||||
|
*.lock
|
||||||
|
/mcp.json
|
||||||
794
.claude/llm-functions/Argcfile.sh
Normal file
794
.claude/llm-functions/Argcfile.sh
Normal file
@@ -0,0 +1,794 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @meta dotenv
|
||||||
|
|
||||||
|
BIN_DIR=bin
|
||||||
|
TMP_DIR="cache/__tmp__"
|
||||||
|
VENV_DIR=".venv"
|
||||||
|
|
||||||
|
LANG_CMDS=( \
|
||||||
|
"sh:bash" \
|
||||||
|
"js:node" \
|
||||||
|
"py:python" \
|
||||||
|
)
|
||||||
|
|
||||||
|
# @cmd Run the tool
|
||||||
|
# @option -C --cwd <dir> Change the current working directory
|
||||||
|
# @alias tool:run
|
||||||
|
# @arg tool![`_choice_tool`] The tool name
|
||||||
|
# @arg json The json data
|
||||||
|
run@tool() {
|
||||||
|
if [[ -z "$argc_json" ]]; then
|
||||||
|
declaration="$(generate-declarations@tool "$argc_tool" | jq -r '.[0]')"
|
||||||
|
if [[ -n "$declaration" ]]; then
|
||||||
|
_ask_json_data "$declaration"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$argc_json" ]]; then
|
||||||
|
_die "error: no JSON data"
|
||||||
|
fi
|
||||||
|
lang="${argc_tool##*.}"
|
||||||
|
cmd="$(_lang_to_cmd "$lang")"
|
||||||
|
run_tool_script="scripts/run-tool.$lang"
|
||||||
|
[[ -n "$argc_cwd" ]] && cd "$argc_cwd"
|
||||||
|
exec "$cmd" "$run_tool_script" "$argc_tool" "$argc_json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Run the agent
|
||||||
|
# @alias agent:run
|
||||||
|
# @option -C --cwd <dir> Change the current working directory
|
||||||
|
# @arg agent![`_choice_agent`] The agent name
|
||||||
|
# @arg action![?`_choice_agent_action`] The agent action
|
||||||
|
# @arg json The json data
|
||||||
|
run@agent() {
|
||||||
|
if [[ -z "$argc_json" ]]; then
|
||||||
|
declaration="$(generate-declarations@agent "$argc_agent" | jq --arg name "$argc_action" '.[] | select(.name == $name)')"
|
||||||
|
if [[ -n "$declaration" ]]; then
|
||||||
|
_ask_json_data "$declaration"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$argc_json" ]]; then
|
||||||
|
_die "error: no JSON data"
|
||||||
|
fi
|
||||||
|
tools_path="$(_get_agent_tools_path "$argc_agent")"
|
||||||
|
lang="${tools_path##*.}"
|
||||||
|
cmd="$(_lang_to_cmd "$lang")"
|
||||||
|
run_agent_script="scripts/run-agent.$lang"
|
||||||
|
[[ -n "$argc_cwd" ]] && cd "$argc_cwd"
|
||||||
|
exec "$cmd" "$run_agent_script" "$argc_agent" "$argc_action" "$argc_json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Build the project
|
||||||
|
build() {
|
||||||
|
if [[ -f tools.txt ]]; then
|
||||||
|
argc build@tool
|
||||||
|
else
|
||||||
|
echo 'Skipped building tools since tools.txt is missing'
|
||||||
|
fi
|
||||||
|
if [[ -f agents.txt ]]; then
|
||||||
|
argc build@agent
|
||||||
|
else
|
||||||
|
echo 'Skipped building agents since agents.txt is missing'
|
||||||
|
fi
|
||||||
|
if [[ -f mcp.json ]]; then
|
||||||
|
argc mcp merge-functions -S
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Build tools
|
||||||
|
# @alias tool:build
|
||||||
|
# @option --names-file=tools.txt Path to a file containing tool filenames, one per line.
|
||||||
|
# This file specifies which tools will be used.
|
||||||
|
# @option --declarations-file=functions.json <FILE> Path to a json file to save function declarations
|
||||||
|
# @arg tools*[`_choice_tool`] The tool filenames
|
||||||
|
build@tool() {
|
||||||
|
if [[ "${#argc_tools[@]}" -gt 0 ]]; then
|
||||||
|
mkdir -p "$TMP_DIR"
|
||||||
|
argc_names_file="$TMP_DIR/tools.txt"
|
||||||
|
printf "%s\n" "${argc_tools[@]}" > "$argc_names_file"
|
||||||
|
elif [[ "$argc_declarations_file" == "functions.json" ]]; then
|
||||||
|
argc clean@tool
|
||||||
|
fi
|
||||||
|
argc build-declarations@tool --names-file "${argc_names_file}" --declarations-file "${argc_declarations_file}"
|
||||||
|
argc build-bin@tool --names-file "${argc_names_file}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Build tools to bin
|
||||||
|
# @alias tool:build-bin
|
||||||
|
# @option --names-file=tools.txt Path to a file containing tool filenames, one per line.
|
||||||
|
# @arg tools*[`_choice_tool`] The tool filenames
|
||||||
|
build-bin@tool() {
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
if [[ "${#argc_tools[@]}" -gt 0 ]]; then
|
||||||
|
names=("${argc_tools[@]}" )
|
||||||
|
elif [[ -f "$argc_names_file" ]]; then
|
||||||
|
names=($(cat "$argc_names_file" | grep -v '^#'))
|
||||||
|
if [[ "${#names[@]}" -gt 0 ]]; then
|
||||||
|
(cd "$BIN_DIR" && rm -rf "${names[@]}")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$names" ]]; then
|
||||||
|
_die "error: no tools provided. '$argc_names_file' is missing. please create it and add some tools."
|
||||||
|
fi
|
||||||
|
not_found_tools=()
|
||||||
|
for name in "${names[@]}"; do
|
||||||
|
basename="${name%.*}"
|
||||||
|
lang="${name##*.}"
|
||||||
|
tool_path="tools/$name"
|
||||||
|
if [[ -f "$tool_path" ]]; then
|
||||||
|
if _is_win; then
|
||||||
|
bin_file="$BIN_DIR/$basename.cmd"
|
||||||
|
_build_win_shim tool $lang > "$bin_file"
|
||||||
|
else
|
||||||
|
bin_file="$BIN_DIR/$basename"
|
||||||
|
if [[ "$lang" == "py" && -d "$VENV_DIR" ]]; then
|
||||||
|
rm -rf "$bin_file"
|
||||||
|
_build_py_shim tool $lang > "$bin_file"
|
||||||
|
chmod +x "$bin_file"
|
||||||
|
else
|
||||||
|
ln -s -f "$PWD/scripts/run-tool.$lang" "$bin_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "Build bin/$basename"
|
||||||
|
else
|
||||||
|
not_found_tools+=("$name")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -n "$not_found_tools" ]]; then
|
||||||
|
_die "error: not found tools: ${not_found_tools[*]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Build tools function declarations file
|
||||||
|
# @alias tool:build-declarations
|
||||||
|
# @option --names-file=tools.txt Path to a file containing tool filenames, one per line.
|
||||||
|
# @option --declarations-file=functions.json <FILE> Path to a json file to save function declarations
|
||||||
|
# @arg tools*[`_choice_tool`] The tool filenames
|
||||||
|
build-declarations@tool() {
|
||||||
|
if [[ "${#argc_tools[@]}" -gt 0 ]]; then
|
||||||
|
names=("${argc_tools[@]}" )
|
||||||
|
elif [[ -f "$argc_names_file" ]]; then
|
||||||
|
names=($(cat "$argc_names_file" | grep -v '^#'))
|
||||||
|
fi
|
||||||
|
if [[ -z "$names" ]]; then
|
||||||
|
_die "error: no tools provided. '$argc_names_file' is missing. please create it and add some tools."
|
||||||
|
fi
|
||||||
|
json_list=()
|
||||||
|
not_found_tools=()
|
||||||
|
build_failed_tools=()
|
||||||
|
for name in "${names[@]}"; do
|
||||||
|
lang="${name##*.}"
|
||||||
|
tool_path="tools/$name"
|
||||||
|
if [[ ! -f "$tool_path" ]]; then
|
||||||
|
not_found_tools+=("$name")
|
||||||
|
continue;
|
||||||
|
fi
|
||||||
|
json_data="$(generate-declarations@tool "$name" | jq -r '.[0]')" || {
|
||||||
|
build_failed_tools+=("$name")
|
||||||
|
}
|
||||||
|
if [[ "$json_data" == "null" ]]; then
|
||||||
|
_die "error: failed to build declarations for tool $name"
|
||||||
|
fi
|
||||||
|
json_list+=("$json_data")
|
||||||
|
done
|
||||||
|
if [[ -n "$not_found_tools" ]]; then
|
||||||
|
_die "error: not found tools: ${not_found_tools[*]}"
|
||||||
|
fi
|
||||||
|
if [[ -n "$build_failed_tools" ]]; then
|
||||||
|
_die "error: invalid tools: ${build_failed_tools[*]}"
|
||||||
|
fi
|
||||||
|
json_data="$(echo "${json_list[@]}" | jq -s '.')"
|
||||||
|
if [[ "$argc_declarations_file" == "-" ]]; then
|
||||||
|
echo "$json_data"
|
||||||
|
else
|
||||||
|
echo "Build $argc_declarations_file"
|
||||||
|
echo "$json_data" > "$argc_declarations_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# @cmd Generate function declaration for the tool
|
||||||
|
# @alias tool:generate-declarations
|
||||||
|
# @arg tool![`_choice_tool`] The tool name
|
||||||
|
generate-declarations@tool() {
|
||||||
|
lang="${1##*.}"
|
||||||
|
cmd="$(_lang_to_cmd "$lang")"
|
||||||
|
"$cmd" "scripts/build-declarations.$lang" "tools/$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Build agents
|
||||||
|
# @alias agent:build
|
||||||
|
# @option --names-file=agents.txt Path to a file containing agent filenames, one per line.
|
||||||
|
# @arg agents*[`_choice_agent`] The agent filenames
|
||||||
|
build@agent() {
|
||||||
|
if [[ "${#argc_agents[@]}" -gt 0 ]]; then
|
||||||
|
mkdir -p "$TMP_DIR"
|
||||||
|
argc_names_file="$TMP_DIR/agents.txt"
|
||||||
|
printf "%s\n" "${argc_agents[@]}" > "$argc_names_file"
|
||||||
|
else
|
||||||
|
argc clean@agent
|
||||||
|
fi
|
||||||
|
argc build-declarations@agent --names-file "${argc_names_file}"
|
||||||
|
argc build-bin@agent --names-file "${argc_names_file}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Build agents to bin
|
||||||
|
# @alias agent:build-bin
|
||||||
|
# @option --names-file=agents.txt Path to a file containing agent dirs, one per line.
|
||||||
|
# @arg agents*[`_choice_agent`] The agent names
|
||||||
|
build-bin@agent() {
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
if [[ "${#argc_agents[@]}" -gt 0 ]]; then
|
||||||
|
names=("${argc_agents[@]}" )
|
||||||
|
elif [[ -f "$argc_names_file" ]]; then
|
||||||
|
names=($(cat "$argc_names_file" | grep -v '^#'))
|
||||||
|
if [[ "${#names[@]}" -gt 0 ]]; then
|
||||||
|
(cd "$BIN_DIR" && rm -rf "${names[@]}")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$names" ]]; then
|
||||||
|
_die "error: no agents provided. '$argc_names_file' is missing. please create it and add some agents."
|
||||||
|
fi
|
||||||
|
not_found_agents=()
|
||||||
|
for name in "${names[@]}"; do
|
||||||
|
agent_dir="agents/$name"
|
||||||
|
found=false
|
||||||
|
for item in "${LANG_CMDS[@]}"; do
|
||||||
|
lang="${item%:*}"
|
||||||
|
agent_tools_path="$agent_dir/tools.$lang"
|
||||||
|
if [[ -f "$agent_tools_path" ]]; then
|
||||||
|
found=true
|
||||||
|
if _is_win; then
|
||||||
|
bin_file="$BIN_DIR/$name.cmd"
|
||||||
|
_build_win_shim agent $lang > "$bin_file"
|
||||||
|
else
|
||||||
|
bin_file="$BIN_DIR/$name"
|
||||||
|
if [[ "$lang" == "py" && -d "$VENV_DIR" ]]; then
|
||||||
|
rm -rf "$bin_file"
|
||||||
|
_build_py_shim tool $lang > "$bin_file"
|
||||||
|
chmod +x "$bin_file"
|
||||||
|
else
|
||||||
|
ln -s -f "$PWD/scripts/run-agent.$lang" "$bin_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "Build bin/$name"
|
||||||
|
tool_names_file="$agent_dir/tools.txt"
|
||||||
|
if [[ -f "$tool_names_file" ]]; then
|
||||||
|
argc build-bin@tool --names-file "${tool_names_file}"
|
||||||
|
fi
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$found" == "false" ]] && [[ ! -d "$agent_dir" ]]; then
|
||||||
|
not_found_agents+=("$name")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -n "$not_found_agents" ]]; then
|
||||||
|
_die "error: not found agents: ${not_found_agents[*]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Build agents function declarations file
|
||||||
|
# @alias agent:build-declarations
|
||||||
|
# @option --names-file=agents.txt Path to a file containing agent dirs, one per line.
|
||||||
|
# @arg agents*[`_choice_agent`] The tool filenames
|
||||||
|
build-declarations@agent() {
|
||||||
|
if [[ "${#argc_agents[@]}" -gt 0 ]]; then
|
||||||
|
names=("${argc_agents[@]}" )
|
||||||
|
elif [[ -f "$argc_names_file" ]]; then
|
||||||
|
names=($(cat "$argc_names_file" | grep -v '^#'))
|
||||||
|
fi
|
||||||
|
if [[ -z "$names" ]]; then
|
||||||
|
_die "error: no agents provided. '$argc_names_file' is missing. please create it and add some agents."
|
||||||
|
fi
|
||||||
|
not_found_agents=()
|
||||||
|
build_failed_agents=()
|
||||||
|
exist_tools="$(ls -1 tools)"
|
||||||
|
for name in "${names[@]}"; do
|
||||||
|
agent_dir="agents/$name"
|
||||||
|
declarations_file="$agent_dir/functions.json"
|
||||||
|
tool_names_file="$agent_dir/tools.txt"
|
||||||
|
found=false
|
||||||
|
if [[ -d "$agent_dir" ]]; then
|
||||||
|
found=true
|
||||||
|
ok=true
|
||||||
|
json_data=""
|
||||||
|
agent_json_data=""
|
||||||
|
tools_json_data=""
|
||||||
|
for item in "${LANG_CMDS[@]}"; do
|
||||||
|
lang="${item%:*}"
|
||||||
|
agent_tools_path="$agent_dir/tools.$lang"
|
||||||
|
if [[ -f "$agent_tools_path" ]]; then
|
||||||
|
agent_json_data="$(generate-declarations@agent "$name")" || {
|
||||||
|
ok=false
|
||||||
|
build_failed_agents+=("$name")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -f "$tool_names_file" ]]; then
|
||||||
|
if grep -q '^web_search\.' "$tool_names_file" && ! grep -q '^web_search\.' <<<"$exist_tools"; then
|
||||||
|
echo "WARNING: no found web_search tool, please run \`argc link-web-search <web-search-tool>\` to set one."
|
||||||
|
fi
|
||||||
|
if grep -q '^code_interpreter\.' "$tool_names_file" && ! grep -q '^code_interpreter\.' <<<"$exist_tools"; then
|
||||||
|
echo "WARNING: no found code_interpreter tool, please run \`argc link-code-interpreter <execute-code-tool>\` to set one."
|
||||||
|
fi
|
||||||
|
tools_json_data="$(argc build-declarations@tool --names-file="$tool_names_file" --declarations-file=-)" || {
|
||||||
|
ok=false
|
||||||
|
build_failed_agents+=("$name")
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
if [[ "$ok" == "true" ]]; then
|
||||||
|
if [[ -n "$agent_json_data" ]] && [[ -n "$tools_json_data" ]]; then
|
||||||
|
json_data="$(echo "[$agent_json_data,$tools_json_data]" | jq 'flatten')"
|
||||||
|
elif [[ -n "$agent_json_data" ]]; then
|
||||||
|
json_data="$agent_json_data"
|
||||||
|
elif [[ -n "$tools_json_data" ]]; then
|
||||||
|
json_data="$tools_json_data"
|
||||||
|
fi
|
||||||
|
if [[ -n "$json_data" ]]; then
|
||||||
|
echo "Build $declarations_file"
|
||||||
|
echo "$json_data" > "$declarations_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ "$found" == "false" ]]; then
|
||||||
|
not_found_agents+=("$name")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -n "$not_found_agents" ]]; then
|
||||||
|
_die "error: not found agents: ${not_found_agents[*]}"
|
||||||
|
fi
|
||||||
|
if [[ -n "$build_failed_agents" ]]; then
|
||||||
|
_die "error: invalid agents: ${build_failed_agents[*]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Generate function declarations for the agent
|
||||||
|
# @alias agent:generate-declarations
|
||||||
|
# @flag --oneline Summary JSON in one line
|
||||||
|
# @arg agent![`_choice_agent`] The agent name
|
||||||
|
generate-declarations@agent() {
|
||||||
|
tools_path="$(_get_agent_tools_path "$1")"
|
||||||
|
if [[ -z "$tools_path" ]]; then
|
||||||
|
_die "error: no found entry file at agents/$1/tools.<lang>"
|
||||||
|
fi
|
||||||
|
lang="${tools_path##*.}"
|
||||||
|
cmd="$(_lang_to_cmd "$lang")"
|
||||||
|
json="$("$cmd" "scripts/build-declarations.$lang" "$tools_path" | jq 'map(. + {agent: true})')"
|
||||||
|
if [[ -n "$argc_oneline" ]]; then
|
||||||
|
echo "$json" | jq -r '.[] | .name + ": " + (.description | split("\n"))[0]'
|
||||||
|
else
|
||||||
|
echo "$json"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Check environment variables, Node/Python dependencies, MCP-Bridge-Server status
|
||||||
|
check() {
|
||||||
|
argc check@tool
|
||||||
|
argc check@agent
|
||||||
|
argc mcp check
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Check dependencies and environment variables for a specific tool
|
||||||
|
# @alias tool:check
|
||||||
|
# @arg tools*[`_choice_tool`] The tool name
|
||||||
|
check@tool() {
|
||||||
|
if [[ "${#argc_tools[@]}" -gt 0 ]]; then
|
||||||
|
tool_names=("${argc_tools[@]}")
|
||||||
|
else
|
||||||
|
tool_names=($(cat tools.txt | grep -v '^#'))
|
||||||
|
fi
|
||||||
|
for name in "${tool_names[@]}"; do
|
||||||
|
tool_path="tools/$name"
|
||||||
|
echo "Check $tool_path"
|
||||||
|
if [[ -f "$tool_path" ]]; then
|
||||||
|
_check_bin "${name%.*}"
|
||||||
|
_check_envs "$tool_path"
|
||||||
|
./scripts/check-deps.sh "$tool_path"
|
||||||
|
else
|
||||||
|
echo "✗ not found tool file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Check dependencies and environment variables for a specific agent
|
||||||
|
# @alias agent:check
|
||||||
|
# @arg agents*[`_choice_agent`] The agent name
|
||||||
|
check@agent() {
|
||||||
|
if [[ "${#argc_agents[@]}" -gt 0 ]]; then
|
||||||
|
agent_names=("${argc_agents[@]}")
|
||||||
|
else
|
||||||
|
agent_names=($(cat agents.txt | grep -v '^#'))
|
||||||
|
fi
|
||||||
|
for name in "${agent_names[@]}"; do
|
||||||
|
agent_dir="agents/$name"
|
||||||
|
echo "Check $agent_dir"
|
||||||
|
if [[ -d "$agent_dir" ]]; then
|
||||||
|
for item in "${LANG_CMDS[@]}"; do
|
||||||
|
lang="${item%:*}"
|
||||||
|
agent_tools_path="$agent_dir/tools.$lang"
|
||||||
|
if [[ -f "$agent_tools_path" ]]; then
|
||||||
|
_check_bin "$name"
|
||||||
|
_check_envs "$agent_tools_path"
|
||||||
|
./scripts/check-deps.sh "$agent_tools_path"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "✗ not found agent dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd List tools which can be put into functions.txt
|
||||||
|
# @alias tool:list
|
||||||
|
# Examples:
|
||||||
|
# argc list-tools > tools.txt
|
||||||
|
list@tool() {
|
||||||
|
_choice_tool
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd List agents which can be put into agents.txt
|
||||||
|
# @alias agent:list
|
||||||
|
# Examples:
|
||||||
|
# argc list-agents > agents.txt
|
||||||
|
list@agent() {
|
||||||
|
_choice_agent
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Test the project
|
||||||
|
test() {
|
||||||
|
test@tool
|
||||||
|
test@agent
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Test tools
|
||||||
|
# @alias tool:test
|
||||||
|
test@tool() {
|
||||||
|
mkdir -p "$TMP_DIR"
|
||||||
|
names_file="$TMP_DIR/tools.txt"
|
||||||
|
declarations_file="$TMP_DIR/functions.json"
|
||||||
|
argc list@tool > "$names_file"
|
||||||
|
argc build@tool --names-file "$names_file" --declarations-file "$declarations_file"
|
||||||
|
test-demo@tool
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Test demo tools
|
||||||
|
# @alias tool:test-demo
|
||||||
|
test-demo@tool() {
|
||||||
|
for item in "${LANG_CMDS[@]}"; do
|
||||||
|
lang="${item%:*}"
|
||||||
|
tool="demo_$lang.$lang"
|
||||||
|
echo "---- Test $tool ---"
|
||||||
|
argc build-bin@tool "$tool"
|
||||||
|
argc run@tool $tool '{
|
||||||
|
"boolean": true,
|
||||||
|
"string": "Hello",
|
||||||
|
"string_enum": "foo",
|
||||||
|
"integer": 123,
|
||||||
|
"number": 3.14,
|
||||||
|
"array": [
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
"string_optional": "OptionalValue",
|
||||||
|
"array_optional": [
|
||||||
|
"x",
|
||||||
|
"y"
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Test agents
|
||||||
|
# @alias agent:test
|
||||||
|
test@agent() {
|
||||||
|
mkdir -p "$TMP_DIR"
|
||||||
|
names_file="$TMP_DIR/agents.txt"
|
||||||
|
argc list@agent > "$names_file"
|
||||||
|
argc build@agent --names-file "$names_file"
|
||||||
|
test-demo@agent
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Test demo agents
|
||||||
|
# @alias agent:test-demo
|
||||||
|
test-demo@agent() {
|
||||||
|
echo "---- Test demo agent ---"
|
||||||
|
args=(demo get_ipinfo '{}')
|
||||||
|
argc run@agent "${args[@]}"
|
||||||
|
for item in "${LANG_CMDS[@]}"; do
|
||||||
|
cmd="${item#*:}"
|
||||||
|
lang="${item%:*}"
|
||||||
|
echo "---- Test agents/demo/tools.$lang ---"
|
||||||
|
if [[ "$cmd" == "sh" ]]; then
|
||||||
|
"$(argc --argc-shell-path)" ./scripts/run-agent.sh "${args[@]}"
|
||||||
|
elif command -v "$cmd" &> /dev/null; then
|
||||||
|
$cmd ./scripts/run-agent.$lang "${args[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Clean the project
|
||||||
|
clean() {
|
||||||
|
clean@tool
|
||||||
|
clean@agent
|
||||||
|
rm -rf "$BIN_DIR/"*
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Clean tools
|
||||||
|
# @alias tool:clean
|
||||||
|
clean@tool() {
|
||||||
|
_choice_tool | sed -E 's/\.([a-z]+)$//' | xargs -I{} rm -rf "$BIN_DIR/{}"
|
||||||
|
rm -rf functions.json
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Clean agents
|
||||||
|
# @alias agent:clean
|
||||||
|
clean@agent() {
|
||||||
|
_choice_agent | xargs -I{} rm -rf "$BIN_DIR/{}"
|
||||||
|
_choice_agent | xargs -I{} rm -rf agents/{}/functions.json
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Link a tool as web_search tool
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# argc link-web-search web_search_perplexity.sh
|
||||||
|
# @arg tool![`_choice_web_search`] The tool work as web_search
|
||||||
|
link-web-search() {
|
||||||
|
_link_tool $1 web_search
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Link a tool as code_interpreter tool
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# argc link-code-interpreter execute_py_code.py
|
||||||
|
# @arg tool![`_choice_code_interpreter`] The tool work as code_interpreter
|
||||||
|
link-code-interpreter() {
|
||||||
|
_link_tool $1 code_interpreter
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Link this repo to aichat functions_dir
|
||||||
|
link-to-aichat() {
|
||||||
|
functions_dir="$(aichat --info | grep -w functions_dir | awk '{$1=""; print substr($0,2)}')"
|
||||||
|
if [[ -z "$functions_dir" ]]; then
|
||||||
|
_die "error: your aichat version don't support function calling"
|
||||||
|
fi
|
||||||
|
if [[ ! -e "$functions_dir" ]]; then
|
||||||
|
if _is_win; then
|
||||||
|
current_dir="$(cygpath -w "$(pwd)")"
|
||||||
|
cmd <<< "mklink /D \"${functions_dir%/}\" \"${current_dir%/}\"" > /dev/null
|
||||||
|
else
|
||||||
|
ln -s "$(pwd)" "$functions_dir"
|
||||||
|
fi
|
||||||
|
echo "$functions_dir symlinked"
|
||||||
|
else
|
||||||
|
echo "$functions_dir already exists"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Run mcp command
|
||||||
|
# @arg args~[?`_choice_mcp_args`] The mcp command and arguments
|
||||||
|
mcp() {
|
||||||
|
bash ./scripts/mcp.sh "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Create a boilplate tool script
|
||||||
|
# @alias tool:create
|
||||||
|
# @arg args~
|
||||||
|
create@tool() {
|
||||||
|
./scripts/create-tool.sh "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Displays version information for required tools
|
||||||
|
version() {
|
||||||
|
uname -a
|
||||||
|
if command -v aichat &> /dev/null; then
|
||||||
|
aichat --version
|
||||||
|
fi
|
||||||
|
argc --argc-version
|
||||||
|
jq --version
|
||||||
|
ls --version 2>&1 | head -n 1
|
||||||
|
for item in "${LANG_CMDS[@]}"; do
|
||||||
|
cmd="${item#*:}"
|
||||||
|
if [[ "$cmd" == "bash" ]]; then
|
||||||
|
echo "$(argc --argc-shell-path) $("$(argc --argc-shell-path)" --version | head -n 1)"
|
||||||
|
elif command -v "$cmd" &> /dev/null; then
|
||||||
|
echo "$(_normalize_path "$(which $cmd)") $($cmd --version)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_lang_to_cmd() {
|
||||||
|
match_lang="$1"
|
||||||
|
for item in "${LANG_CMDS[@]}"; do
|
||||||
|
lang="${item%:*}"
|
||||||
|
if [[ "$lang" == "$match_lang" ]]; then
|
||||||
|
echo "${item#*:}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_get_agent_tools_path() {
|
||||||
|
name="$1"
|
||||||
|
for item in "${LANG_CMDS[@]}"; do
|
||||||
|
lang="${item%:*}"
|
||||||
|
entry_file="agents/$name/tools.$lang"
|
||||||
|
if [[ -f "agents/$name/tools.$lang" ]]; then
|
||||||
|
echo "$entry_file"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_build_win_shim() {
|
||||||
|
kind="$1"
|
||||||
|
lang="$2"
|
||||||
|
cmd="$(_lang_to_cmd "$lang")"
|
||||||
|
if [[ "$lang" == "sh" ]]; then
|
||||||
|
run="\"$(argc --argc-shell-path)\" --noprofile --norc"
|
||||||
|
else
|
||||||
|
if [[ "$cmd" == "python" && -d "$VENV_DIR" ]]; then
|
||||||
|
run="call \"$(_normalize_path "$PWD/$VENV_DIR/Scripts/activate.bat")\" && python"
|
||||||
|
else
|
||||||
|
run="\"$(_normalize_path "$(which $cmd)")\""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
cat <<-EOF
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
set "bin_dir=%~dp0"
|
||||||
|
for %%i in ("%bin_dir:~0,-1%") do set "script_dir=%%~dpi"
|
||||||
|
set "script_name=%~n0"
|
||||||
|
|
||||||
|
$run "%script_dir%scripts\run-$kind.$lang" "%script_name%" %*
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
_build_py_shim() {
|
||||||
|
kind="$1"
|
||||||
|
lang="$2"
|
||||||
|
cat <<-'EOF' | sed -e "s|__ROOT_DIR__|$PWD|g" -e "s|__VENV_DIR__|$VENV_DIR|g" -e "s/__KIND__/$kind/g"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ -f "__ROOT_DIR__/__VENV_DIR__/bin/activate" ]]; then
|
||||||
|
source "__ROOT_DIR__/__VENV_DIR__/bin/activate"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python "__ROOT_DIR__/scripts/run-__KIND__.py" "$(basename "$0")" "$@"
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
_check_bin() {
|
||||||
|
bin_name="$1"
|
||||||
|
if _is_win; then
|
||||||
|
bin_name+=".cmd"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$BIN_DIR/$bin_name" ]]; then
|
||||||
|
echo "✗ missing bin/$bin_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_check_envs() {
|
||||||
|
script_path="$1"
|
||||||
|
envs=( $(sed -E -n 's/.* @env ([A-Z0-9_]+)!.*/\1/p' $script_path) )
|
||||||
|
missing_envs=()
|
||||||
|
for env in $envs; do
|
||||||
|
if [[ -z "${!env}" ]]; then
|
||||||
|
missing_envs+=("$env")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -n "$missing_envs" ]]; then
|
||||||
|
echo "✗ missing envs ${missing_envs[*]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_link_tool() {
|
||||||
|
from="$1"
|
||||||
|
to="$2.${1##*.}"
|
||||||
|
rm -rf tools/$to
|
||||||
|
if _is_win; then
|
||||||
|
(cd tools && cp -f $from $to)
|
||||||
|
else
|
||||||
|
(cd tools && ln -s $from $to)
|
||||||
|
fi
|
||||||
|
(cd tools && ls -l $to)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ask_json_data() {
|
||||||
|
declaration="$1"
|
||||||
|
echo 'Missing the JSON data but here are its properties:'
|
||||||
|
echo "$declaration" | ./scripts/declarations-util.sh pretty-print | sed -n '2,$s/^/>/p'
|
||||||
|
echo 'Generate placeholder data:'
|
||||||
|
data="$(echo "$declaration" | _declarations_json_data)"
|
||||||
|
echo "> $data"
|
||||||
|
read -e -r -p 'JSON data (Press ENTER to use placeholder): ' res
|
||||||
|
if [[ -z "$res" ]]; then
|
||||||
|
argc_json="$data"
|
||||||
|
else
|
||||||
|
argc_json="$res"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_declarations_json_data() {
|
||||||
|
./scripts/declarations-util.sh generate-json | tail -n +2
|
||||||
|
}
|
||||||
|
|
||||||
|
_normalize_path() {
|
||||||
|
if _is_win; then
|
||||||
|
cygpath -w "$1"
|
||||||
|
else
|
||||||
|
echo "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_is_win() {
|
||||||
|
if [[ "$OS" == "Windows_NT" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_argc_before() {
|
||||||
|
if [[ -d ".venv/bin/activate" ]]; then
|
||||||
|
source .venv/bin/activate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_choice_tool() {
|
||||||
|
for item in "${LANG_CMDS[@]}"; do
|
||||||
|
lang="${item%:*}"
|
||||||
|
cmd="${item#*:}"
|
||||||
|
if command -v "$cmd" &> /dev/null; then
|
||||||
|
ls -1 tools | grep "\.$lang$"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_choice_web_search() {
|
||||||
|
_choice_tool | grep '^web_search_'
|
||||||
|
}
|
||||||
|
|
||||||
|
_choice_code_interpreter() {
|
||||||
|
_choice_tool | grep '^execute_.*_code'
|
||||||
|
}
|
||||||
|
|
||||||
|
_choice_agent() {
|
||||||
|
ls -1 agents
|
||||||
|
}
|
||||||
|
|
||||||
|
_choice_agent_action() {
|
||||||
|
if [[ "$ARGC_COMPGEN" -eq 1 ]]; then
|
||||||
|
expr="s/: /\t/"
|
||||||
|
else
|
||||||
|
expr="s/:.*//"
|
||||||
|
fi
|
||||||
|
argc generate-declarations@agent "$1" --oneline | sed "$expr"
|
||||||
|
}
|
||||||
|
|
||||||
|
_choice_mcp_args() {
|
||||||
|
if [[ "$ARGC_COMPGEN" -eq 1 ]]; then
|
||||||
|
args=( "${argc__positionals[@]}" )
|
||||||
|
args[-1]="$ARGC_LAST_ARG"
|
||||||
|
argc --argc-compgen generic scripts/mcp.sh mcp "${args[@]}"
|
||||||
|
else
|
||||||
|
:;
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_die() {
|
||||||
|
echo "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if _is_win; then set -o igncr; fi
|
||||||
|
|
||||||
|
# See more details at https://github.com/sigoden/argc
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
21
.claude/llm-functions/LICENSE
Normal file
21
.claude/llm-functions/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) sigoden
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
215
.claude/llm-functions/README.md
Normal file
215
.claude/llm-functions/README.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# LLM Functions
|
||||||
|
|
||||||
|
This project empowers you to effortlessly build powerful LLM tools and agents using familiar languages like Bash, JavaScript, and Python.
|
||||||
|
|
||||||
|
Forget complex integrations, **harness the power of [function calling](https://platform.openai.com/docs/guides/function-calling)** to connect your LLMs directly to custom code and unlock a world of possibilities. Execute system commands, process data, interact with APIs – the only limit is your imagination.
|
||||||
|
|
||||||
|
**Tools Showcase**
|
||||||
|

|
||||||
|
|
||||||
|
**Agents showcase**
|
||||||
|

|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Make sure you have the following tools installed:
|
||||||
|
|
||||||
|
- [argc](https://github.com/sigoden/argc): A bash command-line framework and command runner
|
||||||
|
- [jq](https://github.com/jqlang/jq): A JSON processor
|
||||||
|
|
||||||
|
## Getting Started with [AIChat](https://github.com/sigoden/aichat)
|
||||||
|
|
||||||
|
**Currently, AIChat is the only CLI tool that supports `llm-functions`. We look forward to more tools supporting `llm-functions`.**
|
||||||
|
|
||||||
|
### 1. Clone the repository
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/sigoden/llm-functions
|
||||||
|
cd llm-functions
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build tools and agents
|
||||||
|
|
||||||
|
#### I. Create a `./tools.txt` file with each tool filename on a new line.
|
||||||
|
|
||||||
|
```
|
||||||
|
get_current_weather.sh
|
||||||
|
execute_command.sh
|
||||||
|
#execute_py_code.py
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Where is the web_search tool?</summary>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
The `web_search` tool itself doesn't exist directly, Instead, you can choose from a variety of web search tools.
|
||||||
|
|
||||||
|
To use one as the `web_search` tool, follow these steps:
|
||||||
|
|
||||||
|
1. **Choose a Tool:** Available tools include:
|
||||||
|
* `web_search_cohere.sh`
|
||||||
|
* `web_search_perplexity.sh`
|
||||||
|
* `web_search_tavily.sh`
|
||||||
|
* `web_search_vertexai.sh`
|
||||||
|
|
||||||
|
2. **Link Your Choice:** Use the `argc` command to link your chosen tool as `web_search`. For example, to use `web_search_perplexity.sh`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ argc link-web-search web_search_perplexity.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This command creates a symbolic link, making `web_search.sh` point to your selected `web_search_perplexity.sh` tool.
|
||||||
|
|
||||||
|
Now there is a `web_search.sh` ready to be added to your `./tools.txt`.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
#### II. Create a `./agents.txt` file with each agent name on a new line.
|
||||||
|
|
||||||
|
```
|
||||||
|
coder
|
||||||
|
todo
|
||||||
|
```
|
||||||
|
|
||||||
|
#### III. Build `bin` and `functions.json`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
argc build
|
||||||
|
```
|
||||||
|
|
||||||
|
#### IV. Ensure that everything is ready (environment variables, Node/Python dependencies, mcp-bridge server)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
argc check
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Link LLM-functions and AIChat
|
||||||
|
|
||||||
|
AIChat expects LLM-functions to be placed in AIChat's **functions_dir** so that AIChat can use the tools and agents that LLM-functions provides.
|
||||||
|
|
||||||
|
You can symlink this repository directory to AIChat's **functions_dir** with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ln -s "$(pwd)" "$(aichat --info | sed -n 's/^functions_dir\s\+//p')"
|
||||||
|
# OR
|
||||||
|
argc link-to-aichat
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can tell AIChat where the LLM-functions directory is by using an environment variable:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export AICHAT_FUNCTIONS_DIR="$(pwd)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Start using the functions
|
||||||
|
|
||||||
|
Done! Now you can use the tools and agents with AIChat.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
aichat --role %functions% what is the weather in Paris?
|
||||||
|
aichat --agent todo list all my todos
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Your Own Tools
|
||||||
|
|
||||||
|
Building tools for our platform is remarkably straightforward. You can leverage your existing programming knowledge, as tools are essentially just functions written in your preferred language.
|
||||||
|
|
||||||
|
LLM Functions automatically generates the JSON declarations for the tools based on **comments**. Refer to `./tools/demo_tool.{sh,js,py}` for examples of how to use comments for autogeneration of declarations.
|
||||||
|
|
||||||
|
### Bash
|
||||||
|
|
||||||
|
Create a new bashscript in the [./tools/](./tools/) directory (.e.g. `execute_command.sh`).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Execute the shell command.
|
||||||
|
# @option --command! The command to execute.
|
||||||
|
|
||||||
|
main() {
|
||||||
|
eval "$argc_command" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Javascript
|
||||||
|
|
||||||
|
Create a new javascript in the [./tools/](./tools/) directory (.e.g. `execute_js_code.js`).
|
||||||
|
|
||||||
|
```js
|
||||||
|
/**
|
||||||
|
* Execute the javascript code in node.js.
|
||||||
|
* @typedef {Object} Args
|
||||||
|
* @property {string} code - Javascript code to execute, such as `console.log("hello world")`
|
||||||
|
* @param {Args} args
|
||||||
|
*/
|
||||||
|
exports.run = function ({ code }) {
|
||||||
|
eval(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
Create a new python script in the [./tools/](./tools/) directory (e.g. `execute_py_code.py`).
|
||||||
|
|
||||||
|
```py
|
||||||
|
def run(code: str):
|
||||||
|
"""Execute the python code.
|
||||||
|
Args:
|
||||||
|
code: Python code to execute, such as `print("hello world")`
|
||||||
|
"""
|
||||||
|
exec(code)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Your Own Agents
|
||||||
|
|
||||||
|
Agent = Prompt + Tools (Function Calling) + Documents (RAG), which is equivalent to OpenAI's GPTs.
|
||||||
|
|
||||||
|
The agent has the following folder structure:
|
||||||
|
```
|
||||||
|
└── agents
|
||||||
|
└── myagent
|
||||||
|
├── functions.json # JSON declarations for functions (Auto-generated)
|
||||||
|
├── index.yaml # Agent definition
|
||||||
|
├── tools.txt # Shared tools
|
||||||
|
└── tools.{sh,js,py} # Agent tools
|
||||||
|
```
|
||||||
|
|
||||||
|
The agent definition file (`index.yaml`) defines crucial aspects of your agent:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: TestAgent
|
||||||
|
description: This is test agent
|
||||||
|
version: 0.1.0
|
||||||
|
instructions: You are a test ai agent to ...
|
||||||
|
conversation_starters:
|
||||||
|
- What can you do?
|
||||||
|
variables:
|
||||||
|
- name: foo
|
||||||
|
description: This is a foo
|
||||||
|
documents:
|
||||||
|
- local-file.txt
|
||||||
|
- local-dir/
|
||||||
|
- https://example.com/remote-file.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to [./agents/demo](https://github.com/sigoden/llm-functions/tree/main/agents/demo) for examples of how to implement a agent.
|
||||||
|
|
||||||
|
## MCP (Model Context Protocol)
|
||||||
|
|
||||||
|
- [mcp/server](https://github.com/sigoden/llm-functions/tree/main/mcp/server): Let LLM-Functions tools/agents be used through the Model Context Protocol.
|
||||||
|
- [mcp/bridge](https://github.com/sigoden/llm-functions/tree/main/mcp/bridge): Let external MCP tools be used by LLM-Functions.
|
||||||
|
|
||||||
|
## Documents
|
||||||
|
|
||||||
|
- [Tool Guide](https://github.com/sigoden/llm-functions/blob/main/docs/tool.md)
|
||||||
|
- [Agent Guide](https://github.com/sigoden/llm-functions/blob/main/docs/agent.md)
|
||||||
|
- [Argc Commands](https://github.com/sigoden/llm-functions/blob/main/docs/argcfile.md)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The project is under the MIT License, Refer to the [LICENSE](https://github.com/sigoden/llm-functions/blob/main/LICENSE) file for detailed information.
|
||||||
22
.claude/llm-functions/agents/coder/README.md
Normal file
22
.claude/llm-functions/agents/coder/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Coder
|
||||||
|
|
||||||
|
An AI agent that assists your coding tasks.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🏗️ Intelligent project structure creation and management
|
||||||
|
- 🖼️ Convert screenshots into clean, functional code
|
||||||
|
- 📁 Comprehensive file system operations (create folders, files, read/write files)
|
||||||
|
- 🧐 Advanced code analysis and improvement suggestions
|
||||||
|
- 📊 Precise diff-based file editing for controlled code modifications
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/9363990f-15a9-48c6-b227-8900cfbe0a18
|
||||||
|
|
||||||
|
## Similar Projects
|
||||||
|
|
||||||
|
- https://github.com/Doriandarko/claude-engineer
|
||||||
|
- https://github.com/paul-gauthier/aider
|
||||||
47
.claude/llm-functions/agents/coder/index.yaml
Normal file
47
.claude/llm-functions/agents/coder/index.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Coder
|
||||||
|
description: An AI agent that assists your coding tasks
|
||||||
|
version: 0.1.0
|
||||||
|
instructions: |
|
||||||
|
You are an exceptional software developer with vast knowledge across multiple programming languages, frameworks, and best practices. Your capabilities include:
|
||||||
|
|
||||||
|
1. Creating and managing project structures
|
||||||
|
2. Writing, debugging, and improving code across multiple languages
|
||||||
|
3. Providing architectural insights and applying design patterns
|
||||||
|
4. Staying current with the latest technologies and best practices
|
||||||
|
5. Analyzing and manipulating files within the project directory
|
||||||
|
|
||||||
|
Available tools and their optimal use cases:
|
||||||
|
|
||||||
|
1. fs_mkdir: Create new directories in the project structure.
|
||||||
|
2. fs_create: Generate new files with specified contents.
|
||||||
|
3. fs_patch: Examine and modify existing files.
|
||||||
|
4. fs_cat: View the contents of existing files without making changes.
|
||||||
|
5. fs_ls: Understand the current project structure or locate specific files.
|
||||||
|
|
||||||
|
Tool Usage Guidelines:
|
||||||
|
- Always use the most appropriate tool for the task at hand.
|
||||||
|
- For file modifications, use fs_patch. Read the file first, then apply changes if needed.
|
||||||
|
- After making changes, always review the diff output to ensure accuracy.
|
||||||
|
|
||||||
|
Project Creation and Management:
|
||||||
|
1. Start by creating a root folder for new projects.
|
||||||
|
2. Create necessary subdirectories and files within the root folder.
|
||||||
|
3. Organize the project structure logically, following best practices for the specific project type.
|
||||||
|
|
||||||
|
Code Editing Best Practices:
|
||||||
|
1. Always read the file content before making changes.
|
||||||
|
2. Analyze the code and determine necessary modifications.
|
||||||
|
3. Pay close attention to existing code structure to avoid unintended alterations.
|
||||||
|
4. Review changes thoroughly after each modification.
|
||||||
|
|
||||||
|
Always strive for accuracy, clarity, and efficiency in your responses and actions.
|
||||||
|
|
||||||
|
Answer the user's request using relevant tools (if they are available). Before calling a tool, do some analysis within <thinking></thinking> tags. First, think about which of the provided tools is the relevant tool to answer the user's request. Second, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool call. BUT, if one of the values for a required parameter is missing, DO NOT invoke the function (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters. DO NOT ask for more information on optional parameters if it is not provided.
|
||||||
|
|
||||||
|
Do not reflect on the quality of the returned search results in your response.
|
||||||
|
|
||||||
|
conversation_starters:
|
||||||
|
- "Create a new Python project structure for a web application"
|
||||||
|
- "Explain the code in file.py and suggest improvements"
|
||||||
|
- "Search for the latest best practices in React development"
|
||||||
|
- "Help me debug this error: [paste your error message]"
|
||||||
19
.claude/llm-functions/agents/coder/tools.sh
Executable file
19
.claude/llm-functions/agents/coder/tools.sh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
|
||||||
|
# @cmd Create a new file at the specified path with contents.
|
||||||
|
# @option --path! The path where the file should be created
|
||||||
|
# @option --contents! The contents of the file
|
||||||
|
fs_create() {
|
||||||
|
"$ROOT_DIR/utils/guard_path.sh" "$argc_path" "Create '$argc_path'?"
|
||||||
|
mkdir -p "$(dirname "$argc_path")"
|
||||||
|
printf "%s" "$argc_contents" > "$argc_path"
|
||||||
|
echo "File created: $argc_path" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# See more details at https://github.com/sigoden/argc
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
4
.claude/llm-functions/agents/coder/tools.txt
Normal file
4
.claude/llm-functions/agents/coder/tools.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
fs_mkdir.sh
|
||||||
|
fs_ls.sh
|
||||||
|
fs_patch.sh
|
||||||
|
fs_cat.sh
|
||||||
3
.claude/llm-functions/agents/demo/README.md
Normal file
3
.claude/llm-functions/agents/demo/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Demo
|
||||||
|
|
||||||
|
This agent serves as a demo to guide agent development and showcase various agent capabilities.
|
||||||
35
.claude/llm-functions/agents/demo/index.yaml
Normal file
35
.claude/llm-functions/agents/demo/index.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Demo
|
||||||
|
description: An AI agent that demonstrates agent capabilities
|
||||||
|
version: 0.1.0
|
||||||
|
instructions: |
|
||||||
|
You are a AI agent designed to demonstrate agent capabilities.
|
||||||
|
|
||||||
|
<tools>
|
||||||
|
{{__tools__}}
|
||||||
|
</tools>
|
||||||
|
|
||||||
|
<system>
|
||||||
|
os: {{__os__}}
|
||||||
|
os_family: {{__os_family__}}
|
||||||
|
arch: {{__arch__}}
|
||||||
|
shell: {{__shell__}}
|
||||||
|
locale: {{__locale__}}
|
||||||
|
now: {{__now__}}
|
||||||
|
cwd: {{__cwd__}}
|
||||||
|
</system>
|
||||||
|
|
||||||
|
<user>
|
||||||
|
username: {{username}}
|
||||||
|
</user>
|
||||||
|
variables:
|
||||||
|
- name: username
|
||||||
|
description: Your user name
|
||||||
|
conversation_starters:
|
||||||
|
- What is my username?
|
||||||
|
- What is my current shell?
|
||||||
|
- What is my ip?
|
||||||
|
- How much disk space is left on my PC??
|
||||||
|
- How to create an agent?
|
||||||
|
documents:
|
||||||
|
- README.md
|
||||||
|
- https://github.com/sigoden/llm-functions/blob/main/README.md
|
||||||
7
.claude/llm-functions/agents/demo/tools.js
Normal file
7
.claude/llm-functions/agents/demo/tools.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Get the system info
|
||||||
|
*/
|
||||||
|
exports.get_ipinfo = async function () {
|
||||||
|
const res = await fetch("https://httpbin.org/ip")
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
9
.claude/llm-functions/agents/demo/tools.py
Normal file
9
.claude/llm-functions/agents/demo/tools.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import urllib.request
|
||||||
|
|
||||||
|
def get_ipinfo():
|
||||||
|
"""
|
||||||
|
Get the ip info
|
||||||
|
"""
|
||||||
|
with urllib.request.urlopen("https://httpbin.org/ip") as response:
|
||||||
|
data = response.read()
|
||||||
|
return data.decode('utf-8')
|
||||||
12
.claude/llm-functions/agents/demo/tools.sh
Executable file
12
.claude/llm-functions/agents/demo/tools.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
# @cmd Get the ip info
|
||||||
|
get_ipinfo() {
|
||||||
|
curl -fsSL https://httpbin.org/ip >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# See more details at https://github.com/sigoden/argc
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
1
.claude/llm-functions/agents/demo/tools.txt
Normal file
1
.claude/llm-functions/agents/demo/tools.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
execute_command.sh
|
||||||
10
.claude/llm-functions/agents/json-viewer/README.md
Normal file
10
.claude/llm-functions/agents/json-viewer/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Json-Viewer
|
||||||
|
|
||||||
|
An AI agent to view and filter json data
|
||||||
|
|
||||||
|
The agent only sends the JSON schema instead of the JSON data to the LLM, which has the following advantages:
|
||||||
|
|
||||||
|
- Less data transmission, faster response speed, and lower token costs.
|
||||||
|
- More privacy, as no actual JSON data is transmitted.
|
||||||
|
|
||||||
|

|
||||||
5
.claude/llm-functions/agents/json-viewer/index.yaml
Normal file
5
.claude/llm-functions/agents/json-viewer/index.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
name: Json-Viewer
|
||||||
|
description: An AI agent to view and filter json data
|
||||||
|
version: 0.1.0
|
||||||
|
instructions: ""
|
||||||
|
dynamic_instructions: true
|
||||||
6
.claude/llm-functions/agents/json-viewer/package.json
Normal file
6
.claude/llm-functions/agents/json-viewer/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/input": "^4.0.2",
|
||||||
|
"to-json-schema": "^0.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
61
.claude/llm-functions/agents/json-viewer/tools.js
Normal file
61
.claude/llm-functions/agents/json-viewer/tools.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const fs = require("node:fs/promises");
|
||||||
|
const { exec, spawn } = require("node:child_process");
|
||||||
|
const { promisify } = require("node:util");
|
||||||
|
const path = require("node:path");
|
||||||
|
const { tmpdir } = require("node:os");
|
||||||
|
|
||||||
|
const toJsonSchema = require('to-json-schema');
|
||||||
|
const input = require("@inquirer/input").default;
|
||||||
|
|
||||||
|
exports._instructions = async function () {
|
||||||
|
const value = await input({ message: "Enter the json file path or command to generate json", required: true });
|
||||||
|
let json_file_path;
|
||||||
|
let generate_json_command_context = "";
|
||||||
|
try {
|
||||||
|
await fs.access(value);
|
||||||
|
json_file_path = value;
|
||||||
|
} catch {
|
||||||
|
generate_json_command_context = `command_to_generate_json: \`${value}\`\n`;
|
||||||
|
const { stdout } = await promisify(exec)(value, { maxBuffer: 100 * 1024 * 1024 });
|
||||||
|
json_file_path = path.join(tmpdir(), `${process.env.LLM_AGENT_NAME}-${process.pid}.data.json`);
|
||||||
|
await fs.writeFile(json_file_path, stdout);
|
||||||
|
console.log(`ⓘ Generated json data saved to: ${json_file_path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json_data = await fs.readFile(json_file_path, "utf8");
|
||||||
|
const json_schema = toJsonSchema(JSON.parse(json_data));
|
||||||
|
|
||||||
|
return `You are a AI agent that can view and filter json data with jq.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
${generate_json_command_context}json_file_path: ${json_file_path}
|
||||||
|
json_schema: ${JSON.stringify(json_schema, null, 2)}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the json data.
|
||||||
|
*
|
||||||
|
* @typedef {Object} Args
|
||||||
|
* @property {string} json_file_path The json file path
|
||||||
|
* @property {string} jq_expr The jq expression
|
||||||
|
* @param {Args} args
|
||||||
|
*/
|
||||||
|
exports.print_json = async function (args) {
|
||||||
|
const { json_file_path, jq_expr } = args;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn("jq", ["-r", jq_expr, json_file_path], { stdio: "inherit" });
|
||||||
|
|
||||||
|
child.on('close', code => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`jq exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
7
.claude/llm-functions/agents/sql/README.md
Normal file
7
.claude/llm-functions/agents/sql/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# SQL
|
||||||
|
|
||||||
|
An AI agent that helps you manage a SQL database.
|
||||||
|
|
||||||
|
> The tool script uses [usql](https://github.com/xo/usql) to interact with SQL, it supports all mainstream databases.
|
||||||
|
|
||||||
|

|
||||||
13
.claude/llm-functions/agents/sql/index.yaml
Normal file
13
.claude/llm-functions/agents/sql/index.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: Sql
|
||||||
|
description: An AI agent that helps you manage a SQL database
|
||||||
|
version: 0.1.0
|
||||||
|
instructions: |
|
||||||
|
You are an AI agent that manages a SQL database.
|
||||||
|
|
||||||
|
Available tools:
|
||||||
|
{{__tools__}}
|
||||||
|
variables:
|
||||||
|
- name: dsn
|
||||||
|
description: The database connection url. e.g. pgsql://user:pass@host:port
|
||||||
|
conversation_starters:
|
||||||
|
- What you can do?
|
||||||
43
.claude/llm-functions/agents/sql/tools.sh
Executable file
43
.claude/llm-functions/agents/sql/tools.sh
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
|
||||||
|
# @meta require-tools usql
|
||||||
|
# @env LLM_AGENT_VAR_DSN! The database connection url. e.g. pgsql://user:pass@host:port
|
||||||
|
|
||||||
|
# @cmd Execute a SELECT query
|
||||||
|
# @option --query! SELECT SQL query to execute
|
||||||
|
read_query() {
|
||||||
|
if ! grep -qi '^select' <<<"$argc_query"; then
|
||||||
|
echo "error: only SELECT query is allowed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
_run_sql "$argc_query"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Execute an SQL query
|
||||||
|
# @option --query! SQL query to execute
|
||||||
|
write_query() {
|
||||||
|
"$ROOT_DIR/utils/guard_operation.sh" "Execute SQL?"
|
||||||
|
_run_sql "$argc_query"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd List all tables
|
||||||
|
list_tables() {
|
||||||
|
_run_sql "\dt+"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Get the schema information for a specific table
|
||||||
|
# @option --table-name! Name of the table to describe
|
||||||
|
describe_table() {
|
||||||
|
_run_sql "\d $argc_table_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
_run_sql() {
|
||||||
|
usql "$LLM_AGENT_VAR_DSN" -c "$1" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# See more details at https://github.com/sigoden/argc
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
5
.claude/llm-functions/agents/todo/README.md
Normal file
5
.claude/llm-functions/agents/todo/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Todo
|
||||||
|
|
||||||
|
An AI agent that helps you manage a todo list.
|
||||||
|
|
||||||
|

|
||||||
19
.claude/llm-functions/agents/todo/index.yaml
Normal file
19
.claude/llm-functions/agents/todo/index.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Todo
|
||||||
|
description: An AI agent that helps you manage a todo list
|
||||||
|
version: 0.1.0
|
||||||
|
instructions: |
|
||||||
|
You are AI agent that manage a todo list.
|
||||||
|
|
||||||
|
Available tools:
|
||||||
|
{{__tools__}}
|
||||||
|
|
||||||
|
When outputting the todo list to the user, don't simply print JSON data; instead, output it in Markdown format.
|
||||||
|
`{"id": 1, "desc": "Buy milk", "done": true }` => `1. [x] Buy milk`
|
||||||
|
`{"id": 2, "desc": "Buy eggs", "done": false}` => `2. [ ] Buy eggs`
|
||||||
|
|
||||||
|
conversation_starters:
|
||||||
|
- "List all todos"
|
||||||
|
- "Clean the entire todo list"
|
||||||
|
- "Add a new todo: Buy milk"
|
||||||
|
- "Done todo id=1"
|
||||||
|
- "Delete todo id=1"
|
||||||
85
.claude/llm-functions/agents/todo/tools.sh
Executable file
85
.claude/llm-functions/agents/todo/tools.sh
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
|
||||||
|
# @cmd Add a new todo item
|
||||||
|
# @option --desc! The todo description
|
||||||
|
add_todo() {
|
||||||
|
todos_file="$(_get_todos_file)"
|
||||||
|
if [[ -f "$todos_file" ]]; then
|
||||||
|
data="$(cat "$todos_file")"
|
||||||
|
num="$(echo "$data" | jq '[.[].id] | max + 1')"
|
||||||
|
else
|
||||||
|
num=1
|
||||||
|
data="[]"
|
||||||
|
fi
|
||||||
|
echo "$data" | \
|
||||||
|
jq --arg new_id $num --arg new_desc "$argc_desc" \
|
||||||
|
'. += [{"id": $new_id | tonumber, "desc": $new_desc, "done": false}]' \
|
||||||
|
> "$todos_file"
|
||||||
|
echo "Successfully added todo id=$num" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Delete an todo item
|
||||||
|
# @option --id! <INT> The todo id
|
||||||
|
del_todo() {
|
||||||
|
todos_file="$(_get_todos_file)"
|
||||||
|
if [[ -f "$todos_file" ]]; then
|
||||||
|
data="$(cat "$todos_file")"
|
||||||
|
echo "$data" | \
|
||||||
|
jq '[.[] | select(.id != '$argc_id')]' \
|
||||||
|
> "$todos_file"
|
||||||
|
echo "Successfully deleted todo id=$argc_id" >> "$LLM_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "The operation failed because the todo list is currently empty." >> "$LLM_OUTPUT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Set a todo item status as done
|
||||||
|
# @option --id! <INT> The todo id
|
||||||
|
done_todo() {
|
||||||
|
todos_file="$(_get_todos_file)"
|
||||||
|
if [[ -f "$todos_file" ]]; then
|
||||||
|
data="$(cat "$todos_file")"
|
||||||
|
echo "$data" | \
|
||||||
|
jq '. |= map(if .id == '$argc_id' then .done = true else . end)' \
|
||||||
|
> "$todos_file"
|
||||||
|
echo "Successfully mark todo id=$argc_id as done" >> "$LLM_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "The operation failed because the todo list is currently empty." >> "$LLM_OUTPUT"
|
||||||
|
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" >> "$LLM_OUTPUT"
|
||||||
|
else
|
||||||
|
echo '[]' >> "$LLM_OUTPUT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Clean the entire todo list
|
||||||
|
clear_todos() {
|
||||||
|
todos_file="$(_get_todos_file)"
|
||||||
|
if [[ -f "$todos_file" ]]; then
|
||||||
|
"$ROOT_DIR/utils/guard_operation.sh" "Clean the entire todo list?"
|
||||||
|
rm -rf "$todos_file"
|
||||||
|
echo "Successfully cleaned the entire todo list" >> "$LLM_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "The operation failed because the todo list is currently empty." >> "$LLM_OUTPUT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_get_todos_file() {
|
||||||
|
todos_dir="${LLM_AGENT_CACHE_DIR:-.}"
|
||||||
|
mkdir -p "$todos_dir"
|
||||||
|
echo "$todos_dir/todos.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# See more details at https://github.com/sigoden/argc
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
108
.claude/llm-functions/docs/agent.md
Normal file
108
.claude/llm-functions/docs/agent.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Agent
|
||||||
|
|
||||||
|
## folder structure
|
||||||
|
|
||||||
|
The agent follows a specific organizational structure to ensure streamlined functionality and easy access to essential files:
|
||||||
|
```
|
||||||
|
└── agents
|
||||||
|
└── myagent
|
||||||
|
├── functions.json # Auto-generated JSON declarations for functions
|
||||||
|
├── index.yaml # Main agent definition file
|
||||||
|
├── tools.txt # List of shared tools
|
||||||
|
└── tools.{sh,js,py} # Scripts implementing agent-specific tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## index.yaml
|
||||||
|
|
||||||
|
This is the main definition file for your agent where you provide all essential information and configuration for the agent.
|
||||||
|
|
||||||
|
### metadata
|
||||||
|
|
||||||
|
Metadata provides basic information about the agent:
|
||||||
|
|
||||||
|
- `name`: A unique name for your agent, which helps in identifying and referencing the agent.
|
||||||
|
- `description`: A brief explanation of what the agent is or its primary purpose.
|
||||||
|
- `version`: The version number of the agent, which helps track changes or updates to the agent over time.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: TestAgent
|
||||||
|
description: This is test agent
|
||||||
|
version: 0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### instructions
|
||||||
|
|
||||||
|
Defines the initial context or behavior directives for the agent:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
instructions: You are a test ai agent to ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### variables
|
||||||
|
|
||||||
|
Variables store user-related data, such as behavior or preferences. Below is the syntax for defining variables:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
variables:
|
||||||
|
- name: foo
|
||||||
|
description: This is a foo
|
||||||
|
- name: bar
|
||||||
|
description: This is a bar with default value
|
||||||
|
default: val
|
||||||
|
```
|
||||||
|
> For sensitive information such as api_key, client_id, client_secret, and token, it's recommended to use environment variables instead of agent variables.
|
||||||
|
|
||||||
|
When use define variables, please avoid these built-in variables:
|
||||||
|
|
||||||
|
| name | description | example |
|
||||||
|
| :-------------- | :-------------------------------------------- | :----------------------- |
|
||||||
|
| `__os__` | Operating system name | linux |
|
||||||
|
| `__os_family__` | Operating system family | unix |
|
||||||
|
| `__arch__` | System architecture | x86_64 |
|
||||||
|
| `__shell__` | Current user's default shell | bash |
|
||||||
|
| `__locale__` | User's preferred language and region settings | en-US |
|
||||||
|
| `__now__` | Current timestamp in ISO 8601 format | 2024-07-29T08:11:24.367Z |
|
||||||
|
| `__cwd__` | Current working directory | /tmp |
|
||||||
|
| `__tools__` | List of agent tools | |
|
||||||
|
|
||||||
|
Variables can be used within `instructions` and within tool scripts:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
instructions: |
|
||||||
|
The instructions can access user-defined variables: {{foo}} and {{bar}}, or built-in variables: {{__cwd__}}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo "he tools script can access user-defined variables in environment variables: $LLM_AGENT_VAR_FOO and $LLM_AGENT_VAR_BAR"
|
||||||
|
```
|
||||||
|
|
||||||
|
### documents
|
||||||
|
|
||||||
|
A list of resources or references that the agent can access. Documents are used for building RAG.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
documents:
|
||||||
|
- local-file.txt
|
||||||
|
- local-dir/
|
||||||
|
- https://example.com/remote-file.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
> All local files and directories are relative to the agent directory (where index.yaml is located).
|
||||||
|
|
||||||
|
### conversation_starters
|
||||||
|
|
||||||
|
Define Predefined prompts or questions that users can ask to initiate interactions or conversations with the agent.
|
||||||
|
This helps provide guidance for users on how to engage with the agent effectively.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
conversation_starters:
|
||||||
|
- What can you do?
|
||||||
|
```
|
||||||
|
|
||||||
|
## tools.{sh,js,py}
|
||||||
|
|
||||||
|
Scripts for implementing tools tailored to the agent's unique requirements.
|
||||||
|
|
||||||
|
## tools.txt
|
||||||
|
|
||||||
|
`tools.txt` facilitates the reuse of tools specified in the `/tools` directory within this project.
|
||||||
93
.claude/llm-functions/docs/argcfile.md
Normal file
93
.claude/llm-functions/docs/argcfile.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Argcfile
|
||||||
|
|
||||||
|
The [Argcfile.sh](https://github.com/sigoden/llm-functions/blob/main/Argcfile.sh) is a powerful Bash script designed to streamline the process of managing LLM functions and agents in your AIChat environment.
|
||||||
|
|
||||||
|
We encourage running `Argcfile.sh` using `argc`. Because `argc` provides better autocompletion, it can also be used without trouble on Windows.
|
||||||
|
|
||||||
|
Argcfile.sh is to argc what Makefile is to make.
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/1acef548-4735-49c1-8f60-c4e0baf528de
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# -------- Help --------
|
||||||
|
argc -h # Print help information
|
||||||
|
argc <command> -h # Print help information for <command>
|
||||||
|
|
||||||
|
# -------- Build --------
|
||||||
|
# Build all
|
||||||
|
argc build
|
||||||
|
|
||||||
|
# Build all tools
|
||||||
|
argc build@tool
|
||||||
|
# Build specific tools
|
||||||
|
argc build@tool get_current_weather.sh execute_command.sh
|
||||||
|
|
||||||
|
# Build all agents
|
||||||
|
argc build@agent
|
||||||
|
# Build specific agents
|
||||||
|
argc build@agent coder todo
|
||||||
|
|
||||||
|
# -------- Check --------
|
||||||
|
# Check all
|
||||||
|
argc check
|
||||||
|
|
||||||
|
# Check all tools
|
||||||
|
argc check@tool
|
||||||
|
# Check specific tools
|
||||||
|
argc check@tool get_current_weather.sh execute_command.sh
|
||||||
|
|
||||||
|
# Check all agents
|
||||||
|
argc check@agent
|
||||||
|
# Check specific agents
|
||||||
|
argc check@agent coder todo
|
||||||
|
|
||||||
|
# -------- Run --------
|
||||||
|
# Run tool
|
||||||
|
argc run@tool get_current_weather.sh '{"location":"London"}'
|
||||||
|
# Run agent tool
|
||||||
|
argc run@agent todo add_todo '{"desc":"Watch a movie"}'
|
||||||
|
|
||||||
|
# -------- Test --------
|
||||||
|
# Test all
|
||||||
|
argc test
|
||||||
|
# Test tools
|
||||||
|
argc test@tool
|
||||||
|
# Test agents
|
||||||
|
argc test@agent
|
||||||
|
|
||||||
|
# -------- Clean --------
|
||||||
|
# Clean all
|
||||||
|
argc clean
|
||||||
|
# Clean tools
|
||||||
|
argc clean@tool
|
||||||
|
# Clean agents
|
||||||
|
argc clean@agent
|
||||||
|
|
||||||
|
# -------- Link --------
|
||||||
|
argc link-web-search web_search_tavily.sh
|
||||||
|
argc link-code-interpreter execute_py_code.py
|
||||||
|
|
||||||
|
# -------- Misc --------
|
||||||
|
# Link this repo to aichat functions_dir
|
||||||
|
argc link-to-aichat
|
||||||
|
# Displays version information for required tools
|
||||||
|
argc version
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Start/restart the mcp bridge server
|
||||||
|
argc mcp start
|
||||||
|
|
||||||
|
# Stop the mcp bridge server
|
||||||
|
argc mcp stop
|
||||||
|
|
||||||
|
# Run the mcp tool
|
||||||
|
argc mcp run@tool fs_read_file '{"path":"/tmp/file1"}'
|
||||||
|
|
||||||
|
# Show the logs
|
||||||
|
argc mcp logs
|
||||||
|
```
|
||||||
30
.claude/llm-functions/docs/environment-variables.md
Normal file
30
.claude/llm-functions/docs/environment-variables.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
## Injected by `run-tool.*`/`run-agent.*`
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `LLM_ROOT_DIR` | Path to `<llm-functions-dir>` |
|
||||||
|
| `LLM_TOOL_NAME` | Tool name, such as `execute_command` |
|
||||||
|
| `LLM_TOOL_CACHE_DIR` | Path to `<llm-functions-dir>/cache/<tool-name>`,<br>The tool script can use this directory to store some cache data |
|
||||||
|
| `LLM_AGENT_NAME` | Agent name, such as `todo` |
|
||||||
|
| `LLM_AGENT_FUNC` | Agent function, such as `list_todos` |
|
||||||
|
| `LLM_AGENT_ROOT_DIR` | Path to `<llm-functions-dir>/agents/<agent-name>` |
|
||||||
|
| `LLM_AGENT_CACHE_DIR` | Path to `<llm-functions-dir>/cache/<agent-name>`,<br>The agent tool script can use this directory to store some cache data |
|
||||||
|
|
||||||
|
## Injected by runtime (AIChat)
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| ---------------------- | ---------------------------------------------------- |
|
||||||
|
| `LLM_OUTPUT` | File to store the the execution results of the tool. |
|
||||||
|
| `LLM_AGENT_VAR_<NAME>` | Agent variables. |
|
||||||
|
|
||||||
|
## Provided by users
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `LLM_DUMP_RESULTS` | Controls whether to print the execution results of the tool, e.g. `get_current_weather\|fs.*\|todo:.*`, `.*` |
|
||||||
|
| `LLM_MCP_NEED_CONFIRM`| Controls whether to prompt for confirmation before executing certain tools, e.g., `git_commit\|git_reset`, `.*` . |
|
||||||
|
| `LLM_MCP_SKIP_CONFIRM`| Controls whether to bypass confirmation requests for certain tools, e.g., `git_status\|git_diff.*`, `.*` . |
|
||||||
|
|
||||||
|
> LLM-Functions supports `.env`, just put environment variables into dotenv file to make it work.
|
||||||
328
.claude/llm-functions/docs/tool.md
Normal file
328
.claude/llm-functions/docs/tool.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# Tool
|
||||||
|
|
||||||
|
This document guides you on creating custom tools for the LLM Functions framework in Bash, JavaScript, and Python.
|
||||||
|
|
||||||
|
## Defining Tool Parameters
|
||||||
|
|
||||||
|
To define the parameters that your tool accepts, you will use specially formatted comments within your tool's source code.
|
||||||
|
The `Argcfile.sh` script utilizes these comments to automatically generate the function declarations needed by the LLM.
|
||||||
|
|
||||||
|
### Json Schema
|
||||||
|
|
||||||
|
The following JSON schema includes various types of properties. We will use this as an example to see how to write comments in each language so they can be automatically generated.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "demo",
|
||||||
|
"description": "Demonstrate how to create a tool using Javascript and how to use comments.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"string": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Define a required string property"
|
||||||
|
},
|
||||||
|
"string_enum": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"description": "Define a required string property with enum"
|
||||||
|
},
|
||||||
|
"string_optional": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Define a optional string property"
|
||||||
|
},
|
||||||
|
"boolean": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Define a required boolean property"
|
||||||
|
},
|
||||||
|
"integer": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Define a required integer property"
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Define a required number property"
|
||||||
|
},
|
||||||
|
"array": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Define a required string array property"
|
||||||
|
},
|
||||||
|
"array_optional": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Define a optional string array property"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"string",
|
||||||
|
"string_enum",
|
||||||
|
"boolean",
|
||||||
|
"integer",
|
||||||
|
"number",
|
||||||
|
"array"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bash
|
||||||
|
|
||||||
|
Use `# @describe`, `# @option`, and `# @flag` comments to define your tool's parameters.
|
||||||
|
|
||||||
|
* `# @describe <description>`: A brief description of your tool's functionality. This is required.
|
||||||
|
|
||||||
|
* `# @option --<option-name>[!<type>][<constraints>] <description>`: Defines an option.
|
||||||
|
* `--<option-name>`: The name of the option (use kebab-case).
|
||||||
|
* `!`: Indicates a required option.
|
||||||
|
* `<type>`: The data type (e.g., `INT`, `NUM`, `<enum>`). If omitted, defaults to `STRING`.
|
||||||
|
* `<constraints>`: Any constraints (e.g., `[foo|bar]` for an enum).
|
||||||
|
* `<description>`: A description of the option.
|
||||||
|
|
||||||
|
* `# @flag --<flag-name> <description>`: Defines a boolean flag.
|
||||||
|
* `--<flag-name>`: The name of the flag (use kebab-case).
|
||||||
|
* `<description>`: A description of the flag.
|
||||||
|
|
||||||
|
**Example ([tools/demo_sh.sh](https://github.com/sigoden/llm-functions/blob/main/tools/demo_sh.sh)):**
|
||||||
|
|
||||||
|
```sh file=tools/demo_sh.sh
|
||||||
|
#!/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! <INT> Define a required integer property
|
||||||
|
# @option --number! <NUM> Define a required number property
|
||||||
|
# @option --array+ <VALUE> Define a required string array property
|
||||||
|
# @option --array-optional* Define a optional string array property
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# ... your bash code ...
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
Use JSDoc-style comments to define your tool's parameters. The `@typedef` block defines the argument object, and each property within that object represents a parameter.
|
||||||
|
|
||||||
|
* `/** ... */`: JSDoc comment block containing the description and parameter definitions.
|
||||||
|
* `@typedef {Object} Args`: Defines the type of the argument object.
|
||||||
|
* `@property {<type>} <name> <description>`: Defines a property (parameter) of the `Args` object.
|
||||||
|
* `<type>`: The data type (e.g., `string`, `boolean`, `number`, `string[]`, `{foo|bar}`).
|
||||||
|
* `<name>`: The name of the parameter.
|
||||||
|
* `<description>`: A description of the parameter.
|
||||||
|
* `[]`: Indicates an optional parameter.
|
||||||
|
|
||||||
|
**Example ([tools/demo_js.js](https://github.com/sigoden/llm-functions/blob/main/tools/demo_js.js)):**
|
||||||
|
|
||||||
|
```js file=tools/demo_js.js
|
||||||
|
/**
|
||||||
|
* Demonstrate how to create a tool using Javascript and how to use comments.
|
||||||
|
* @typedef {Object} Args
|
||||||
|
* @property {string} string - Define a required string property
|
||||||
|
* @property {'foo'|'bar'} string_enum - Define a required string property with enum
|
||||||
|
* @property {string} [string_optional] - Define a optional string property
|
||||||
|
* @property {boolean} boolean - Define a required boolean property
|
||||||
|
* @property {Integer} integer - Define a required integer property
|
||||||
|
* @property {number} number - Define a required number property
|
||||||
|
* @property {string[]} array - Define a required string array property
|
||||||
|
* @property {string[]} [array_optional] - Define a optional string array property
|
||||||
|
* @param {Args} args
|
||||||
|
*/
|
||||||
|
exports.run = function (args) {
|
||||||
|
// ... your JavaScript code ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Of course, you can also use ESM `export` expressions to export functions.
|
||||||
|
```js
|
||||||
|
export function run() {
|
||||||
|
// ... your JavaScript code ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
Use type hints and docstrings to define your tool's parameters.
|
||||||
|
|
||||||
|
* `def run(...)`: Function definition.
|
||||||
|
* `<type> <parameter_name>: <description>`: Type hints with descriptions in the docstring.
|
||||||
|
* `<type>`: The data type (e.g., `str`, `bool`, `int`, `float`, `List[str]`, `Literal["foo", "bar"]`).
|
||||||
|
* `<parameter_name>`: The name of the parameter.
|
||||||
|
* `<description>`: Description of the parameter.
|
||||||
|
* `Optional[...]`: Indicates an optional parameter.
|
||||||
|
|
||||||
|
**Example ([tools/demo_py.py](https://github.com/sigoden/llm-functions/blob/main/tools/demo_py.py)):**
|
||||||
|
|
||||||
|
```py file=tools/demo_py.py
|
||||||
|
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,
|
||||||
|
):
|
||||||
|
"""Demonstrate 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 a optional string property
|
||||||
|
array_optional: Define a optional string array property
|
||||||
|
"""
|
||||||
|
# ... your Python code ...
|
||||||
|
```
|
||||||
|
## Common tools
|
||||||
|
|
||||||
|
Common tools can be found in `tools/<tool-name>.{sh,js,py}`. Each script defines a single tool.
|
||||||
|
|
||||||
|
## Agent tools
|
||||||
|
|
||||||
|
Agents can possess their own toolset scripts located under `agents/<agent-name>/tools.{sh,js,py}`, which can contain multiple tool functions.
|
||||||
|
|
||||||
|
The following is an example of git agent:
|
||||||
|
|
||||||
|
### Bash
|
||||||
|
|
||||||
|
```sh file=agents/git/tools.sh
|
||||||
|
# @cmd Shows the working tree status
|
||||||
|
git_status() {
|
||||||
|
# ... your bash code ...
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Shows differences between branches or commits
|
||||||
|
# @option --target! Shows differences between branches or commits
|
||||||
|
git_diff() {
|
||||||
|
# ... your bash code ...
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
|
```
|
||||||
|
|
||||||
|
> In `tools/<tool-name>.sh`, we use the `@describe` comment tag and a single `main` function, since it has only one function and no subcommands.
|
||||||
|
> In `agent/<agent-name>/tools.sh`, we use the `@cmd` comment tag and named functions, since it can have multiple tool functions.
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```js file=agents/git/tools.js
|
||||||
|
/**
|
||||||
|
* Shows the working tree status
|
||||||
|
*/
|
||||||
|
exports.git_status = function() {
|
||||||
|
// ... your JavaScript code ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows differences between branches or commits
|
||||||
|
* @typedef {Object} Args
|
||||||
|
* @property {string} target - Shows differences between branches or commits
|
||||||
|
* @param {Args} args
|
||||||
|
*/
|
||||||
|
exports.git_diff = function() {
|
||||||
|
// ... your JavaScript code ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```py file=agents/git/tools.py
|
||||||
|
def git_status():
|
||||||
|
"""Shows the working tree status"""
|
||||||
|
# ... your Python code ...
|
||||||
|
|
||||||
|
|
||||||
|
def git_diff(target: str):
|
||||||
|
"""Shows differences between branches or commits
|
||||||
|
Args:
|
||||||
|
target: Shows differences between branches or commits
|
||||||
|
"""
|
||||||
|
# ... your Python code ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quickly Create Tools
|
||||||
|
|
||||||
|
### Use argc
|
||||||
|
|
||||||
|
`Argcfile.sh` provides a tool `create@tool` to quickly create tool scripts.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
argc create@tool _test.sh foo bar! baz+ qux*
|
||||||
|
```
|
||||||
|
|
||||||
|
The argument details
|
||||||
|
|
||||||
|
- `_test.sh`: The name of the tool script you want to create. The file extension can only be `.sh`, `.js`, or `.py`.
|
||||||
|
- `foo bar! baz+ qux*`: The parameters for the tool.
|
||||||
|
|
||||||
|
The suffixes attached to the tool's parameters define their characteristics:
|
||||||
|
|
||||||
|
- `!`: Indicates that the property is required.
|
||||||
|
- `*`: Specifies that the property value should be an array.
|
||||||
|
- `+`: Marks the property as required, with the value also needing to be an array.
|
||||||
|
- No suffix: Denotes that the property is optional.
|
||||||
|
|
||||||
|
### Use aichat
|
||||||
|
|
||||||
|
AI is smart enough to automatically create tool scripts for us. We just need to provide the documentation and describe the requirements well.
|
||||||
|
|
||||||
|
Use aichat to create a common tool script:
|
||||||
|
```
|
||||||
|
aichat -f docs/tool.md <<-'EOF'
|
||||||
|
create tools/get_youtube_transcript.py
|
||||||
|
|
||||||
|
description: Extract transcripts from YouTube videos
|
||||||
|
parameters:
|
||||||
|
url (required): YouTube video URL or video ID
|
||||||
|
lang (default: "en"): Language code for transcript (e.g., "ko", "en")
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Use aichat to create a agent tools script:
|
||||||
|
```
|
||||||
|
aichat -f docs/agent.md -f docs/tool.md <<-'EOF'
|
||||||
|
|
||||||
|
create a spotify agent
|
||||||
|
|
||||||
|
index.yaml:
|
||||||
|
name: spotify
|
||||||
|
description: An AI agent that works with Spotify
|
||||||
|
|
||||||
|
tools.py:
|
||||||
|
search: Search for tracks, albums, artists, or playlists on Spotify
|
||||||
|
query (required): Query term
|
||||||
|
qtype (default: "track"): Type of items to search for (track, album, artist, playlist, or comma-separated combination)
|
||||||
|
limit (default: 10): Maximum number of items to return
|
||||||
|
get_info: Get detailed information about a Spotify item (track, album, artist, or playlist)
|
||||||
|
item_id (required): ID of the item to get information about
|
||||||
|
qtype (default: "track"): Type of item: 'track', 'album', 'artist', or 'playlist'
|
||||||
|
get_queue: Get the playback queue
|
||||||
|
add_queue: Add tracks to the playback queue
|
||||||
|
track_id (required): Track ID to add to queue
|
||||||
|
get_track: Get information about user's current track
|
||||||
|
start: Starts of resumes playback
|
||||||
|
track_id (required): Specifies track to play
|
||||||
|
pause: Pauses current playback
|
||||||
|
skip: Skips current track
|
||||||
|
num_skips (default: 1): Number of tracks to skip
|
||||||
|
EOF
|
||||||
|
```
|
||||||
55
.claude/llm-functions/mcp/bridge/README.md
Normal file
55
.claude/llm-functions/mcp/bridge/README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# MCP-Bridge
|
||||||
|
|
||||||
|
Let external MCP tools be used by LLM-Functions.
|
||||||
|
|
||||||
|
## Get Started
|
||||||
|
|
||||||
|
### 1. Create a `mcp.json` at `<llm-functions-dir>`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"sqlite": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": [
|
||||||
|
"mcp-server-sqlite",
|
||||||
|
"--db-path",
|
||||||
|
"/tmp/foo.db"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": [
|
||||||
|
"mcp-server-git",
|
||||||
|
"--repository",
|
||||||
|
"path/to/git/repo"
|
||||||
|
],
|
||||||
|
"prefix": false
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/server-github"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> MCP-Bridge will launch the server and register all the tools listed by the server.
|
||||||
|
|
||||||
|
> To avoid name clashes, The server automatically prefix tool names with `<server>_`. You can disable this behavior by add `prefix: false` to server configuration.
|
||||||
|
|
||||||
|
### 2. Run the bridge server, build mcp tool binaries, update functions.json, all with:
|
||||||
|
|
||||||
|
```
|
||||||
|
argc mcp start
|
||||||
|
```
|
||||||
|
|
||||||
|
> Run `argc mcp stop` to stop the bridge server, recover functions.json.
|
||||||
|
|
||||||
|
> Run `argc mcp logs` to check the server's logs.
|
||||||
198
.claude/llm-functions/mcp/bridge/index.js
Normal file
198
.claude/llm-functions/mcp/bridge/index.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||||
|
import express from "express";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.MCP_BRIDGE_PORT || 8808;
|
||||||
|
|
||||||
|
let [rootDir] = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (!rootDir) {
|
||||||
|
console.error("Usage: mcp-bridge <llm-functions-dir>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mcpServers = {};
|
||||||
|
const mcpJsonPath = path.join(rootDir, "mcp.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.promises.readFile(mcpJsonPath, "utf8");
|
||||||
|
mcpServers = JSON.parse(data)?.mcpServers;
|
||||||
|
} catch {
|
||||||
|
console.error(`Failed to read json at '${mcpJsonPath}'`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startMcpServer(id, serverConfig) {
|
||||||
|
console.log(`Starting ${id} server...`);
|
||||||
|
const capabilities = { tools: {} };
|
||||||
|
const { prefix = true, ...rest } = serverConfig;
|
||||||
|
const transport = new StdioClientTransport({
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
const client = new Client(
|
||||||
|
{ name: id, version: "1.0.0" },
|
||||||
|
{ capabilities }
|
||||||
|
);
|
||||||
|
await client.connect(transport);
|
||||||
|
const { tools: toolDefinitions } = await client.listTools()
|
||||||
|
const tools = toolDefinitions.map(
|
||||||
|
({ name, description, inputSchema }) =>
|
||||||
|
({
|
||||||
|
spec: {
|
||||||
|
name: `${formatToolName(id, name, prefix)}`,
|
||||||
|
description,
|
||||||
|
parameters: inputSchema,
|
||||||
|
mcp: id,
|
||||||
|
},
|
||||||
|
impl: async args => {
|
||||||
|
const res = await client.callTool({
|
||||||
|
name: name,
|
||||||
|
arguments: args,
|
||||||
|
});
|
||||||
|
const content = res.content;
|
||||||
|
let text = arrayify(content)?.map(c => {
|
||||||
|
switch (c.type) {
|
||||||
|
case "text":
|
||||||
|
return c.text || ""
|
||||||
|
case "image":
|
||||||
|
return c.data
|
||||||
|
case "resource":
|
||||||
|
return c.resource?.uri || ""
|
||||||
|
default:
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}).join("\n");
|
||||||
|
if (res.isError) {
|
||||||
|
text = `Tool Error\n${text}`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
[Symbol.asyncDispose]: async () => {
|
||||||
|
try {
|
||||||
|
console.log(`Closing ${id} server...`);
|
||||||
|
await client.close();
|
||||||
|
await transport.close();
|
||||||
|
} catch { }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBridge() {
|
||||||
|
let hasError = false;
|
||||||
|
let runningMcpServers = await Promise.all(
|
||||||
|
Object.entries(mcpServers).map(
|
||||||
|
async ([name, serverConfig]) => {
|
||||||
|
try {
|
||||||
|
return await startMcpServer(name, serverConfig)
|
||||||
|
} catch (err) {
|
||||||
|
hasError = true;
|
||||||
|
console.error(`Failed to start ${name} server; ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
runningMcpServers = runningMcpServers.filter(s => !!s);
|
||||||
|
const stopMcpServers = () => Promise.all(runningMcpServers.map(s => s[Symbol.asyncDispose]()));
|
||||||
|
if (hasError) {
|
||||||
|
await stopMcpServers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const definitions = runningMcpServers.flatMap(s => s.tools.map(t => t.spec));
|
||||||
|
const runTool = async (name, args) => {
|
||||||
|
for (const server of runningMcpServers) {
|
||||||
|
const tool = server.tools.find(t => t.spec.name === name);
|
||||||
|
if (tool) {
|
||||||
|
return tool.impl(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `Not found tool '${name}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use((err, _req, res, _next) => {
|
||||||
|
res.status(500).send(err?.message || err);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.get("/", (_req, res) => {
|
||||||
|
res.send(`# MCP Bridge API
|
||||||
|
|
||||||
|
- POST /tools/:name
|
||||||
|
\`\`\`
|
||||||
|
curl -X POST http://localhost:8808/tools/filesystem_write_file \\
|
||||||
|
-H 'content-type: application/json' \\
|
||||||
|
-d '{"path": "/tmp/file1", "content": "hello world"}'
|
||||||
|
\`\`\`
|
||||||
|
- GET /tools
|
||||||
|
\`\`\`
|
||||||
|
curl http://localhost:8808/tools
|
||||||
|
\`\`\`
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/tools", (_req, res) => {
|
||||||
|
res.json(definitions);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/tools/:name", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const output = await runTool(req.params.name, req.body);
|
||||||
|
res.send(output);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/pid", (_req, res) => {
|
||||||
|
res.send(process.pid.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/health", (_req, res) => {
|
||||||
|
res.send("OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use((_req, res, _next) => {
|
||||||
|
res.status(404).send("Not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = app.listen(PORT, () => {
|
||||||
|
console.log(`Server is running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
server.close(() => console.log("Http server closed"));
|
||||||
|
await stopMcpServers();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayify(a) {
|
||||||
|
let r;
|
||||||
|
if (a === undefined) r = [];
|
||||||
|
else if (Array.isArray(a)) r = a.slice(0);
|
||||||
|
else r = [a];
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatToolName(serverName, toolName, prefix) {
|
||||||
|
const name = prefix ? `${serverName}_${toolName}` : toolName;
|
||||||
|
return name.toLowerCase().replace(/-/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
runBridge()
|
||||||
|
.then(stop => {
|
||||||
|
if (stop) {
|
||||||
|
process.on('SIGINT', stop);
|
||||||
|
process.on('SIGTERM', stop);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
22
.claude/llm-functions/mcp/bridge/package.json
Normal file
22
.claude/llm-functions/mcp/bridge/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "mcp-bridge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Let MCP tools be used by LLM functions",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "sigoden <sigoden@gmail.com>",
|
||||||
|
"homepage": "https://github.com/sigoden/llm-functions/tree/main/mcp/bridge",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/sigoden/llm-functions.git",
|
||||||
|
"directory": "mcp/bridge"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"mcp-bridge": "index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.3",
|
||||||
|
"express": "^4.21.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
40
.claude/llm-functions/mcp/server/README.md
Normal file
40
.claude/llm-functions/mcp/server/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# MCP-Server
|
||||||
|
|
||||||
|
Let LLM-functions tools/agents be used through the Model Context Protocol.
|
||||||
|
|
||||||
|
## Serve tools
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"tools": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"mcp-llm-functions",
|
||||||
|
"<llm-functions-dir>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Serve the agent
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"<agent-name>": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"mcp-llm-functions",
|
||||||
|
"<llm-functions-dir>"
|
||||||
|
"<agent-name>",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `AGENT_TOOLS_ONLY`: Set to `true` or `1` to ignore shared tools and display only agent tools.
|
||||||
129
.claude/llm-functions/mcp/server/index.js
Executable file
129
.claude/llm-functions/mcp/server/index.js
Executable file
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
let [rootDir, agentName] = process.argv.slice(2);
|
||||||
|
if (!rootDir) {
|
||||||
|
console.error("Usage: mcp-llm-functions <llm-functions-dir> [<agent-name>]");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
rootDir = path.resolve(rootDir);
|
||||||
|
|
||||||
|
let functionsJsonPath = path.join(rootDir, "functions.json");
|
||||||
|
if (agentName) {
|
||||||
|
functionsJsonPath = path.join(rootDir, "agents", agentName, "functions.json");
|
||||||
|
}
|
||||||
|
let functions = [];
|
||||||
|
try {
|
||||||
|
const data = await fs.promises.readFile(functionsJsonPath, "utf8");
|
||||||
|
functions = JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
console.error(`Failed to read functions at '${functionsJsonPath}'`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const agentToolsOnly = process.env["AGENT_TOOLS_ONLY"] === "true" || process.env["AGENT_TOOLS_ONLY"] === "1";
|
||||||
|
functions = functions.filter(f => {
|
||||||
|
if (f.mcp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (agentToolsOnly) {
|
||||||
|
return f.agent;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const env = Object.assign({}, process.env, {
|
||||||
|
PATH: `${path.join(rootDir, "bin")}:${process.env.PATH}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: `llm-functions/${agentName || "common-tools"}`,
|
||||||
|
version: "0.1.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: functions.map((f) => ({
|
||||||
|
name: f.name,
|
||||||
|
description: f.description,
|
||||||
|
inputSchema: f.parameters,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const functionObj = functions.find((f) => f.name === request.params.name);
|
||||||
|
if (!functionObj) {
|
||||||
|
throw new Error(`Unknown tool '${request.params.name}'`);
|
||||||
|
}
|
||||||
|
let command = request.params.name;
|
||||||
|
let args = [JSON.stringify(request.params.arguments || {})];
|
||||||
|
if (agentName && functionObj.agent) {
|
||||||
|
args.unshift(command);
|
||||||
|
command = agentName;
|
||||||
|
}
|
||||||
|
const tmpFile = path.join(os.tmpdir(), `mcp-llm-functions-${process.pid}-eval-${uuid()}`);
|
||||||
|
const { exitCode, stderr } = await runCommand(command, args, { ...env, LLM_OUTPUT: tmpFile });
|
||||||
|
if (exitCode === 0) {
|
||||||
|
let output = '';
|
||||||
|
try {
|
||||||
|
output = await fs.promises.readFile(tmpFile, "utf8");
|
||||||
|
} catch { };
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: output }],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
isError: true,
|
||||||
|
content: [{ type: "text", text: stderr }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function runCommand(command, args, env) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
stdio: ['ignore', 'ignore', 'pipe'],
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (exitCode) => {
|
||||||
|
resolve({ exitCode, stderr });
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
resolve({ exitCode: 1, stderr: `Command execution failed: ${err.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runServer() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
runServer().catch(console.error);
|
||||||
24
.claude/llm-functions/mcp/server/package.json
Normal file
24
.claude/llm-functions/mcp/server/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "mcp-llm-functions",
|
||||||
|
"version": "1.2.0",
|
||||||
|
"description": "Let LLM-functions tools/agents be used through the Model Context Protocol",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "sigoden <sigoden@gmail.com>",
|
||||||
|
"homepage": "https://github.com/sigoden/llm-functions/tree/main/mcp/server",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/sigoden/llm-functions.git",
|
||||||
|
"directory": "mcp/server"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"mcp-llm-functions": "index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.1.0",
|
||||||
|
"uuid": "^11.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
222
.claude/llm-functions/scripts/build-declarations.js
Executable file
222
.claude/llm-functions/scripts/build-declarations.js
Executable file
@@ -0,0 +1,222 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const TOOL_ENTRY_FUNC = "run";
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const scriptfile = process.argv[2];
|
||||||
|
const isTool = path.dirname(scriptfile) == "tools";
|
||||||
|
const contents = fs.readFileSync(process.argv[2], "utf8");
|
||||||
|
const functions = extractFunctions(contents, isTool);
|
||||||
|
let declarations = [];
|
||||||
|
for (const { funcName, jsdoc } of functions) {
|
||||||
|
const { description, params } = parseJsDoc(jsdoc, funcName);
|
||||||
|
if (!description) continue;
|
||||||
|
const declaration = buildDeclaration(funcName, description, params);
|
||||||
|
declarations.push(declaration);
|
||||||
|
}
|
||||||
|
if (isTool) {
|
||||||
|
const name = getBasename(scriptfile);
|
||||||
|
if (declarations.length > 0) {
|
||||||
|
declarations = declarations.slice(0, 1);
|
||||||
|
declarations[0].name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify(declarations, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} contents
|
||||||
|
* @param {bool} isTool
|
||||||
|
*/
|
||||||
|
function extractFunctions(contents, isTool) {
|
||||||
|
const output = [];
|
||||||
|
const lines = contents.split("\n");
|
||||||
|
let isInComment = false;
|
||||||
|
let jsdoc = "";
|
||||||
|
let incompleteComment = "";
|
||||||
|
for (let line of lines) {
|
||||||
|
if (/^\s*\/\*/.test(line)) {
|
||||||
|
isInComment = true;
|
||||||
|
incompleteComment += `\n${line}`;
|
||||||
|
} else if (/^\s*\*\//.test(line)) {
|
||||||
|
isInComment = false;
|
||||||
|
incompleteComment += `\n${line}`;
|
||||||
|
jsdoc = incompleteComment;
|
||||||
|
incompleteComment = "";
|
||||||
|
} else if (isInComment) {
|
||||||
|
incompleteComment += `\n${line}`;
|
||||||
|
} else {
|
||||||
|
if (!jsdoc || line.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isTool) {
|
||||||
|
if (new RegExp(`^export (async )?function ${TOOL_ENTRY_FUNC}|^exports\.${TOOL_ENTRY_FUNC}`).test(line)) {
|
||||||
|
output.push({
|
||||||
|
funcName: TOOL_ENTRY_FUNC,
|
||||||
|
jsdoc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let match = /^export (async )?function ([A-Za-z0-9_]+)/.exec(line);
|
||||||
|
let funcName = null;
|
||||||
|
if (match) {
|
||||||
|
funcName = match[2];
|
||||||
|
}
|
||||||
|
if (!funcName) {
|
||||||
|
match = /^exports\.([A-Za-z0-9_]+) = (async )?function /.exec(line);
|
||||||
|
if (match) {
|
||||||
|
funcName = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (funcName && !funcName.startsWith("_")) {
|
||||||
|
output.push({ funcName, jsdoc });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsdoc = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} jsdoc
|
||||||
|
* @param {string} funcName,
|
||||||
|
*/
|
||||||
|
function parseJsDoc(jsdoc, funcName) {
|
||||||
|
const lines = jsdoc.split("\n");
|
||||||
|
let description = "";
|
||||||
|
const rawParams = [];
|
||||||
|
let tag = "";
|
||||||
|
for (let line of lines) {
|
||||||
|
line = line.replace(/^\s*(\/\*\*|\*\/|\*)/, "").trim();
|
||||||
|
let match = /^@(\w+)/.exec(line);
|
||||||
|
if (match) {
|
||||||
|
tag = match[1];
|
||||||
|
}
|
||||||
|
if (!tag) {
|
||||||
|
description += `\n${line}`;
|
||||||
|
} else if (tag == "property") {
|
||||||
|
if (match) {
|
||||||
|
rawParams.push(line.slice(tag.length + 1).trim());
|
||||||
|
} else {
|
||||||
|
rawParams[rawParams.length - 1] += `\n${line}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const params = [];
|
||||||
|
for (const rawParam of rawParams) {
|
||||||
|
try {
|
||||||
|
params.push(parseParam(rawParam));
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to parse function '${funcName}' of jsdoc '@property ${rawParam}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
description: description.trim(),
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {ReturnType<parseParam>} Param
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} rawParam
|
||||||
|
*/
|
||||||
|
function parseParam(rawParam) {
|
||||||
|
const regex = /^{([^}]+)} +(\S+)( *- +| +)?/;
|
||||||
|
const match = regex.exec(rawParam);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid jsdoc comment`);
|
||||||
|
}
|
||||||
|
const type = match[1];
|
||||||
|
let name = match[2];
|
||||||
|
const description = rawParam.replace(regex, "");
|
||||||
|
|
||||||
|
let required = true;
|
||||||
|
if (/^\[.*\]$/.test(name)) {
|
||||||
|
name = name.slice(1, -1);
|
||||||
|
required = false;
|
||||||
|
}
|
||||||
|
let property = buildProperty(type, description);
|
||||||
|
return { name, property, required };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} type
|
||||||
|
* @param {string} description
|
||||||
|
*/
|
||||||
|
function buildProperty(type, description) {
|
||||||
|
type = type.toLowerCase();
|
||||||
|
const property = {};
|
||||||
|
if (type.includes("|")) {
|
||||||
|
property.type = "string";
|
||||||
|
property.enum = type.replace(/'/g, "").split("|");
|
||||||
|
} else if (type === "boolean") {
|
||||||
|
property.type = "boolean";
|
||||||
|
} else if (type === "string") {
|
||||||
|
property.type = "string";
|
||||||
|
} else if (type === "integer") {
|
||||||
|
property.type = "integer";
|
||||||
|
} else if (type === "number") {
|
||||||
|
property.type = "number";
|
||||||
|
} else if (type === "string[]") {
|
||||||
|
property.type = "array";
|
||||||
|
property.items = { type: "string" };
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported type '${type}'`);
|
||||||
|
}
|
||||||
|
property.description = description;
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} description
|
||||||
|
* @param {Param[]} params
|
||||||
|
*/
|
||||||
|
function buildDeclaration(name, description, params) {
|
||||||
|
const declaration = {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const schema = declaration.parameters;
|
||||||
|
const requiredParams = [];
|
||||||
|
for (const { name, property, required } of params) {
|
||||||
|
schema.properties[name] = property;
|
||||||
|
if (required) {
|
||||||
|
requiredParams.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requiredParams.length > 0) {
|
||||||
|
schema.required = requiredParams;
|
||||||
|
}
|
||||||
|
return declaration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} filePath
|
||||||
|
*/
|
||||||
|
function getBasename(filePath) {
|
||||||
|
const filenameWithExt = filePath.split(/[/\\]/).pop();
|
||||||
|
|
||||||
|
const lastDotIndex = filenameWithExt.lastIndexOf(".");
|
||||||
|
|
||||||
|
if (lastDotIndex === -1) {
|
||||||
|
return filenameWithExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filenameWithExt.substring(0, lastDotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
190
.claude/llm-functions/scripts/build-declarations.py
Executable file
190
.claude/llm-functions/scripts/build-declarations.py
Executable file
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
TOOL_ENTRY_FUNC = "run"
|
||||||
|
|
||||||
|
|
||||||
|
def main(is_tool=True):
|
||||||
|
scriptfile = sys.argv[1]
|
||||||
|
is_tool = os.path.dirname(scriptfile) == "tools"
|
||||||
|
|
||||||
|
with open(scriptfile, "r", encoding="utf-8") as f:
|
||||||
|
contents = f.read()
|
||||||
|
|
||||||
|
functions = extract_functions(contents, is_tool)
|
||||||
|
declarations = []
|
||||||
|
for function in functions:
|
||||||
|
func_name, docstring, func_args = function
|
||||||
|
description, params = parse_docstring(docstring)
|
||||||
|
if not description:
|
||||||
|
continue
|
||||||
|
declarations.append(
|
||||||
|
build_declaration(func_name, description, params, func_args)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_tool:
|
||||||
|
name = os.path.splitext(os.path.basename(scriptfile))[0]
|
||||||
|
if declarations:
|
||||||
|
declarations = declarations[0:1]
|
||||||
|
declarations[0]["name"] = name
|
||||||
|
|
||||||
|
print(json.dumps(declarations, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def extract_functions(contents: str, is_tool: bool):
|
||||||
|
tree = ast.parse(contents)
|
||||||
|
output = []
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ast.FunctionDef):
|
||||||
|
continue
|
||||||
|
func_name = node.name
|
||||||
|
if is_tool and func_name != TOOL_ENTRY_FUNC:
|
||||||
|
continue
|
||||||
|
if func_name.startswith("_"):
|
||||||
|
continue
|
||||||
|
docstring = ast.get_docstring(node) or ""
|
||||||
|
func_args = OrderedDict()
|
||||||
|
for arg in node.args.args:
|
||||||
|
arg_name = arg.arg
|
||||||
|
arg_type = get_arg_type(arg.annotation)
|
||||||
|
func_args[arg_name] = arg_type
|
||||||
|
output.append((func_name, docstring, func_args))
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def get_arg_type(annotation) -> str:
|
||||||
|
if annotation is None:
|
||||||
|
return ""
|
||||||
|
elif isinstance(annotation, ast.Name):
|
||||||
|
return annotation.id
|
||||||
|
elif isinstance(annotation, ast.Subscript):
|
||||||
|
if isinstance(annotation.value, ast.Name):
|
||||||
|
type_name = annotation.value.id
|
||||||
|
if type_name == "List":
|
||||||
|
child = get_arg_type(annotation.slice)
|
||||||
|
return f"list[{child}]"
|
||||||
|
if type_name == "Literal":
|
||||||
|
literals = [ast.unparse(el) for el in annotation.slice.elts]
|
||||||
|
return f"{'|'.join(literals)}"
|
||||||
|
if type_name == "Optional":
|
||||||
|
child = get_arg_type(annotation.slice)
|
||||||
|
return f"{child}?"
|
||||||
|
return "any"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_docstring(docstring: str):
|
||||||
|
lines = docstring.splitlines()
|
||||||
|
description = ""
|
||||||
|
rawParams = []
|
||||||
|
is_in_args = False
|
||||||
|
for line in lines:
|
||||||
|
if not is_in_args:
|
||||||
|
if line.startswith("Args:"):
|
||||||
|
is_in_args = True
|
||||||
|
else:
|
||||||
|
description += f"\n{line}"
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if re.search(r"^\s+", line):
|
||||||
|
rawParams.append(line.strip())
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
params = {}
|
||||||
|
for rawParam in rawParams:
|
||||||
|
name, type_, param_description = parse_param(rawParam)
|
||||||
|
params[name] = (type_, param_description)
|
||||||
|
return (description.strip(), params)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_param(raw_param: str):
|
||||||
|
name = ""
|
||||||
|
description = ""
|
||||||
|
type_from_comment = ""
|
||||||
|
if ":" in raw_param:
|
||||||
|
name, description = raw_param.split(":", 1)
|
||||||
|
name = name.strip()
|
||||||
|
description = description.strip()
|
||||||
|
else:
|
||||||
|
name = raw_param
|
||||||
|
if " " in name:
|
||||||
|
name, type_from_comment = name.split(" ", 1)
|
||||||
|
type_from_comment = type_from_comment.strip()
|
||||||
|
|
||||||
|
if type_from_comment.startswith("(") and type_from_comment.endswith(")"):
|
||||||
|
type_from_comment = type_from_comment[1:-1]
|
||||||
|
type_parts = [value.strip() for value in type_from_comment.split(",")]
|
||||||
|
type_ = type_parts[0]
|
||||||
|
if "optional" in type_parts[1:]:
|
||||||
|
type_ = f"{type_}?"
|
||||||
|
|
||||||
|
return (name, type_, description)
|
||||||
|
|
||||||
|
|
||||||
|
def build_declaration(
|
||||||
|
name: str, description: str, params: dict, args: OrderedDict[str, str]
|
||||||
|
) -> dict[str, dict]:
|
||||||
|
declaration = {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
schema = declaration["parameters"]
|
||||||
|
required_params = []
|
||||||
|
for arg_name, arg_type in args.items():
|
||||||
|
type_ = arg_type
|
||||||
|
description = ""
|
||||||
|
required = True
|
||||||
|
if params.get(arg_name):
|
||||||
|
param_type, description = params[arg_name]
|
||||||
|
if not type_:
|
||||||
|
type_ = param_type
|
||||||
|
if type_.endswith("?"):
|
||||||
|
type_ = type_[:-1]
|
||||||
|
required = False
|
||||||
|
try:
|
||||||
|
property = build_property(type_, description)
|
||||||
|
except:
|
||||||
|
raise ValueError(f"Unable to parse arg '{arg_name}' of function '{name}'")
|
||||||
|
schema["properties"][arg_name] = property
|
||||||
|
if required:
|
||||||
|
required_params.append(arg_name)
|
||||||
|
if required_params:
|
||||||
|
schema["required"] = required_params
|
||||||
|
return declaration
|
||||||
|
|
||||||
|
|
||||||
|
def build_property(type_: str, description: str):
|
||||||
|
property = {}
|
||||||
|
if "|" in type_:
|
||||||
|
property["type"] = "string"
|
||||||
|
property["enum"] = type_.replace("'", "").split("|")
|
||||||
|
elif type_ == "bool":
|
||||||
|
property["type"] = "boolean"
|
||||||
|
elif type_ == "str":
|
||||||
|
property["type"] = "string"
|
||||||
|
elif type_ == "int":
|
||||||
|
property["type"] = "integer"
|
||||||
|
elif type_ == "float":
|
||||||
|
property["type"] = "number"
|
||||||
|
elif type_ == "list[str]":
|
||||||
|
property["type"] = "array"
|
||||||
|
property["items"] = {"type": "string"}
|
||||||
|
elif type_ == "":
|
||||||
|
property["type"] = "string"
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported type `{type_}`")
|
||||||
|
property["description"] = description
|
||||||
|
return property
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
73
.claude/llm-functions/scripts/build-declarations.sh
Executable file
73
.claude/llm-functions/scripts/build-declarations.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
main() {
|
||||||
|
scriptfile="$1"
|
||||||
|
is_tool=false
|
||||||
|
if [[ "$(dirname "$scriptfile")" == tools ]]; then
|
||||||
|
is_tool=true
|
||||||
|
fi
|
||||||
|
if [[ "$is_tool" == "true" ]]; then
|
||||||
|
expr='[.]'
|
||||||
|
else
|
||||||
|
expr='.subcommands'
|
||||||
|
fi
|
||||||
|
argc --argc-export "$scriptfile" | \
|
||||||
|
jq "$expr" | \
|
||||||
|
build_declarations
|
||||||
|
}
|
||||||
|
|
||||||
|
build_declarations() {
|
||||||
|
jq --arg is_tool "$is_tool" -r '
|
||||||
|
def filter_declaration:
|
||||||
|
(if $is_tool == "true" then
|
||||||
|
.
|
||||||
|
else
|
||||||
|
select(.name | startswith("_") | not)
|
||||||
|
end) | select(.description != "");
|
||||||
|
|
||||||
|
def parse_description(flag_option):
|
||||||
|
if flag_option.describe == "" then
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
{ "description": flag_option.describe }
|
||||||
|
end;
|
||||||
|
|
||||||
|
def parse_enum(flag_option):
|
||||||
|
if flag_option.choice.type == "Values" then
|
||||||
|
{ "enum": flag_option.choice.data }
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end;
|
||||||
|
|
||||||
|
def parse_property(flag_option):
|
||||||
|
[
|
||||||
|
{ condition: (flag_option.flag == true), result: { type: "boolean" } },
|
||||||
|
{ condition: (flag_option.multiple_occurs == true), result: { type: "array", items: { type: "string" } } },
|
||||||
|
{ condition: (flag_option.notations[0] == "INT"), result: { type: "integer" } },
|
||||||
|
{ condition: (flag_option.notations[0] == "NUM"), result: { type: "number" } },
|
||||||
|
{ condition: true, result: { type: "string" } } ]
|
||||||
|
| map(select(.condition) | .result) | first
|
||||||
|
| (. + parse_description(flag_option))
|
||||||
|
| (. + parse_enum(flag_option))
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
def parse_parameter(flag_options):
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
properties: (reduce flag_options[] as $item ({}; . + { ($item.id | sub("-"; "_"; "g")): parse_property($item) })),
|
||||||
|
required: [flag_options[] | select(.required == true) | .id | sub("-"; "_"; "g")],
|
||||||
|
};
|
||||||
|
|
||||||
|
def parse_declaration:
|
||||||
|
{
|
||||||
|
name: (.name | sub("-"; "_"; "g")),
|
||||||
|
description: .describe,
|
||||||
|
parameters: parse_parameter([.flag_options[] | select(.id != "help" and .id != "version")])
|
||||||
|
};
|
||||||
|
[
|
||||||
|
.[] | parse_declaration | filter_declaration
|
||||||
|
]'
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
78
.claude/llm-functions/scripts/check-deps.sh
Executable file
78
.claude/llm-functions/scripts/check-deps.sh
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Check dependencies
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./scripts/check-deps.sh tools/execute_sql_code.sh
|
||||||
|
# ./scripts/check-deps.sh agents/json-viewer/tools.js
|
||||||
|
#
|
||||||
|
# @arg script-path! The script file path
|
||||||
|
|
||||||
|
main() {
|
||||||
|
script_path="$argc_script_path"
|
||||||
|
if [[ ! -f "$script_path" ]]; then
|
||||||
|
echo "✗ not found $script_path"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
ext="${script_path##*.}"
|
||||||
|
if [[ "$script_path" == tools/* ]]; then
|
||||||
|
if [[ "$ext" == "sh" ]]; then
|
||||||
|
check_sh_dependencies
|
||||||
|
fi
|
||||||
|
elif [[ "$script_path" == agents/* ]]; then
|
||||||
|
if [[ "$ext" == "sh" ]]; then
|
||||||
|
check_sh_dependencies
|
||||||
|
elif [[ "$ext" == "js" ]]; then
|
||||||
|
check_agent_js_dependencies
|
||||||
|
elif [[ "$ext" == "py" ]]; then
|
||||||
|
check_agent_py_dependencies
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_sh_dependencies() {
|
||||||
|
deps=( $(sed -E -n 's/.*@meta require-tools //p' "$script_path") )
|
||||||
|
missing_deps=()
|
||||||
|
for dep in "${deps[@]}"; do
|
||||||
|
if ! command -v "$dep" &> /dev/null; then
|
||||||
|
missing_deps+=("$dep")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -n "${missing_deps}" ]]; then
|
||||||
|
echo "✗ missing tools: ${missing_deps[*]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_agent_js_dependencies() {
|
||||||
|
agent_dir="$(dirname "$script_path")"
|
||||||
|
if [[ -f "$agent_dir/package.json" ]]; then
|
||||||
|
npm ls --prefix="$agent_dir" --depth=0 --silent >/dev/null 2>&1 || \
|
||||||
|
{
|
||||||
|
cmd="cd $agent_dir && npm install"
|
||||||
|
echo "✗ missing node modules"
|
||||||
|
read -p "? run \`$cmd\` to fix [Y/n] " choice
|
||||||
|
if [[ "$choice" == "Y" || "$choice" == "y" || -z "$choice" ]]; then
|
||||||
|
(eval "$cmd")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_agent_py_dependencies() {
|
||||||
|
agent_dir="$(dirname "$script_path")"
|
||||||
|
if [[ -f "$agent_dir/requirements.txt" ]]; then
|
||||||
|
python <(cat "$agent_dir/requirements.txt" | sed -E -n 's/^([A-Za-z_]+).*/import \1/p') >/dev/null 2>&1 || \
|
||||||
|
{
|
||||||
|
cmd="cd $agent_dir && pip install -r requirements.txt"
|
||||||
|
echo "✗ missing python modules"
|
||||||
|
read -p "? run \`$cmd\` to fix [Y/n] " choice
|
||||||
|
if [[ "$choice" == "Y" || "$choice" == "y" || -z "$choice" ]]; then
|
||||||
|
(eval "$cmd")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# See more details at https://github.com/sigoden/argc
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
195
.claude/llm-functions/scripts/create-tool.sh
Executable file
195
.claude/llm-functions/scripts/create-tool.sh
Executable file
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Create a boilplate tool script
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./scripts/create-tool.sh _test.py foo bar! baz+ qux*
|
||||||
|
#
|
||||||
|
# @option --description <text> The tool description
|
||||||
|
# @flag --force Override the exist tool file
|
||||||
|
# @arg name! The script file name
|
||||||
|
# @arg params* The script parameters
|
||||||
|
|
||||||
|
main() {
|
||||||
|
output="tools/$argc_name"
|
||||||
|
if [[ -f "$output" ]] && [[ -z "$argc_force" ]]; then
|
||||||
|
_die "$output already exists"
|
||||||
|
fi
|
||||||
|
ext="${argc_name##*.}"
|
||||||
|
description="${argc_description:-"The description for the tool"}"
|
||||||
|
support_exts=('.sh' '.js' '.py')
|
||||||
|
if [[ "$ext" == "$argc_name" ]]; then
|
||||||
|
_die "error: no extension name, pelease add one of ${support_exts[*]}"
|
||||||
|
fi
|
||||||
|
case $ext in
|
||||||
|
sh) create_sh ;;
|
||||||
|
js) create_js ;;
|
||||||
|
py) create_py ;;
|
||||||
|
*) _die "error: invalid extension name: $ext, must be one of ${support_exts[*]}" ;;
|
||||||
|
esac
|
||||||
|
echo "$output generated"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_sh() {
|
||||||
|
cat <<-'EOF' > "$output"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
EOF
|
||||||
|
echo "# @describe $description" >> "$output"
|
||||||
|
for param in "${argc_params[@]}"; do
|
||||||
|
echo "# @option --$(echo $param | sed 's/-/_/g')" >> "$output"
|
||||||
|
done
|
||||||
|
cat <<-'EOF' >> "$output"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
( set -o posix ; set ) | grep ^argc_
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
|
EOF
|
||||||
|
chmod +x "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_js() {
|
||||||
|
properties=''
|
||||||
|
for param in "${argc_params[@]}"; do
|
||||||
|
if [[ "$param" == *'!' ]]; then
|
||||||
|
param="${param:0:$((${#param}-1))}"
|
||||||
|
property=" * @property {string} $param - "
|
||||||
|
elif [[ "$param" == *'+' ]]; then
|
||||||
|
param="${param:0:$((${#param}-1))}"
|
||||||
|
property=" * @property {string[]} $param - "
|
||||||
|
elif [[ "$param" == *'*' ]]; then
|
||||||
|
param="${param:0:$((${#param}-1))}"
|
||||||
|
property=" * @property {string[]} [$param] - "
|
||||||
|
else
|
||||||
|
property=" * @property {string} [$param] - "
|
||||||
|
fi
|
||||||
|
properties+=$'\n'"$property"
|
||||||
|
done
|
||||||
|
cat <<EOF > "$output"
|
||||||
|
/**
|
||||||
|
* ${description}
|
||||||
|
* @typedef {Object} Args${properties}
|
||||||
|
* @param {Args} args
|
||||||
|
*/
|
||||||
|
exports.run = function (args) {
|
||||||
|
console.log(args);
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
create_py() {
|
||||||
|
has_array_param=false
|
||||||
|
has_optional_pram=false
|
||||||
|
required_properties=''
|
||||||
|
optional_properties=''
|
||||||
|
required_arguments=()
|
||||||
|
optional_arguments=()
|
||||||
|
indent=" "
|
||||||
|
for param in "${argc_params[@]}"; do
|
||||||
|
optional=false
|
||||||
|
if [[ "$param" == *'!' ]]; then
|
||||||
|
param="${param:0:$((${#param}-1))}"
|
||||||
|
type="str"
|
||||||
|
elif [[ "$param" == *'+' ]]; then
|
||||||
|
param="${param:0:$((${#param}-1))}"
|
||||||
|
type="List[str]"
|
||||||
|
has_array_param=true
|
||||||
|
elif [[ "$param" == *'*' ]]; then
|
||||||
|
param="${param:0:$((${#param}-1))}"
|
||||||
|
type="Optional[List[str]] = None"
|
||||||
|
optional=true
|
||||||
|
has_array_param=true
|
||||||
|
else
|
||||||
|
optional=true
|
||||||
|
type="Optional[str] = None"
|
||||||
|
fi
|
||||||
|
if [[ "$optional" == "true" ]]; then
|
||||||
|
has_optional_pram=true
|
||||||
|
optional_arguments+="$param: $type, "
|
||||||
|
optional_properties+=$'\n'"$indent$indent$param: -"
|
||||||
|
else
|
||||||
|
required_arguments+="$param: $type, "
|
||||||
|
required_properties+=$'\n'"$indent$indent$param: -"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
import_typing_members=()
|
||||||
|
if [[ "$has_array_param" == "true" ]]; then
|
||||||
|
import_typing_members+=("List")
|
||||||
|
fi
|
||||||
|
if [[ "$has_optional_pram" == "true" ]]; then
|
||||||
|
import_typing_members+=("Optional")
|
||||||
|
fi
|
||||||
|
imports=""
|
||||||
|
if [[ -n "$import_typing_members" ]]; then
|
||||||
|
members="$(echo "${import_typing_members[*]}" | sed 's/ /, /')"
|
||||||
|
imports="from typing import $members"$'\n'
|
||||||
|
fi
|
||||||
|
if [[ -n "$imports" ]]; then
|
||||||
|
imports="$imports"$'\n'
|
||||||
|
fi
|
||||||
|
cat <<EOF > "$output"
|
||||||
|
${imports}
|
||||||
|
def run(${required_arguments}${optional_arguments}):
|
||||||
|
"""${description}
|
||||||
|
Args:${required_properties}${optional_properties}
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
build_schema() {
|
||||||
|
echo '{
|
||||||
|
"name": "'"${argc_name%%.*}"'",
|
||||||
|
"description": "",
|
||||||
|
"parameters": '"$(build_properties)"'
|
||||||
|
}' | jq '.' | sed '2,$s/^/ /g'
|
||||||
|
}
|
||||||
|
|
||||||
|
build_properties() {
|
||||||
|
required_params=()
|
||||||
|
properties=''
|
||||||
|
for param in "${argc_params[@]}"; do
|
||||||
|
if [[ "$param" == *'!' ]]; then
|
||||||
|
param="${param:0:$((${#param}-1))}"
|
||||||
|
required_params+=("$param")
|
||||||
|
property='{"'"$param"'":{"type":"string","description":""}}'
|
||||||
|
elif [[ "$param" == *'+' ]]; then
|
||||||
|
param="${param:0:$((${#param}-1))}"
|
||||||
|
required_params+=("$param")
|
||||||
|
property='{"'"$param"'":{"type":"array","description":"","items": {"type":"string"}}}'
|
||||||
|
elif [[ "$param" == *'*' ]]; then
|
||||||
|
param="${param:0:$((${#param}-1))}"
|
||||||
|
property='{"'"$param"'":{"type":"array","description":"","items": {"type":"string"}}}'
|
||||||
|
else
|
||||||
|
property='{"'"$param"'":{"type":"string","description":""}}'
|
||||||
|
fi
|
||||||
|
properties+="$property"
|
||||||
|
done
|
||||||
|
required=''
|
||||||
|
for param in "${required_params[@]}"; do
|
||||||
|
if [[ -z "$required" ]]; then
|
||||||
|
required=',"required":['
|
||||||
|
fi
|
||||||
|
required+="\"$param\","
|
||||||
|
done
|
||||||
|
if [[ -n "$required" ]]; then
|
||||||
|
required="${required:0:$((${#required}-1))}"
|
||||||
|
required+="]"
|
||||||
|
fi
|
||||||
|
echo '{
|
||||||
|
"type": "object",
|
||||||
|
"properties": '"$(echo "$properties" | jq -s 'add')$required"'
|
||||||
|
}' | jq '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
_die() {
|
||||||
|
echo "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# See more details at https://github.com/sigoden/argc
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
135
.claude/llm-functions/scripts/declarations-util.sh
Executable file
135
.claude/llm-functions/scripts/declarations-util.sh
Executable file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Utils for declarations json file
|
||||||
|
|
||||||
|
# @cmd Pretty print declarations
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./scripts/declarations.sh pretty-print functions.json
|
||||||
|
# cat functions.json | ./scripts/declarations.sh pretty-print functions.json
|
||||||
|
# @flag --no-type Do not to display param type info
|
||||||
|
# @arg json-file The json file, Read stdin if omitted
|
||||||
|
pretty-print() {
|
||||||
|
_run _pretty_print
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Generate placeholder json according to declarations
|
||||||
|
# Examples:
|
||||||
|
# ./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
|
||||||
|
}
|
||||||
|
|
||||||
|
_run() {
|
||||||
|
func="$1"
|
||||||
|
_get_declarations_data
|
||||||
|
if [[ "$json_type" == "object" ]]; then
|
||||||
|
echo "$json_data" | $func
|
||||||
|
elif [[ "$json_type" == "array" ]]; then
|
||||||
|
for i in $(seq 1 $json_array_len); do
|
||||||
|
echo "$json_data" | jq '.['$((i-1))']' | $func
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_get_declarations_data() {
|
||||||
|
if [[ -f "$argc_json_file" ]]; then
|
||||||
|
json_data="$(cat "$argc_json_file")"
|
||||||
|
else
|
||||||
|
json_data="$(cat)"
|
||||||
|
fi
|
||||||
|
json_type="$(echo "$json_data" | jq -r '
|
||||||
|
if type == "array" then
|
||||||
|
(. | length) as $len | "array;\($len)"
|
||||||
|
else
|
||||||
|
if type == "object" then
|
||||||
|
type
|
||||||
|
else
|
||||||
|
""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
' 2>/dev/null || true)"
|
||||||
|
if [[ "$json_type" == *object* ]]; then
|
||||||
|
:;
|
||||||
|
elif [[ "$json_type" == *array* ]]; then
|
||||||
|
json_array_len="${json_type#*;}"
|
||||||
|
json_type="${json_type%%;*}"
|
||||||
|
if [[ ! "$json_array_len" -gt 0 ]]; then
|
||||||
|
json_type=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$json_type" ]]; then
|
||||||
|
echo "error: invalid JSON data" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_pretty_print() {
|
||||||
|
jq --arg no_type "$argc_no_type" -r '
|
||||||
|
def get_type:
|
||||||
|
.value.type as $type |
|
||||||
|
(if .required then "" else "?" end) as $symbol |
|
||||||
|
(.value.enum // []) as $enum |
|
||||||
|
([
|
||||||
|
{ condition: ($type == "array"), result: "string[]" },
|
||||||
|
{ condition: ($type == "string" and ($enum | length > 0)), result: ($enum | join("|")) },
|
||||||
|
{ condition: ($type == "string"), result: "" },
|
||||||
|
{ condition: true, result: $type }
|
||||||
|
] | map(select(.condition) | .result) | first) as $kind |
|
||||||
|
if $kind != "" then "(\($kind))\($symbol)" else $symbol end;
|
||||||
|
|
||||||
|
def oneline_description: split("\n")[0];
|
||||||
|
|
||||||
|
def parse_property:
|
||||||
|
.key as $key |
|
||||||
|
(.value.description | oneline_description) as $description |
|
||||||
|
(if $no_type != "1" then (. | get_type) else "" end) as $type |
|
||||||
|
" \($key)\($type): \($description)";
|
||||||
|
|
||||||
|
def print_params:
|
||||||
|
.parameters |
|
||||||
|
.required as $requiredProperties |
|
||||||
|
.properties | to_entries[] |
|
||||||
|
.key as $key | .+ { "required": ($requiredProperties | index($key) != null) } |
|
||||||
|
parse_property;
|
||||||
|
|
||||||
|
def print_title:
|
||||||
|
(.description | oneline_description) as $description |
|
||||||
|
"\(.name): \($description)";
|
||||||
|
|
||||||
|
print_title, print_params
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
_generate_json() {
|
||||||
|
jq -r -c '
|
||||||
|
def convert_string:
|
||||||
|
if has("enum") then .enum[0] else "foo" end;
|
||||||
|
|
||||||
|
def parse_property:
|
||||||
|
.key as $key |
|
||||||
|
.value.type as $type |
|
||||||
|
[
|
||||||
|
{ condition: ($type == "string"), result: { $key: (.value | convert_string) }},
|
||||||
|
{ condition: ($type == "boolean"), result: { $key: false }},
|
||||||
|
{ condition: ($type == "integer"), result: { $key: 42 }},
|
||||||
|
{ condition: ($type == "number"), result: { $key: 3.14 }},
|
||||||
|
{ condition: ($type == "array"), result: { $key: [ "v1" ] } }
|
||||||
|
] | map(select(.condition) | .result) | first;
|
||||||
|
|
||||||
|
.name,
|
||||||
|
(
|
||||||
|
.parameters |
|
||||||
|
[
|
||||||
|
.properties | to_entries[] | parse_property
|
||||||
|
] | add // {}
|
||||||
|
)
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
# See more details at https://github.com/sigoden/argc
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
211
.claude/llm-functions/scripts/mcp.sh
Executable file
211
.claude/llm-functions/scripts/mcp.sh
Executable file
@@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)"
|
||||||
|
BIN_DIR="$ROOT_DIR/bin"
|
||||||
|
MCP_DIR="$ROOT_DIR/cache/__mcp__"
|
||||||
|
MCP_LOG_FILE="$MCP_DIR/mcp-bridge.log"
|
||||||
|
MCP_JSON_PATH="$ROOT_DIR/mcp.json"
|
||||||
|
FUNCTIONS_JSON_PATH="$ROOT_DIR/functions.json"
|
||||||
|
MCP_BRIDGE_PORT="${MCP_BRIDGE_PORT:-8808}"
|
||||||
|
|
||||||
|
# @cmd Start/restart the mcp bridge server
|
||||||
|
# @alias restart
|
||||||
|
start() {
|
||||||
|
if [[ ! -f "$MCP_JSON_PATH" ]]; then
|
||||||
|
_die "error: not found mcp.json"
|
||||||
|
fi
|
||||||
|
stop
|
||||||
|
mkdir -p "$MCP_DIR"
|
||||||
|
index_js="$ROOT_DIR/mcp/bridge/index.js"
|
||||||
|
llm_functions_dir="$ROOT_DIR"
|
||||||
|
if _is_win; then
|
||||||
|
index_js="$(cygpath -w "$index_js")"
|
||||||
|
llm_functions_dir="$(cygpath -w "$llm_functions_dir")"
|
||||||
|
fi
|
||||||
|
echo "Start MCP Bridge server..."
|
||||||
|
echo "Install node dependencies..." > "$MCP_LOG_FILE"
|
||||||
|
(cd "$ROOT_DIR/mcp/bridge" && npm install 1>/dev/null 2>> "$MCP_LOG_FILE")
|
||||||
|
nohup node "$index_js" "$llm_functions_dir" >> "$MCP_LOG_FILE" 2>&1 &
|
||||||
|
wait-for-server
|
||||||
|
echo "Merge MCP tools into functions.json"
|
||||||
|
"$0" merge-functions -S
|
||||||
|
build-bin
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Stop the mcp bridge server
|
||||||
|
stop() {
|
||||||
|
pid="$(get-server-pid)"
|
||||||
|
if [[ -n "$pid" ]]; then
|
||||||
|
if _is_win; then
|
||||||
|
taskkill /PID "$pid" /F > /dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
kill -9 "$pid" > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
"$0" recovery-functions -S
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Check the mcp bridge server is running
|
||||||
|
check() {
|
||||||
|
if [[ -f "$MCP_JSON_PATH" ]]; then
|
||||||
|
echo "Check mcp/bridge"
|
||||||
|
pid="$(get-server-pid)"
|
||||||
|
if [[ -z "$pid" ]]; then
|
||||||
|
stop
|
||||||
|
echo "✗ server is not running"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Run the mcp tool
|
||||||
|
# @arg tool![`_choice_tool`] The tool name
|
||||||
|
# @arg json The json data
|
||||||
|
run@tool() {
|
||||||
|
if [[ -z "$argc_json" ]]; then
|
||||||
|
declaration="$(generate-declarations | jq --arg tool "$argc_tool" -r '.[] | select(.name == $tool)')"
|
||||||
|
if [[ -n "$declaration" ]]; then
|
||||||
|
_ask_json_data "$declaration"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$argc_json" ]]; then
|
||||||
|
_die "error: no JSON data"
|
||||||
|
fi
|
||||||
|
bash "$ROOT_DIR/scripts/run-mcp-tool.sh" "$argc_tool" "$argc_json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Show the logs
|
||||||
|
# @flag -f --follow Follow mode
|
||||||
|
logs() {
|
||||||
|
if [[ ! -f "$MCP_LOG_FILE" ]]; then
|
||||||
|
_die "error: not found log file at '$MCP_LOG_FILE'"
|
||||||
|
fi
|
||||||
|
if [[ -n "$argc_follow" ]]; then
|
||||||
|
tail -f "$MCP_LOG_FILE"
|
||||||
|
else
|
||||||
|
cat "$MCP_LOG_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Build tools to bin
|
||||||
|
build-bin() {
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
tools=( $(generate-declarations | jq -r '.[].name') )
|
||||||
|
for tool in "${tools[@]}"; do
|
||||||
|
if _is_win; then
|
||||||
|
bin_file="$BIN_DIR/$tool.cmd"
|
||||||
|
_build_win_shim > "$bin_file"
|
||||||
|
else
|
||||||
|
bin_file="$BIN_DIR/$tool"
|
||||||
|
ln -s -f "$ROOT_DIR/scripts/run-mcp-tool.sh" "$bin_file"
|
||||||
|
fi
|
||||||
|
echo "Build bin/$tool"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Merge mcp tools into functions.json
|
||||||
|
# @flag -S --save Save to functions.json
|
||||||
|
merge-functions() {
|
||||||
|
local tmpdir="$(mktemp -d)"
|
||||||
|
"$0" recovery-functions > "$tmpdir/1.json"
|
||||||
|
generate-declarations > "$tmpdir/2.json"
|
||||||
|
result="$(jq -s '.[0] + .[1]' "$tmpdir/1.json" "$tmpdir/2.json")"
|
||||||
|
if [[ -n "$argc_save" ]]; then
|
||||||
|
printf "%s" "$result" > "$FUNCTIONS_JSON_PATH"
|
||||||
|
else
|
||||||
|
printf "%s" "$result"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Unmerge mcp tools from functions.json
|
||||||
|
# @flag -S --save Save to functions.json
|
||||||
|
recovery-functions() {
|
||||||
|
functions="[]"
|
||||||
|
if [[ -f "$FUNCTIONS_JSON_PATH" ]]; then
|
||||||
|
functions="$(cat "$FUNCTIONS_JSON_PATH")"
|
||||||
|
fi
|
||||||
|
result="$(printf "%s" "$functions" | jq 'map(select(has("mcp") | not))')"
|
||||||
|
if [[ -n "$argc_save" ]]; then
|
||||||
|
printf "%s" "$result" > "$FUNCTIONS_JSON_PATH"
|
||||||
|
else
|
||||||
|
printf "%s" "$result"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Generate function declarations for the mcp tools
|
||||||
|
generate-declarations() {
|
||||||
|
pid="$(get-server-pid)"
|
||||||
|
if [[ -n "$pid" ]]; then
|
||||||
|
curl -sS http://localhost:$MCP_BRIDGE_PORT/tools
|
||||||
|
else
|
||||||
|
echo "[]"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Wait for the mcp bridge server to ready
|
||||||
|
wait-for-server() {
|
||||||
|
while true; do
|
||||||
|
if [[ "$(curl -fsS --max-time 5 http://localhost:$MCP_BRIDGE_PORT/health 2>&1)" == "OK" ]]; then
|
||||||
|
break;
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Get the server pid
|
||||||
|
get-server-pid() {
|
||||||
|
curl -fsS --max-time 5 http://localhost:$MCP_BRIDGE_PORT/pid 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
_ask_json_data() {
|
||||||
|
declaration="$1"
|
||||||
|
echo 'Missing the JSON data but here are its properties:'
|
||||||
|
echo "$declaration" | ./scripts/declarations-util.sh pretty-print | sed -n '2,$s/^/>/p'
|
||||||
|
echo 'Generate placeholder data:'
|
||||||
|
data="$(echo "$declaration" | _declarations_json_data)"
|
||||||
|
echo "> $data"
|
||||||
|
read -e -r -p 'JSON data (Press ENTER to use placeholder): ' res
|
||||||
|
if [[ -z "$res" ]]; then
|
||||||
|
argc_json="$data"
|
||||||
|
else
|
||||||
|
argc_json="$res"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_declarations_json_data() {
|
||||||
|
./scripts/declarations-util.sh generate-json | tail -n +2
|
||||||
|
}
|
||||||
|
|
||||||
|
_build_win_shim() {
|
||||||
|
run="\"$(argc --argc-shell-path)\" --noprofile --norc"
|
||||||
|
cat <<-EOF
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
set "bin_dir=%~dp0"
|
||||||
|
for %%i in ("%bin_dir:~0,-1%") do set "script_dir=%%~dpi"
|
||||||
|
set "script_name=%~n0"
|
||||||
|
|
||||||
|
$run "%script_dir%scripts\run-mcp-tool.sh" "%script_name%" %*
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
_is_win() {
|
||||||
|
if [[ "$OS" == "Windows_NT" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_choice_tool() {
|
||||||
|
generate-declarations | jq -r '.[].name'
|
||||||
|
}
|
||||||
|
|
||||||
|
_die() {
|
||||||
|
echo "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# See more details at https://github.com/sigoden/argc
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
172
.claude/llm-functions/scripts/run-agent.js
Executable file
172
.claude/llm-functions/scripts/run-agent.js
Executable file
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Usage: ./run-agent.js <agent-name> <agent-func> <agent-data>
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const { readFile, writeFile } = require("fs/promises");
|
||||||
|
const os = require("os");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const [agentName, agentFunc, rawData] = parseArgv("run-agent.js");
|
||||||
|
const agentData = parseRawData(rawData);
|
||||||
|
|
||||||
|
const rootDir = path.resolve(__dirname, "..");
|
||||||
|
await setupEnv(rootDir, agentName, agentFunc);
|
||||||
|
|
||||||
|
const agentToolsPath = path.resolve(rootDir, `agents/${agentName}/tools.js`);
|
||||||
|
await run(agentName, agentToolsPath, agentFunc, agentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgv(thisFileName) {
|
||||||
|
let agentName = process.argv[1];
|
||||||
|
let agentFunc = "";
|
||||||
|
let agentData = null;
|
||||||
|
|
||||||
|
if (agentName.endsWith(thisFileName)) {
|
||||||
|
agentName = process.argv[2];
|
||||||
|
agentFunc = process.argv[3];
|
||||||
|
agentData = process.argv[4];
|
||||||
|
} else {
|
||||||
|
agentName = path.basename(agentName);
|
||||||
|
agentFunc = process.argv[2];
|
||||||
|
agentData = process.argv[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentName && agentName.endsWith(".js")) {
|
||||||
|
agentName = agentName.slice(0, -3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agentData || !agentFunc || !agentName) {
|
||||||
|
console.log(`Usage: ./run-agent.js <agent-name> <agent-func> <agent-data>`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [agentName, agentFunc, agentData];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRawData(data) {
|
||||||
|
if (!data) {
|
||||||
|
throw new Error("No JSON data");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid JSON data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupEnv(rootDir, agentName, agentFunc) {
|
||||||
|
await loadEnv(path.resolve(rootDir, ".env"));
|
||||||
|
process.env["LLM_ROOT_DIR"] = rootDir;
|
||||||
|
process.env["LLM_AGENT_NAME"] = agentName;
|
||||||
|
process.env["LLM_AGENT_FUNC"] = agentFunc;
|
||||||
|
process.env["LLM_AGENT_ROOT_DIR"] = path.resolve(
|
||||||
|
rootDir,
|
||||||
|
"agents",
|
||||||
|
agentName,
|
||||||
|
);
|
||||||
|
process.env["LLM_AGENT_CACHE_DIR"] = path.resolve(
|
||||||
|
rootDir,
|
||||||
|
"cache",
|
||||||
|
agentName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEnv(filePath) {
|
||||||
|
let lines = [];
|
||||||
|
try {
|
||||||
|
const data = await readFile(filePath, "utf-8");
|
||||||
|
lines = data.split("\n");
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const envVars = new Map();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim().startsWith("#") || line.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [key, ...valueParts] = line.split("=");
|
||||||
|
const envName = key.trim();
|
||||||
|
|
||||||
|
if (!process.env[envName]) {
|
||||||
|
let envValue = valueParts.join("=").trim();
|
||||||
|
if ((envValue.startsWith('"') && envValue.endsWith('"')) || (envValue.startsWith("'") && envValue.endsWith("'"))) {
|
||||||
|
envValue = envValue.slice(1, -1);
|
||||||
|
}
|
||||||
|
envVars.set(envName, envValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [envName, envValue] of envVars.entries()) {
|
||||||
|
process.env[envName] = envValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(agentName, agentPath, agentFunc, agentData) {
|
||||||
|
if (os.platform() === "win32") {
|
||||||
|
agentPath = `file://${agentPath}`;
|
||||||
|
}
|
||||||
|
const mod = await import(agentPath);
|
||||||
|
if (!mod || !mod[agentFunc]) {
|
||||||
|
throw new Error(`Not module function '${agentFunc}' at '${agentPath}'`);
|
||||||
|
}
|
||||||
|
const value = await mod[agentFunc](agentData);
|
||||||
|
await returnToLLM(value);
|
||||||
|
await dumpResult(`${agentName}:${agentFunc}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function returnToLLM(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const write = async (value) => {
|
||||||
|
if (process.env["LLM_OUTPUT"]) {
|
||||||
|
await writeFile(process.env["LLM_OUTPUT"], value);
|
||||||
|
} else {
|
||||||
|
process.stdout.write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const type = typeof value;
|
||||||
|
if (type === "string" || type === "number" || type === "boolean") {
|
||||||
|
await write(value.toString());
|
||||||
|
} 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));
|
||||||
|
await write(valueStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dumpResult(name) {
|
||||||
|
if (!process.env["LLM_DUMP_RESULTS"] || !process.env["LLM_OUTPUT"] || !process.stdout.isTTY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let showResult = false;
|
||||||
|
try {
|
||||||
|
if (new RegExp(`\\b(${process.env["LLM_DUMP_RESULTS"]})\\b`).test(name)) {
|
||||||
|
showResult = true;
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
if (!showResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
try {
|
||||||
|
data = await readFile(process.env["LLM_OUTPUT"], "utf-8");
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.stdout.write(`\x1b[2m----------------------\n${data}\n----------------------\x1b[0m\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
153
.claude/llm-functions/scripts/run-agent.py
Executable file
153
.claude/llm-functions/scripts/run-agent.py
Executable file
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Usage: ./run-agent.py <agent-name> <agent-func> <agent-data>
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(agent_name, agent_func, raw_data) = parse_argv("run-agent.py")
|
||||||
|
agent_data = parse_raw_data(raw_data)
|
||||||
|
|
||||||
|
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
setup_env(root_dir, agent_name, agent_func)
|
||||||
|
|
||||||
|
agent_tools_path = os.path.join(root_dir, f"agents/{agent_name}/tools.py")
|
||||||
|
run(agent_name, agent_tools_path, agent_func, agent_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))
|
||||||
|
|
||||||
|
agent_name = argv[0]
|
||||||
|
agent_func = ""
|
||||||
|
agent_data = ""
|
||||||
|
|
||||||
|
if agent_name.endswith(this_file_name):
|
||||||
|
if len(sys.argv) > 3:
|
||||||
|
agent_name = sys.argv[1]
|
||||||
|
agent_func = sys.argv[2]
|
||||||
|
agent_data = sys.argv[3]
|
||||||
|
else:
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
agent_name = os.path.basename(agent_name)
|
||||||
|
agent_func = sys.argv[1]
|
||||||
|
agent_data = sys.argv[2]
|
||||||
|
|
||||||
|
if agent_name and agent_name.endswith(".py"):
|
||||||
|
agent_name = agent_name[:-3]
|
||||||
|
|
||||||
|
if (not agent_data) or (not agent_func) or (not agent_name):
|
||||||
|
print("Usage: ./run-agent.py <agent-name> <agent-func> <agent-data>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return agent_name, agent_func, agent_data
|
||||||
|
|
||||||
|
|
||||||
|
def setup_env(root_dir, agent_name, agent_func):
|
||||||
|
load_env(os.path.join(root_dir, ".env"))
|
||||||
|
os.environ["LLM_ROOT_DIR"] = root_dir
|
||||||
|
os.environ["LLM_AGENT_NAME"] = agent_name
|
||||||
|
os.environ["LLM_AGENT_FUNC"] = agent_func
|
||||||
|
os.environ["LLM_AGENT_ROOT_DIR"] = os.path.join(root_dir, "agents", agent_name)
|
||||||
|
os.environ["LLM_AGENT_CACHE_DIR"] = os.path.join(root_dir, "cache", agent_name)
|
||||||
|
|
||||||
|
|
||||||
|
def load_env(file_path):
|
||||||
|
try:
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
env_vars = {}
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("#") or not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key, *value_parts = line.split("=")
|
||||||
|
env_name = key.strip()
|
||||||
|
|
||||||
|
if env_name not in os.environ:
|
||||||
|
env_value = "=".join(value_parts).strip()
|
||||||
|
if (env_value.startswith('"') and env_value.endswith('"')) or (env_value.startswith("'") and env_value.endswith("'")):
|
||||||
|
env_value = env_value[1:-1]
|
||||||
|
env_vars[env_name] = env_value
|
||||||
|
|
||||||
|
os.environ.update(env_vars)
|
||||||
|
|
||||||
|
|
||||||
|
def run(agent_name, agent_path, agent_func, agent_data):
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
os.path.basename(agent_path), agent_path
|
||||||
|
)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
|
||||||
|
if not hasattr(mod, agent_func):
|
||||||
|
raise Exception(f"Not module function '{agent_func}' at '{agent_path}'")
|
||||||
|
|
||||||
|
value = getattr(mod, agent_func)(**agent_data)
|
||||||
|
return_to_llm(value)
|
||||||
|
dump_result(rf'{agent_name}:{agent_func}')
|
||||||
|
|
||||||
|
|
||||||
|
def return_to_llm(value):
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if "LLM_OUTPUT" in os.environ:
|
||||||
|
writer = open(os.environ["LLM_OUTPUT"], "w")
|
||||||
|
else:
|
||||||
|
writer = sys.stdout
|
||||||
|
|
||||||
|
value_type = type(value).__name__
|
||||||
|
if value_type in ("str", "int", "float", "bool"):
|
||||||
|
writer.write(str(value))
|
||||||
|
elif value_type == "dict" or value_type == "list":
|
||||||
|
value_str = json.dumps(value, indent=2)
|
||||||
|
assert value == json.loads(value_str)
|
||||||
|
writer.write(value_str)
|
||||||
|
|
||||||
|
|
||||||
|
def dump_result(name):
|
||||||
|
if (not os.getenv("LLM_DUMP_RESULTS")) or (not os.getenv("LLM_OUTPUT")) or (not os.isatty(1)):
|
||||||
|
return
|
||||||
|
|
||||||
|
show_result = False
|
||||||
|
try:
|
||||||
|
if re.search(rf'\b({os.environ["LLM_DUMP_RESULTS"]})\b', name):
|
||||||
|
show_result = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not show_result:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(os.environ["LLM_OUTPUT"], "r", encoding="utf-8") as f:
|
||||||
|
data = f.read()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\x1b[2m----------------------\n{data}\n----------------------\x1b[0m")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
125
.claude/llm-functions/scripts/run-agent.sh
Executable file
125
.claude/llm-functions/scripts/run-agent.sh
Executable file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Usage: ./run-agent.sh <agent-name> <agent-func> <agent-data>
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
main() {
|
||||||
|
root_dir="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)"
|
||||||
|
self_name=run-agent.sh
|
||||||
|
parse_argv "$@"
|
||||||
|
setup_env
|
||||||
|
tools_path="$root_dir/agents/$agent_name/tools.sh"
|
||||||
|
run
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_argv() {
|
||||||
|
if [[ "$0" == *"$self_name" ]]; then
|
||||||
|
agent_name="$1"
|
||||||
|
agent_func="$2"
|
||||||
|
agent_data="$3"
|
||||||
|
else
|
||||||
|
agent_name="$(basename "$0")"
|
||||||
|
agent_func="$1"
|
||||||
|
agent_data="$2"
|
||||||
|
fi
|
||||||
|
if [[ "$agent_name" == *.sh ]]; then
|
||||||
|
agent_name="${agent_name:0:$((${#agent_name}-3))}"
|
||||||
|
fi
|
||||||
|
if [[ -z "$agent_data" ]] || [[ -z "$agent_func" ]] || [[ -z "$agent_name" ]]; then
|
||||||
|
die "usage: ./run-agent.sh <agent-name> <agent-func> <agent-data>"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_env() {
|
||||||
|
load_env "$root_dir/.env"
|
||||||
|
export LLM_ROOT_DIR="$root_dir"
|
||||||
|
export LLM_AGENT_NAME="$agent_name"
|
||||||
|
export LLM_AGENT_FUNC="$agent_func"
|
||||||
|
export LLM_AGENT_ROOT_DIR="$LLM_ROOT_DIR/agents/$agent_name"
|
||||||
|
export LLM_AGENT_CACHE_DIR="$LLM_ROOT_DIR/cache/$agent_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
local env_file="$1" env_vars
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
if [[ "$key" == $'#'* ]] || [[ -z "$key" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -z "${!key+x}" ]]; then
|
||||||
|
env_vars="$env_vars $key=$value"
|
||||||
|
fi
|
||||||
|
done < <(cat "$env_file"; echo "")
|
||||||
|
if [[ -n "$env_vars" ]]; then
|
||||||
|
eval "export $env_vars"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
if [[ -z "$agent_data" ]]; then
|
||||||
|
die "error: no JSON data"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$OS" == "Windows_NT" ]]; then
|
||||||
|
set -o igncr
|
||||||
|
tools_path="$(cygpath -w "$tools_path")"
|
||||||
|
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq_script="$(cat <<-'EOF'
|
||||||
|
def escape_shell_word:
|
||||||
|
tostring
|
||||||
|
| gsub("'"; "'\"'\"'")
|
||||||
|
| gsub("\n"; "'$'\\n''")
|
||||||
|
| "'\(.)'";
|
||||||
|
def to_args:
|
||||||
|
to_entries | .[] |
|
||||||
|
(.key | split("_") | join("-")) as $key |
|
||||||
|
if .value | type == "array" then
|
||||||
|
.value | .[] | "--\($key) \(. | escape_shell_word)"
|
||||||
|
elif .value | type == "boolean" then
|
||||||
|
if .value then "--\($key)" else "" end
|
||||||
|
else
|
||||||
|
"--\($key) \(.value | escape_shell_word)"
|
||||||
|
end;
|
||||||
|
[ to_args ] | join(" ")
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
args="$(echo "$agent_data" | jq -r "$jq_script" 2>/dev/null)" || {
|
||||||
|
die "error: invalid JSON data"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$LLM_OUTPUT" ]]; then
|
||||||
|
is_temp_llm_output=1
|
||||||
|
export LLM_OUTPUT="$(mktemp)"
|
||||||
|
fi
|
||||||
|
eval "'$tools_path' '$agent_func' $args"
|
||||||
|
if [[ "$is_temp_llm_output" -eq 1 ]]; then
|
||||||
|
cat "$LLM_OUTPUT"
|
||||||
|
else
|
||||||
|
dump_result "${LLM_AGENT_NAME}:${LLM_AGENT_FUNC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_result() {
|
||||||
|
if [[ "$LLM_OUTPUT" == "/dev/stdout" ]] || [[ -z "$LLM_DUMP_RESULTS" ]] || [[ ! -t 1 ]]; then
|
||||||
|
return;
|
||||||
|
fi
|
||||||
|
if grep -q -w -E "$LLM_DUMP_RESULTS" <<<"$1"; then
|
||||||
|
cat <<EOF
|
||||||
|
$(echo -e "\e[2m")----------------------
|
||||||
|
$(cat "$LLM_OUTPUT")
|
||||||
|
----------------------$(echo -e "\e[0m")
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
||||||
107
.claude/llm-functions/scripts/run-mcp-tool.sh
Executable file
107
.claude/llm-functions/scripts/run-mcp-tool.sh
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Usage: ./run-mcp-tool.sh <tool-name> <tool-data>
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
main() {
|
||||||
|
root_dir="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)"
|
||||||
|
self_name=run-mcp-tool.sh
|
||||||
|
parse_argv "$@"
|
||||||
|
load_env "$root_dir/.env"
|
||||||
|
run
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_argv() {
|
||||||
|
if [[ "$0" == *"$self_name" ]]; then
|
||||||
|
tool_name="$1"
|
||||||
|
tool_data="$2"
|
||||||
|
else
|
||||||
|
tool_name="$(basename "$0")"
|
||||||
|
tool_data="$1"
|
||||||
|
fi
|
||||||
|
if [[ "$tool_name" == *.sh ]]; then
|
||||||
|
tool_name="${tool_name:0:$((${#tool_name}-3))}"
|
||||||
|
fi
|
||||||
|
if [[ -z "$tool_data" ]] || [[ -z "$tool_name" ]]; then
|
||||||
|
die "usage: ./run-tool.sh <tool-name> <tool-data>"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
local env_file="$1" env_vars
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
if [[ "$key" == $'#'* ]] || [[ -z "$key" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -z "${!key+x}" ]]; then
|
||||||
|
env_vars="$env_vars $key=$value"
|
||||||
|
fi
|
||||||
|
done < <(cat "$env_file"; echo "")
|
||||||
|
if [[ -n "$env_vars" ]]; then
|
||||||
|
eval "export $env_vars"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
if [[ -z "$tool_data" ]]; then
|
||||||
|
die "error: no JSON data"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$OS" == "Windows_NT" ]]; then
|
||||||
|
set -o igncr
|
||||||
|
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$LLM_OUTPUT" ]]; then
|
||||||
|
is_temp_llm_output=1
|
||||||
|
export LLM_OUTPUT="$(mktemp)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$LLM_MCP_SKIP_CONFIRM" ]]; then
|
||||||
|
if grep -q -w -E "$LLM_MCP_SKIP_CONFIRM" <<<"$tool_name"; then
|
||||||
|
skip_confirm=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -n "$LLM_MCP_NEED_CONFIRM" ]]; then
|
||||||
|
if grep -q -w -E "$LLM_MCP_NEED_CONFIRM" <<<"$tool_name"; then
|
||||||
|
skip_confirm=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -t 1 ]] && [[ "$skip_confirm" -ne 1 ]]; then
|
||||||
|
read -r -p "Are you sure you want to continue? [Y/n] " ans
|
||||||
|
if [[ "$ans" == "N" || "$ans" == "n" ]]; then
|
||||||
|
echo "error: canceled!" 2>&1
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -sS "http://localhost:${MCP_BRIDGE_PORT:-8808}/tools/$tool_name" \
|
||||||
|
-X POST \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
-d "$tool_data" > "$LLM_OUTPUT"
|
||||||
|
|
||||||
|
if [[ "$is_temp_llm_output" -eq 1 ]]; then
|
||||||
|
cat "$LLM_OUTPUT"
|
||||||
|
else
|
||||||
|
dump_result "$tool_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_result() {
|
||||||
|
if [[ "$LLM_OUTPUT" == "/dev/stdout" ]] || [[ -z "$LLM_DUMP_RESULTS" ]] || [[ ! -t 1 ]]; then
|
||||||
|
return;
|
||||||
|
fi
|
||||||
|
if grep -q -w -E "$LLM_DUMP_RESULTS" <<<"$1"; then
|
||||||
|
cat <<EOF
|
||||||
|
$(echo -e "\e[2m")----------------------
|
||||||
|
$(cat "$LLM_OUTPUT")
|
||||||
|
----------------------$(echo -e "\e[0m")
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
159
.claude/llm-functions/scripts/run-tool.js
Executable file
159
.claude/llm-functions/scripts/run-tool.js
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Usage: ./run-tool.js <tool-name> <tool-data>
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const { readFile, writeFile } = require("fs/promises");
|
||||||
|
const os = require("os");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const [toolName, rawData] = parseArgv("run-tool.js");
|
||||||
|
const toolData = parseRawData(rawData);
|
||||||
|
|
||||||
|
const rootDir = path.resolve(__dirname, "..");
|
||||||
|
await setupEnv(rootDir, toolName);
|
||||||
|
|
||||||
|
const toolPath = path.resolve(rootDir, `tools/${toolName}.js`);
|
||||||
|
await run(toolName, toolPath, "run", toolData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgv(thisFileName) {
|
||||||
|
let toolName = process.argv[1];
|
||||||
|
let toolData = null;
|
||||||
|
|
||||||
|
if (toolName.endsWith(thisFileName)) {
|
||||||
|
toolName = process.argv[2];
|
||||||
|
toolData = process.argv[3];
|
||||||
|
} else {
|
||||||
|
toolName = path.basename(toolName);
|
||||||
|
toolData = process.argv[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolName && toolName.endsWith(".js")) {
|
||||||
|
toolName = toolName.slice(0, -3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toolData || !toolName) {
|
||||||
|
console.log(`Usage: ./run-tools.js <tool-name> <tool-data>`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [toolName, toolData];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRawData(data) {
|
||||||
|
if (!data) {
|
||||||
|
throw new Error("No JSON data");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid JSON data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupEnv(rootDir, toolName) {
|
||||||
|
await loadEnv(path.resolve(rootDir, ".env"));
|
||||||
|
process.env["LLM_ROOT_DIR"] = rootDir;
|
||||||
|
process.env["LLM_TOOL_NAME"] = toolName;
|
||||||
|
process.env["LLM_TOOL_CACHE_DIR"] = path.resolve(rootDir, "cache", toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEnv(filePath) {
|
||||||
|
let lines = [];
|
||||||
|
try {
|
||||||
|
const data = await readFile(filePath, "utf-8");
|
||||||
|
lines = data.split("\n");
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const envVars = new Map();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim().startsWith("#") || line.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [key, ...valueParts] = line.split("=");
|
||||||
|
const envName = key.trim();
|
||||||
|
|
||||||
|
if (!process.env[envName]) {
|
||||||
|
let envValue = valueParts.join("=").trim();
|
||||||
|
if ((envValue.startsWith('"') && envValue.endsWith('"')) || (envValue.startsWith("'") && envValue.endsWith("'"))) {
|
||||||
|
envValue = envValue.slice(1, -1);
|
||||||
|
}
|
||||||
|
envVars.set(envName, envValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [envName, envValue] of envVars.entries()) {
|
||||||
|
process.env[envName] = envValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(toolName, toolPath, toolFunc, toolData) {
|
||||||
|
if (os.platform() === "win32") {
|
||||||
|
toolPath = `file://${toolPath}`;
|
||||||
|
}
|
||||||
|
const mod = await import(toolPath);
|
||||||
|
if (!mod || !mod[toolFunc]) {
|
||||||
|
throw new Error(`Not module function '${toolFunc}' at '${toolPath}'`);
|
||||||
|
}
|
||||||
|
const value = await mod[toolFunc](toolData);
|
||||||
|
await returnToLLM(value);
|
||||||
|
await dumpResult(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function returnToLLM(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const write = async (value) => {
|
||||||
|
if (process.env["LLM_OUTPUT"]) {
|
||||||
|
await writeFile(process.env["LLM_OUTPUT"], value);
|
||||||
|
} else {
|
||||||
|
process.stdout.write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const type = typeof value;
|
||||||
|
if (type === "string" || type === "number" || type === "boolean") {
|
||||||
|
await write(value.toString());
|
||||||
|
} 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));
|
||||||
|
await write(valueStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dumpResult(name) {
|
||||||
|
if (!process.env["LLM_DUMP_RESULTS"] || !process.env["LLM_OUTPUT"] || !process.stdout.isTTY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let showResult = false;
|
||||||
|
try {
|
||||||
|
if (new RegExp(`\\b(${process.env["LLM_DUMP_RESULTS"]})\\b`).test(name)) {
|
||||||
|
showResult = true;
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
if (!showResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
try {
|
||||||
|
data = await readFile(process.env["LLM_OUTPUT"], "utf-8");
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.stdout.write(`\x1b[2m----------------------\n${data}\n----------------------\x1b[0m\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
148
.claude/llm-functions/scripts/run-tool.py
Executable file
148
.claude/llm-functions/scripts/run-tool.py
Executable file
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Usage: ./run-tool.py <tool-name> <tool-data>
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(tool_name, raw_data) = parse_argv("run-tool.py")
|
||||||
|
tool_data = parse_raw_data(raw_data)
|
||||||
|
|
||||||
|
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
setup_env(root_dir, tool_name)
|
||||||
|
|
||||||
|
tool_path = os.path.join(root_dir, f"tools/{tool_name}.py")
|
||||||
|
run(tool_name, tool_path, "run", tool_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, 3 - len(sys.argv))
|
||||||
|
|
||||||
|
tool_name = argv[0]
|
||||||
|
tool_data = ""
|
||||||
|
|
||||||
|
if tool_name.endswith(this_file_name):
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
tool_name = argv[1]
|
||||||
|
tool_data = argv[2]
|
||||||
|
else:
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
tool_name = os.path.basename(tool_name)
|
||||||
|
tool_data = sys.argv[1]
|
||||||
|
|
||||||
|
if tool_name and tool_name.endswith(".py"):
|
||||||
|
tool_name = tool_name[:-3]
|
||||||
|
|
||||||
|
if (not tool_data) or (not tool_name):
|
||||||
|
print("Usage: ./run-tool.py <tool-name> <tool-data>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return tool_name, tool_data
|
||||||
|
|
||||||
|
|
||||||
|
def setup_env(root_dir, tool_name):
|
||||||
|
load_env(os.path.join(root_dir, ".env"))
|
||||||
|
os.environ["LLM_ROOT_DIR"] = root_dir
|
||||||
|
os.environ["LLM_TOOL_NAME"] = tool_name
|
||||||
|
os.environ["LLM_TOOL_CACHE_DIR"] = os.path.join(root_dir, "cache", tool_name)
|
||||||
|
|
||||||
|
|
||||||
|
def load_env(file_path):
|
||||||
|
try:
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
env_vars = {}
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("#") or not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key, *value_parts = line.split("=")
|
||||||
|
env_name = key.strip()
|
||||||
|
|
||||||
|
if env_name not in os.environ:
|
||||||
|
env_value = "=".join(value_parts).strip()
|
||||||
|
if (env_value.startswith('"') and env_value.endswith('"')) or (env_value.startswith("'") and env_value.endswith("'")):
|
||||||
|
env_value = env_value[1:-1]
|
||||||
|
env_vars[env_name] = env_value
|
||||||
|
|
||||||
|
os.environ.update(env_vars)
|
||||||
|
|
||||||
|
|
||||||
|
def run(tool_name, tool_path, tool_func, tool_data):
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
os.path.basename(tool_path), tool_path
|
||||||
|
)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
|
||||||
|
if not hasattr(mod, tool_func):
|
||||||
|
raise Exception(f"Not module function '{tool_func}' at '{tool_path}'")
|
||||||
|
|
||||||
|
value = getattr(mod, tool_func)(**tool_data)
|
||||||
|
return_to_llm(value)
|
||||||
|
dump_result(tool_name)
|
||||||
|
|
||||||
|
|
||||||
|
def return_to_llm(value):
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if "LLM_OUTPUT" in os.environ:
|
||||||
|
writer = open(os.environ["LLM_OUTPUT"], "w")
|
||||||
|
else:
|
||||||
|
writer = sys.stdout
|
||||||
|
|
||||||
|
value_type = type(value).__name__
|
||||||
|
if value_type in ("str", "int", "float", "bool"):
|
||||||
|
writer.write(str(value))
|
||||||
|
elif value_type == "dict" or value_type == "list":
|
||||||
|
value_str = json.dumps(value, indent=2)
|
||||||
|
assert value == json.loads(value_str)
|
||||||
|
writer.write(value_str)
|
||||||
|
|
||||||
|
|
||||||
|
def dump_result(name):
|
||||||
|
if (not os.getenv("LLM_DUMP_RESULTS")) or (not os.getenv("LLM_OUTPUT")) or (not os.isatty(1)):
|
||||||
|
return
|
||||||
|
|
||||||
|
show_result = False
|
||||||
|
try:
|
||||||
|
if re.search(rf'\b({os.environ["LLM_DUMP_RESULTS"]})\b', name):
|
||||||
|
show_result = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not show_result:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(os.environ["LLM_OUTPUT"], "r", encoding="utf-8") as f:
|
||||||
|
data = f.read()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\x1b[2m----------------------\n{data}\n----------------------\x1b[0m")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
119
.claude/llm-functions/scripts/run-tool.sh
Executable file
119
.claude/llm-functions/scripts/run-tool.sh
Executable file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Usage: ./run-tool.sh <tool-name> <tool-data>
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
main() {
|
||||||
|
root_dir="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)"
|
||||||
|
self_name=run-tool.sh
|
||||||
|
parse_argv "$@"
|
||||||
|
setup_env
|
||||||
|
tool_path="$root_dir/tools/$tool_name.sh"
|
||||||
|
run
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_argv() {
|
||||||
|
if [[ "$0" == *"$self_name" ]]; then
|
||||||
|
tool_name="$1"
|
||||||
|
tool_data="$2"
|
||||||
|
else
|
||||||
|
tool_name="$(basename "$0")"
|
||||||
|
tool_data="$1"
|
||||||
|
fi
|
||||||
|
if [[ "$tool_name" == *.sh ]]; then
|
||||||
|
tool_name="${tool_name:0:$((${#tool_name}-3))}"
|
||||||
|
fi
|
||||||
|
if [[ -z "$tool_data" ]] || [[ -z "$tool_name" ]]; then
|
||||||
|
die "usage: ./run-tool.sh <tool-name> <tool-data>"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_env() {
|
||||||
|
load_env "$root_dir/.env"
|
||||||
|
export LLM_ROOT_DIR="$root_dir"
|
||||||
|
export LLM_TOOL_NAME="$tool_name"
|
||||||
|
export LLM_TOOL_CACHE_DIR="$LLM_ROOT_DIR/cache/$tool_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
local env_file="$1" env_vars
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
if [[ "$key" == $'#'* ]] || [[ -z "$key" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -z "${!key+x}" ]]; then
|
||||||
|
env_vars="$env_vars $key=$value"
|
||||||
|
fi
|
||||||
|
done < <(cat "$env_file"; echo "")
|
||||||
|
if [[ -n "$env_vars" ]]; then
|
||||||
|
eval "export $env_vars"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
if [[ -z "$tool_data" ]]; then
|
||||||
|
die "error: no JSON data"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$OS" == "Windows_NT" ]]; then
|
||||||
|
set -o igncr
|
||||||
|
tool_path="$(cygpath -w "$tool_path")"
|
||||||
|
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq_script="$(cat <<-'EOF'
|
||||||
|
def escape_shell_word:
|
||||||
|
tostring
|
||||||
|
| gsub("'"; "'\"'\"'")
|
||||||
|
| gsub("\n"; "'$'\\n''")
|
||||||
|
| "'\(.)'";
|
||||||
|
def to_args:
|
||||||
|
to_entries | .[] |
|
||||||
|
(.key | split("_") | join("-")) as $key |
|
||||||
|
if .value | type == "array" then
|
||||||
|
.value | .[] | "--\($key) \(. | escape_shell_word)"
|
||||||
|
elif .value | type == "boolean" then
|
||||||
|
if .value then "--\($key)" else "" end
|
||||||
|
else
|
||||||
|
"--\($key) \(.value | escape_shell_word)"
|
||||||
|
end;
|
||||||
|
[ to_args ] | join(" ")
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
args="$(echo "$tool_data" | jq -r "$jq_script" 2>/dev/null)" || {
|
||||||
|
die "error: invalid JSON data"
|
||||||
|
}
|
||||||
|
if [[ -z "$LLM_OUTPUT" ]]; then
|
||||||
|
is_temp_llm_output=1
|
||||||
|
export LLM_OUTPUT="$(mktemp)"
|
||||||
|
fi
|
||||||
|
eval "'$tool_path' $args"
|
||||||
|
if [[ "$is_temp_llm_output" -eq 1 ]]; then
|
||||||
|
cat "$LLM_OUTPUT"
|
||||||
|
else
|
||||||
|
dump_result "$tool_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_result() {
|
||||||
|
if [[ "$LLM_OUTPUT" == "/dev/stdout" ]] || [[ -z "$LLM_DUMP_RESULTS" ]] || [[ ! -t 1 ]]; then
|
||||||
|
return;
|
||||||
|
fi
|
||||||
|
if grep -q -w -E "$LLM_DUMP_RESULTS" <<<"$1"; then
|
||||||
|
cat <<EOF
|
||||||
|
$(echo -e "\e[2m")----------------------
|
||||||
|
$(cat "$LLM_OUTPUT")
|
||||||
|
----------------------$(echo -e "\e[0m")
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
29
.claude/llm-functions/tools/demo_js.js
Normal file
29
.claude/llm-functions/tools/demo_js.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Demonstrate how to create a tool using Javascript and how to use comments.
|
||||||
|
* @typedef {Object} Args
|
||||||
|
* @property {string} string - Define a required string property
|
||||||
|
* @property {'foo'|'bar'} string_enum - Define a required string property with enum
|
||||||
|
* @property {string} [string_optional] - Define a optional string property
|
||||||
|
* @property {boolean} boolean - Define a required boolean property
|
||||||
|
* @property {Integer} integer - Define a required integer property
|
||||||
|
* @property {number} number - Define a required number property
|
||||||
|
* @property {string[]} array - Define a required string array property
|
||||||
|
* @property {string[]} [array_optional] - Define a optional string array property
|
||||||
|
* @param {Args} args
|
||||||
|
*/
|
||||||
|
exports.run = function (args) {
|
||||||
|
let output = `string: ${args.string}
|
||||||
|
string_enum: ${args.string_enum}
|
||||||
|
string_optional: ${args.string_optional}
|
||||||
|
boolean: ${args.boolean}
|
||||||
|
integer: ${args.integer}
|
||||||
|
number: ${args.number}
|
||||||
|
array: ${args.array}
|
||||||
|
array_optional: ${args.array_optional}`;
|
||||||
|
for (const [key, value] of Object.entries(process.env)) {
|
||||||
|
if (key.startsWith("LLM_")) {
|
||||||
|
output = `${output}\n${key}: ${value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
38
.claude/llm-functions/tools/demo_py.py
Normal file
38
.claude/llm-functions/tools/demo_py.py
Normal file
@@ -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,
|
||||||
|
):
|
||||||
|
"""Demonstrate 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 a optional string property
|
||||||
|
array_optional: Define a 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
|
||||||
30
.claude/llm-functions/tools/demo_sh.sh
Executable file
30
.claude/llm-functions/tools/demo_sh.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/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! <INT> Define a required integer property
|
||||||
|
# @option --number! <NUM> Define a required number property
|
||||||
|
# @option --array+ <VALUE> Define a required string array property
|
||||||
|
# @option --array-optional* Define a optional string array property
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cat <<EOF >> "$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
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
16
.claude/llm-functions/tools/execute_command.sh
Executable file
16
.claude/llm-functions/tools/execute_command.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
"$ROOT_DIR/utils/guard_operation.sh"
|
||||||
|
eval "$argc_command" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
22
.claude/llm-functions/tools/execute_js_code.js
Normal file
22
.claude/llm-functions/tools/execute_js_code.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Execute the javascript code in node.js.
|
||||||
|
* @typedef {Object} Args
|
||||||
|
* @property {string} code - Javascript code to execute, such as `console.log("hello world")`
|
||||||
|
* @param {Args} args
|
||||||
|
*/
|
||||||
|
exports.run = function ({ code }) {
|
||||||
|
let output = "";
|
||||||
|
const oldStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||||
|
process.stdout.write = (chunk, _encoding, callback) => {
|
||||||
|
output += chunk;
|
||||||
|
if (callback) callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = eval(code);
|
||||||
|
if (value !== undefined) {
|
||||||
|
output += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write = oldStdoutWrite;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
33
.claude/llm-functions/tools/execute_py_code.py
Normal file
33
.claude/llm-functions/tools/execute_py_code.py
Normal file
@@ -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)
|
||||||
21
.claude/llm-functions/tools/execute_sql_code.sh
Executable file
21
.claude/llm-functions/tools/execute_sql_code.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Execute the 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
|
||||||
|
|
||||||
|
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if ! grep -qi '^select' <<<"$argc_code"; then
|
||||||
|
"$ROOT_DIR/utils/guard_operation.sh"
|
||||||
|
fi
|
||||||
|
usql -c "$argc_code" "$USQL_DSN" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
19
.claude/llm-functions/tools/fetch_url_via_curl.sh
Executable file
19
.claude/llm-functions/tools/fetch_url_via_curl.sh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
18
.claude/llm-functions/tools/fetch_url_via_jina.sh
Executable file
18
.claude/llm-functions/tools/fetch_url_via_jina.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Extract the content from a given URL.
|
||||||
|
# @option --url! The URL to scrape.
|
||||||
|
|
||||||
|
# @env JINA_API_KEY The api key
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
15
.claude/llm-functions/tools/fs_cat.sh
Executable file
15
.claude/llm-functions/tools/fs_cat.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/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() {
|
||||||
|
cat "$argc_path" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
14
.claude/llm-functions/tools/fs_ls.sh
Executable file
14
.claude/llm-functions/tools/fs_ls.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/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() {
|
||||||
|
ls -1 "$argc_path" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
15
.claude/llm-functions/tools/fs_mkdir.sh
Executable file
15
.claude/llm-functions/tools/fs_mkdir.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/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() {
|
||||||
|
mkdir -p "$argc_path"
|
||||||
|
echo "Directory created: $argc_path" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
36
.claude/llm-functions/tools/fs_patch.sh
Executable file
36
.claude/llm-functions/tools/fs_patch.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/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 to
|
||||||
|
# @option --contents! The patch to apply to the file
|
||||||
|
#
|
||||||
|
# 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}")
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [ ! -f "$argc_path" ]; then
|
||||||
|
echo "Not found file: $argc_path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
new_contents="$(awk -f "$ROOT_DIR/utils/patch.awk" "$argc_path" <(printf "%s" "$argc_contents"))"
|
||||||
|
printf "%s" "$new_contents" | git diff --no-index "$argc_path" - || true
|
||||||
|
"$ROOT_DIR/utils/guard_operation.sh" "Apply changes?"
|
||||||
|
printf "%s" "$new_contents" > "$argc_path"
|
||||||
|
|
||||||
|
echo "The patch applied to: $argc_path" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
20
.claude/llm-functions/tools/fs_rm.sh
Executable file
20
.claude/llm-functions/tools/fs_rm.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ -f "$argc_path" ]]; then
|
||||||
|
"$ROOT_DIR/utils/guard_path.sh" "$argc_path" "Remove '$argc_path'?"
|
||||||
|
rm -rf "$argc_path"
|
||||||
|
fi
|
||||||
|
echo "Path removed: $argc_path" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
25
.claude/llm-functions/tools/fs_write.sh
Executable file
25
.claude/llm-functions/tools/fs_write.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ -f "$argc_path" ]]; then
|
||||||
|
printf "%s" "$argc_contents" | git diff --no-index "$argc_path" - || true
|
||||||
|
"$ROOT_DIR/utils/guard_operation.sh" "Apply changes?"
|
||||||
|
else
|
||||||
|
"$ROOT_DIR/utils/guard_path.sh" "$argc_path" "Write '$argc_path'?"
|
||||||
|
mkdir -p "$(dirname "$argc_path")"
|
||||||
|
fi
|
||||||
|
printf "%s" "$argc_contents" > "$argc_path"
|
||||||
|
echo "The contents written to: $argc_path" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
12
.claude/llm-functions/tools/get_current_time.sh
Executable file
12
.claude/llm-functions/tools/get_current_time.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Get the current time.
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
main() {
|
||||||
|
date >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
14
.claude/llm-functions/tools/get_current_weather.sh
Executable file
14
.claude/llm-functions/tools/get_current_weather.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/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"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
17
.claude/llm-functions/tools/search_arxiv.sh
Executable file
17
.claude/llm-functions/tools/search_arxiv.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Search arXiv for a query and return the top papers.
|
||||||
|
|
||||||
|
# @option --query! The query to search for.
|
||||||
|
|
||||||
|
# @env ARXIV_MAX_RESULTS=3 The max results to return.
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
main() {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
28
.claude/llm-functions/tools/search_wikipedia.sh
Executable file
28
.claude/llm-functions/tools/search_wikipedia.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Search Wikipedia for a query.
|
||||||
|
# Uses it to get detailed information about a public figure, interpretation of a complex scientific concept or in-depth connectivity of a significant historical event,.
|
||||||
|
|
||||||
|
# @option --query! The query to search for.
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
19
.claude/llm-functions/tools/search_wolframalpha.sh
Executable file
19
.claude/llm-functions/tools/search_wolframalpha.sh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Get an answer to a question using Wolfram Alpha. Input should the query in English.
|
||||||
|
# Use it to answer user questions that require computation, detailed facts, data analysis, or complex queries.
|
||||||
|
|
||||||
|
# @option --query! The query to search for.
|
||||||
|
|
||||||
|
# @env WOLFRAM_API_ID! The api id
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
31
.claude/llm-functions/tools/send_mail.sh
Executable file
31
.claude/llm-functions/tools/send_mail.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Send a 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 name
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
46
.claude/llm-functions/tools/send_twilio.sh
Executable file
46
.claude/llm-functions/tools/send_twilio.sh
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Send SMS or Twilio Messaging Channels messages using Twilio API.
|
||||||
|
# @option --to-number! The recipient's phone number. Prefix 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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
35
.claude/llm-functions/tools/web_search_aichat.sh
Executable file
35
.claude/llm-functions/tools/web_search_aichat.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/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 query to search for.
|
||||||
|
|
||||||
|
# @meta require-tools aichat
|
||||||
|
|
||||||
|
# @env WEB_SEARCH_MODEL! The model for web-searching.
|
||||||
|
#
|
||||||
|
# supported aichat models:
|
||||||
|
# - gemini:gemini-2.0-*
|
||||||
|
# - vertexai:gemini-*
|
||||||
|
# - perplexity:*
|
||||||
|
# - ernie:*
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
main() {
|
||||||
|
client="${WEB_SEARCH_MODEL%%:*}"
|
||||||
|
if [[ "$client" == "gemini" ]]; then
|
||||||
|
export AICHAT_PATCH_GEMINI_CHAT_COMPLETIONS='{".*":{"body":{"tools":[{"google_search":{}}]}}}'
|
||||||
|
elif [[ "$client" == "vertexai" ]]; then
|
||||||
|
export AICHAT_PATCH_VERTEXAI_CHAT_COMPLETIONS='{
|
||||||
|
"gemini-1.5-.*":{"body":{"tools":[{"googleSearchRetrieval":{}}]}},
|
||||||
|
"gemini-2.0-.*":{"body":{"tools":[{"google_search":{}}]}}
|
||||||
|
}'
|
||||||
|
elif [[ "$client" == "ernie" ]]; then
|
||||||
|
export AICHAT_PATCH_ERNIE_CHAT_COMPLETIONS='{".*":{"body":{"web_search":{"enable":true}}}}'
|
||||||
|
fi
|
||||||
|
aichat -m "$WEB_SEARCH_MODEL" "$argc_query" >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
33
.claude/llm-functions/tools/web_search_perplexity.sh
Executable file
33
.claude/llm-functions/tools/web_search_perplexity.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Perform a web search using 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 query to search for.
|
||||||
|
|
||||||
|
# @env PERPLEXITY_API_KEY! The api key
|
||||||
|
# @env PERPLEXITY_WEB_SEARCH_MODEL=llama-3.1-sonar-small-128k-online The LLM model for web search
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
24
.claude/llm-functions/tools/web_search_tavily.sh
Executable file
24
.claude/llm-functions/tools/web_search_tavily.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# @describe Perform a web search using 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 query to search for.
|
||||||
|
|
||||||
|
# @env TAVILY_API_KEY! The api key
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout The output path The output path
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
eval "$(argc --argc-eval "$0" "$@")"
|
||||||
16
.claude/llm-functions/utils/guard_operation.sh
Executable file
16
.claude/llm-functions/utils/guard_operation.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Guard an operation with a confirmation prompt.
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
confirmation_prompt="${1:-"Are you sure you want to continue?"}"
|
||||||
|
read -r -p "$confirmation_prompt [Y/n] " ans
|
||||||
|
if [[ "$ans" == "N" || "$ans" == "n" ]]; then
|
||||||
|
echo "error: aborted!" 2>&1
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
60
.claude/llm-functions/utils/guard_path.sh
Executable file
60
.claude/llm-functions/utils/guard_path.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ "$#" -ne 2 ]]; then
|
||||||
|
echo "Usage: guard_path.sh <path> <confirmation_prompt>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
path="$(_to_realpath "$1")"
|
||||||
|
confirmation_prompt="$2"
|
||||||
|
if [[ ! "$path" == "$(pwd)"* ]]; then
|
||||||
|
read -r -p "$confirmation_prompt [Y/n] " ans
|
||||||
|
if [[ "$ans" == "N" || "$ans" == "n" ]]; then
|
||||||
|
echo "error: aborted!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_to_realpath() {
|
||||||
|
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 "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
112
.claude/llm-functions/utils/patch.awk
Executable file
112
.claude/llm-functions/utils/patch.awk
Executable file
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/awk -f
|
||||||
|
|
||||||
|
# Apply a diff file to an original
|
||||||
|
# Usage: awk -f patch.awk target-file patch-file
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# inspectHunks()
|
||||||
|
|
||||||
|
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
.claude/logs/subagent-decisions.log
Normal file
1
.claude/logs/subagent-decisions.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2026-03-27T08:01:50Z agent=unknown
|
||||||
@@ -1,34 +1,56 @@
|
|||||||
#!/usr/bin/env -S .claude/venv/bin/python3
|
#!/usr/bin/env -S .claude/venv/bin/python3
|
||||||
"""
|
"""
|
||||||
MCP server stdio exposant un outil `qwen3_task`.
|
MCP server stdio exposant un outil `qwen3_task`.
|
||||||
|
Lance aichat en mode serveur au démarrage et gère la boucle tool_calls.
|
||||||
|
Les outils disponibles pour Qwen3 sont chargés depuis llm-functions/functions.json.
|
||||||
Appelé par Claude Code via : claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py
|
Appelé par Claude Code via : claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
SERVER = "http://localhost"
|
QWEN3_MODEL = "LMStudio:qwen/qwen3-coder-next"
|
||||||
PORT = 8888 # 1248
|
MAX_TOOL_ITERATIONS = 10
|
||||||
LMSTUDIO_URL = f"{SERVER}:{PORT}/v1/chat/completions"
|
|
||||||
QWEN3_MODEL = "qwen/qwen3-coder-next"
|
|
||||||
|
|
||||||
TOOLS = [
|
# Répertoire llm-functions relatif à ce script
|
||||||
|
_HERE = Path(__file__).parent
|
||||||
|
_LLM_FUNCTIONS_DIR = _HERE.parent.parent / "llm-functions"
|
||||||
|
_FUNCTIONS_JSON = _LLM_FUNCTIONS_DIR / "functions.json"
|
||||||
|
_BIN_DIR = _LLM_FUNCTIONS_DIR / "bin"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_qwen3_tools() -> list[dict]:
|
||||||
|
"""Charge les outils depuis llm-functions/functions.json et les wrappe au format OpenAI."""
|
||||||
|
if not _FUNCTIONS_JSON.exists():
|
||||||
|
print(f"WARN: {_FUNCTIONS_JSON} introuvable — aucun outil disponible",
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
return []
|
||||||
|
raw = json.loads(_FUNCTIONS_JSON.read_text())
|
||||||
|
return [{"type": "function", "function": tool} for tool in raw]
|
||||||
|
|
||||||
|
|
||||||
|
# Outil MCP exposé à Claude Code
|
||||||
|
MCP_TOOLS = [
|
||||||
{
|
{
|
||||||
"name": "qwen3_task",
|
"name": "qwen3_task",
|
||||||
"description": "Délègue une tâche de codage atomique à Qwen3-Coder via LM Studio.",
|
"description": "Délègue une tâche de codage à Qwen3-Coder via aichat. Qwen3 dispose d'outils filesystem, shell et web (llm-functions).",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"task": {
|
"task": {"type": "string", "description": "Description précise de la tâche"},
|
||||||
"type": "string",
|
|
||||||
"description": "Description précise de la tâche",
|
|
||||||
},
|
|
||||||
"files": {
|
"files": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"type": "string"},
|
"items": {"type": "string"},
|
||||||
"description": "Chemins des fichiers concernés",
|
"description": "Fichiers à mettre en contexte initial (optionnel)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["task"],
|
"required": ["task"],
|
||||||
@@ -37,40 +59,148 @@ TOOLS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def send(obj: dict):
|
# --- Gestion du processus aichat ---
|
||||||
print(json.dumps(obj), flush=True)
|
|
||||||
|
|
||||||
|
_aichat_proc: subprocess.Popen | None = None
|
||||||
|
_aichat_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _find_free_port() -> int:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.bind(("127.0.0.1", 0))
|
||||||
|
return s.getsockname()[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _start_aichat() -> str:
|
||||||
|
global _aichat_proc, _aichat_url
|
||||||
|
|
||||||
|
if not shutil.which("aichat"):
|
||||||
|
raise RuntimeError("aichat introuvable dans le PATH")
|
||||||
|
|
||||||
|
port = _find_free_port()
|
||||||
|
address = f"127.0.0.1:{port}"
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
if _LLM_FUNCTIONS_DIR.exists():
|
||||||
|
env["AICHAT_FUNCTIONS_DIR"] = str(_LLM_FUNCTIONS_DIR)
|
||||||
|
|
||||||
|
_aichat_proc = subprocess.Popen(
|
||||||
|
["aichat", "--serve", address],
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
atexit.register(_stop_aichat)
|
||||||
|
|
||||||
|
url = f"http://{address}"
|
||||||
|
for _ in range(30):
|
||||||
|
try:
|
||||||
|
requests.get(f"{url}/v1/models", timeout=1)
|
||||||
|
_aichat_url = url
|
||||||
|
return url
|
||||||
|
except requests.RequestException:
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
_aichat_proc.kill()
|
||||||
|
raise RuntimeError(f"aichat n'a pas démarré sur {address}")
|
||||||
|
|
||||||
|
|
||||||
|
def _stop_aichat():
|
||||||
|
if _aichat_proc and _aichat_proc.poll() is None:
|
||||||
|
_aichat_proc.terminate()
|
||||||
|
try:
|
||||||
|
_aichat_proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
_aichat_proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Exécution des outils via llm-functions/bin/ ---
|
||||||
|
|
||||||
|
def _execute_tool(name: str, args: dict) -> str:
|
||||||
|
"""Exécute un outil llm-functions via son binaire dans bin/.
|
||||||
|
Les binaires attendent le JSON des arguments comme premier argument positionnel.
|
||||||
|
"""
|
||||||
|
bin_path = _BIN_DIR / name
|
||||||
|
if not bin_path.exists():
|
||||||
|
return f"ERROR: outil '{name}' introuvable dans {_BIN_DIR}"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(bin_path), json.dumps(args)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
return output or f"(exit code {result.returncode})"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return f"ERROR: timeout lors de l'exécution de '{name}'"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Boucle tool_calls → Qwen3 ---
|
||||||
|
|
||||||
def call_qwen3(task: str, files: list[str]) -> str:
|
def call_qwen3(task: str, files: list[str]) -> str:
|
||||||
|
if _aichat_url is None:
|
||||||
|
raise RuntimeError("aichat non démarré")
|
||||||
|
|
||||||
|
qwen3_tools = _load_qwen3_tools()
|
||||||
|
|
||||||
context = ""
|
context = ""
|
||||||
for path in files:
|
for path in files:
|
||||||
try:
|
try:
|
||||||
with open(path) as f:
|
with open(path, encoding="utf-8") as f:
|
||||||
context += f"--- {path} ---\n{f.read()}\n\n"
|
context += f"--- {path} ---\n{f.read()}\n\n"
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
context += f"--- {path} --- ERREUR: {e}\n\n"
|
context += f"--- {path} --- ERREUR: {e}\n\n"
|
||||||
|
|
||||||
prompt = f"""Tu es un assistant de codage. Effectue la tâche suivante de manière précise.
|
user_content = f"TÂCHE: {task}"
|
||||||
|
if context:
|
||||||
|
user_content += f"\n\nFICHIERS:\n{context}"
|
||||||
|
|
||||||
TÂCHE: {task}
|
messages = [{"role": "user", "content": user_content}]
|
||||||
|
payload: dict = {
|
||||||
|
"model": QWEN3_MODEL,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": 0.2,
|
||||||
|
"max_tokens": 4096,
|
||||||
|
}
|
||||||
|
if qwen3_tools:
|
||||||
|
payload["tools"] = qwen3_tools
|
||||||
|
|
||||||
FICHIERS:
|
for _ in range(MAX_TOOL_ITERATIONS):
|
||||||
{context}
|
resp = requests.post(
|
||||||
|
f"{_aichat_url}/v1/chat/completions",
|
||||||
|
json=payload,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
choice = data["choices"][0]
|
||||||
|
message = choice["message"]
|
||||||
|
payload["messages"].append(message)
|
||||||
|
|
||||||
Retourne uniquement le code modifié ou généré, sans explication.
|
if choice["finish_reason"] != "tool_calls":
|
||||||
"""
|
return message.get("content") or ""
|
||||||
resp = requests.post(
|
|
||||||
LMSTUDIO_URL,
|
for tc in message.get("tool_calls", []):
|
||||||
json={
|
fn = tc["function"]
|
||||||
"model": QWEN3_MODEL,
|
args = json.loads(fn["arguments"])
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
result = _execute_tool(fn["name"], args)
|
||||||
"temperature": 0.2,
|
payload["messages"].append({
|
||||||
"max_tokens": 4096,
|
"role": "tool",
|
||||||
},
|
"tool_call_id": tc["id"],
|
||||||
timeout=120,
|
"content": result,
|
||||||
)
|
})
|
||||||
resp.raise_for_status()
|
|
||||||
return resp.json()["choices"][0]["message"]["content"]
|
return "ERROR: nombre maximum d'itérations tool_calls atteint"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Protocole MCP stdio ---
|
||||||
|
|
||||||
|
def send(obj: dict):
|
||||||
|
print(json.dumps(obj), flush=True)
|
||||||
|
|
||||||
|
|
||||||
def handle(req: dict):
|
def handle(req: dict):
|
||||||
@@ -78,21 +208,19 @@ def handle(req: dict):
|
|||||||
req_id = req.get("id")
|
req_id = req.get("id")
|
||||||
|
|
||||||
if method == "initialize":
|
if method == "initialize":
|
||||||
send(
|
send({
|
||||||
{
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": req_id,
|
||||||
"id": req_id,
|
"result": {
|
||||||
"result": {
|
"protocolVersion": "2024-11-05",
|
||||||
"protocolVersion": "2024-11-05",
|
"capabilities": {"tools": {}},
|
||||||
"capabilities": {"tools": {}},
|
"serverInfo": {"name": "qwen3-mcp", "version": "3.0.0"},
|
||||||
"serverInfo": {"name": "qwen3-mcp", "version": "1.0.0"},
|
},
|
||||||
},
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
elif method == "notifications/initialized":
|
elif method == "notifications/initialized":
|
||||||
pass # notification, pas de réponse
|
pass
|
||||||
elif method == "tools/list":
|
elif method == "tools/list":
|
||||||
send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}})
|
send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": MCP_TOOLS}})
|
||||||
elif method == "tools/call":
|
elif method == "tools/call":
|
||||||
params = req.get("params", {})
|
params = req.get("params", {})
|
||||||
name = params.get("name", "")
|
name = params.get("name", "")
|
||||||
@@ -100,42 +228,31 @@ def handle(req: dict):
|
|||||||
if name == "qwen3_task":
|
if name == "qwen3_task":
|
||||||
try:
|
try:
|
||||||
text = call_qwen3(args["task"], args.get("files", []))
|
text = call_qwen3(args["task"], args.get("files", []))
|
||||||
send(
|
send({"jsonrpc": "2.0", "id": req_id, "result": {
|
||||||
{
|
"content": [{"type": "text", "text": text}]
|
||||||
"jsonrpc": "2.0",
|
}})
|
||||||
"id": req_id,
|
|
||||||
"result": {"content": [{"type": "text", "text": text}]},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
send(
|
send({"jsonrpc": "2.0", "id": req_id, "result": {
|
||||||
{
|
"content": [{"type": "text", "text": f"ERROR: {e}"}],
|
||||||
"jsonrpc": "2.0",
|
"isError": True,
|
||||||
"id": req_id,
|
}})
|
||||||
"result": {
|
|
||||||
"content": [{"type": "text", "text": f"ERROR: {e}"}],
|
|
||||||
"isError": True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
send(
|
send({"jsonrpc": "2.0", "id": req_id,
|
||||||
{
|
"error": {"code": -32601, "message": f"Outil inconnu : {name}"}})
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": req_id,
|
|
||||||
"error": {"code": -32601, "message": f"Outil inconnu : {name}"},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif req_id is not None:
|
elif req_id is not None:
|
||||||
send(
|
send({"jsonrpc": "2.0", "id": req_id,
|
||||||
{
|
"error": {"code": -32601, "message": f"Méthode inconnue : {method}"}})
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": req_id,
|
|
||||||
"error": {"code": -32601, "message": f"Méthode inconnue : {method}"},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- Démarrage ---
|
||||||
|
|
||||||
|
try:
|
||||||
|
_start_aichat()
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": f"Impossible de démarrer aichat : {e}"}),
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
for line in sys.stdin:
|
for line in sys.stdin:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line:
|
if not line:
|
||||||
|
|||||||
@@ -1,32 +1,43 @@
|
|||||||
---
|
---
|
||||||
name: delegation-rules
|
name: delegation-rules
|
||||||
description: Règles pour décider quand et comment déléguer à qwen3-worker ou qwen3 MCP
|
description: Règles pour décider quand et comment déléguer à qwen3-worker via le MCP qwen3_task
|
||||||
---
|
---
|
||||||
|
|
||||||
# Règles de Délégation à Qwen3
|
# Règles de Délégation à Qwen3
|
||||||
|
|
||||||
## Critères de délégation (TOUS requis)
|
Qwen3-Coder opère via le MCP tool `qwen3_task`. Il dispose d'outils filesystem, shell
|
||||||
- ≤ 3 fichiers source concernés
|
et web (llm-functions) — il peut lire/écrire des fichiers, exécuter des commandes,
|
||||||
- Tâche atomique, sans dépendance à résoudre
|
faire des recherches web et exécuter du code de façon autonome.
|
||||||
- Pas de modification de signature publique (trait, interface exportée)
|
|
||||||
- Contexte de fichier estimé < 8 000 tokens
|
## Critères de délégation (TOUS requis)
|
||||||
|
|
||||||
|
- Tâche bien définie, objectif clair
|
||||||
|
- Pas de modification de signature publique (trait, interface exportée)
|
||||||
|
- Pas de refactoring cross-module nécessitant une vision globale de l'architecture
|
||||||
|
|
||||||
|
## Tâches éligibles
|
||||||
|
|
||||||
## Tâches typiques éligibles
|
|
||||||
- Génération de tests unitaires pour une fonction donnée
|
- Génération de tests unitaires pour une fonction donnée
|
||||||
- Ajout de documentation (doc comments Rust, godoc Go)
|
- Ajout de documentation (doc comments Rust, godoc Go)
|
||||||
- Scaffolding de stubs à partir d'une interface
|
- Scaffolding de stubs à partir d'une interface
|
||||||
- Conversion de types internes simples
|
- Conversion de types internes simples
|
||||||
- Reformatage / réorganisation d'imports
|
- Reformatage / réorganisation d'imports
|
||||||
|
- Recherche d'information sur le web ou dans une URL
|
||||||
|
- Exécution et vérification d'un script Python
|
||||||
|
- Création ou modification de fichiers de configuration
|
||||||
|
|
||||||
## Procédure
|
## Procédure
|
||||||
|
|
||||||
1. Évalue les critères ci-dessus.
|
1. Évalue les critères ci-dessus.
|
||||||
2. Si éligible → utilise le subagent `qwen3-worker` en précisant la tâche et les fichiers.
|
2. Si éligible → utilise le subagent `qwen3-worker` avec une description précise de la tâche.
|
||||||
3. Attends le résumé de sortie (STATUS / FILES_MODIFIED / LINT).
|
Ne pas pré-charger les fichiers : Qwen3 les lit lui-même si nécessaire.
|
||||||
4. Si STATUS=failure → prends en charge toi-même.
|
3. Attends le résumé (STATUS / FILES_MODIFIED / SUMMARY).
|
||||||
5. Si STATUS=partial → corrige les points restants directement.
|
4. Si `STATUS: failure` → prends en charge toi-même.
|
||||||
|
5. Si `STATUS: partial` → corrige les points restants directement.
|
||||||
|
|
||||||
## Ne jamais déléguer
|
## Ne jamais déléguer
|
||||||
- Refactoring impliquant > 3 fichiers
|
|
||||||
|
- Refactoring impliquant une vision cross-module
|
||||||
- Changement d'architecture ou de design pattern
|
- Changement d'architecture ou de design pattern
|
||||||
- Debugging avec stacktrace à analyser
|
- Debugging avec stacktrace à analyser sur plusieurs fichiers
|
||||||
- Tâches nécessitant une recherche web ou du contexte externe
|
- Tâches nécessitant le contexte de la conversation en cours
|
||||||
|
|||||||
46
.claude/update_crazyclaude.sh
Executable file
46
.claude/update_crazyclaude.sh
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Met à jour .claude depuis CrazyClaude et llm-functions depuis upstream,
|
||||||
|
# puis reconstruit les outils aichat.
|
||||||
|
#
|
||||||
|
# Usage : bash .claude/update_crazyclaude.sh [--dry-run]
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CRAZY_CLAUDE_URL="https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git"
|
||||||
|
LLM_FUNCTIONS_URL="https://github.com/sigoden/llm-functions.git"
|
||||||
|
CLAUDE_PREFIX=".claude"
|
||||||
|
LLM_PREFIX=".claude/llm-functions"
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
[[ "${1:-}" == "--dry-run" ]] && dry_run=1
|
||||||
|
|
||||||
|
run() {
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] $*"
|
||||||
|
else
|
||||||
|
echo "» $*"
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require() {
|
||||||
|
command -v "$1" &>/dev/null || { echo "ERREUR : '$1' introuvable dans le PATH" >&2; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
require git
|
||||||
|
require argc
|
||||||
|
require jq
|
||||||
|
|
||||||
|
echo "=== Mise à jour de .claude (CrazyClaude) ==="
|
||||||
|
run git subtree pull --prefix "$CLAUDE_PREFIX" "$CRAZY_CLAUDE_URL" main --squash
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Mise à jour de .claude/llm-functions ==="
|
||||||
|
run git subtree pull --prefix "$LLM_PREFIX" "$LLM_FUNCTIONS_URL" main --squash
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Reconstruction des outils llm-functions ==="
|
||||||
|
run argc build --arcgfile "$LLM_PREFIX/Argcfile.sh"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "OK — outils reconstruits dans $LLM_PREFIX/bin/"
|
||||||
151
README.md
151
README.md
@@ -1,17 +1,38 @@
|
|||||||
# CrazyClaude
|
# CrazyClaude
|
||||||
|
|
||||||
Configuration Claude Code pour orchestrer des tâches de développement Rust/Go en déléguant les tâches atomiques à **Qwen3-Coder** via LM Studio.
|
Configuration Claude Code pour orchestrer des tâches de développement en déléguant les tâches atomiques à **Qwen3-Coder** via [aichat](https://github.com/sigoden/aichat).
|
||||||
|
|
||||||
Ce dépôt est conçu pour être intégré comme sous-répertoire `.claude/` dans n'importe quel projet. Il fournit agents, hooks, skills et un serveur MCP prêts à l'emploi.
|
Ce dépôt est conçu pour être intégré comme sous-répertoire `.claude/` dans n'importe quel projet. Il fournit agents, hooks, skills, un serveur MCP et une collection d'outils prêts à l'emploi.
|
||||||
|
|
||||||
Pour le détail de l'architecture, voir [`rapport-orchestration-claude-code.md`](./rapport-orchestration-claude-code.md).
|
Pour le détail de l'architecture, voir [`rapport-orchestration-claude-code.md`](./rapport-orchestration-claude-code.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Claude Code (orchestrateur)
|
||||||
|
│
|
||||||
|
└── MCP qwen3 (stdio)
|
||||||
|
│
|
||||||
|
└── aichat --serve (port dynamique)
|
||||||
|
│
|
||||||
|
└── LM Studio → Qwen3-Coder-Next
|
||||||
|
+ outils llm-functions
|
||||||
|
(fs, shell, web, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le serveur MCP lance aichat au démarrage sur un port libre. Qwen3 dispose d'outils filesystem, shell et web fournis par [llm-functions](https://github.com/sigoden/llm-functions) — sans aucune logique d'outil dans le serveur lui-même.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Prérequis
|
## Prérequis
|
||||||
|
|
||||||
- [Claude Code](https://claude.ai/code) installé (`claude` disponible dans le PATH)
|
- [Claude Code](https://claude.ai/code) (`claude` dans le PATH)
|
||||||
- [LM Studio](https://lmstudio.ai/) avec le modèle `qwen/qwen3-coder-next` chargé, écoutant sur `http://localhost:1248`
|
- [aichat](https://github.com/sigoden/aichat) (`aichat` dans le PATH)
|
||||||
|
- [LM Studio](https://lmstudio.ai/) avec le modèle `qwen/qwen3-coder-next` chargé et accessible depuis aichat
|
||||||
|
- [argc](https://github.com/sigoden/argc) — runner de scripts pour llm-functions
|
||||||
|
- [jq](https://jqlang.github.io/jq/) — requis par llm-functions
|
||||||
- Python 3.11+ (pour le serveur MCP)
|
- Python 3.11+ (pour le serveur MCP)
|
||||||
- Outils selon votre stack : `cargo` + `clippy` (Rust), `golangci-lint` (Go)
|
- Outils selon votre stack : `cargo` + `clippy` (Rust), `golangci-lint` (Go)
|
||||||
|
|
||||||
@@ -28,18 +49,14 @@ Avec **Jujutsu** :
|
|||||||
jj git clone https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git .claude-crazy
|
jj git clone https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git .claude-crazy
|
||||||
```
|
```
|
||||||
|
|
||||||
Puis déplacez ou liez le contenu :
|
Puis copiez le contenu :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Copier le répertoire .claude dans votre projet
|
|
||||||
cp -r .claude-crazy/.claude ./.claude
|
cp -r .claude-crazy/.claude ./.claude
|
||||||
|
cp -r .claude-crazy/llm-functions ./.claude/llm-functions # si présent dans le clone
|
||||||
# Ou, si vous préférez garder une référence au dépôt source :
|
|
||||||
# utilisez jj workspace add pour ajouter un workspace dédié
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> Si vous utilisez git en sous-jacent avec jj (`jj git init --colocate`),
|
> Avec jj colocalisé git, vous pouvez aussi utiliser subtree :
|
||||||
> vous pouvez aussi ajouter ce dépôt comme subtree :
|
|
||||||
>
|
>
|
||||||
> ```sh
|
> ```sh
|
||||||
> git subtree add --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
|
> git subtree add --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
|
||||||
@@ -53,24 +70,61 @@ python3 -m venv venv
|
|||||||
venv/bin/pip install -r mcp/qwen3-mcp/requirements.txt
|
venv/bin/pip install -r mcp/qwen3-mcp/requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Enregistrer le serveur MCP
|
### 3. Installer et construire llm-functions
|
||||||
|
|
||||||
|
Si `llm-functions/` n'est pas déjà présent dans `.claude/` :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/sigoden/llm-functions .claude/llm-functions
|
||||||
|
```
|
||||||
|
|
||||||
|
Choisissez les outils à activer en éditant `.claude/llm-functions/tools.txt`, puis construisez :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd .claude/llm-functions
|
||||||
|
argc build
|
||||||
|
```
|
||||||
|
|
||||||
|
Les outils activés par défaut :
|
||||||
|
|
||||||
|
```
|
||||||
|
fs_cat.sh
|
||||||
|
fs_ls.sh
|
||||||
|
fs_mkdir.sh
|
||||||
|
fs_write.sh
|
||||||
|
fs_patch.sh
|
||||||
|
execute_command.sh
|
||||||
|
fetch_url_via_curl.sh
|
||||||
|
web_search_aichat.sh
|
||||||
|
execute_py_code.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Configurer aichat
|
||||||
|
|
||||||
|
Vérifiez que aichat peut accéder à LM Studio et que le modèle `qwen/qwen3-coder-next` est disponible :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
aichat --list-models | grep qwen
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Enregistrer le serveur MCP
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py
|
claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Vérifier que la connexion est établie :
|
Vérifier :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
claude mcp list
|
claude mcp list
|
||||||
# qwen3: ... - ✓ Connected
|
# qwen3: ... - ✓ Connected
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Adapter CLAUDE.md à votre projet
|
### 6. Adapter CLAUDE.md à votre projet
|
||||||
|
|
||||||
Éditez `.claude/CLAUDE.md` : mettez à jour la section `## Stack` et les heuristiques de délégation selon vos besoins.
|
Éditez `.claude/CLAUDE.md` : mettez à jour la section `## Stack` et les heuristiques de délégation.
|
||||||
|
|
||||||
### 5. Rendre les hooks exécutables
|
### 7. Rendre les hooks exécutables
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
chmod +x .claude/hooks/*.sh
|
chmod +x .claude/hooks/*.sh
|
||||||
@@ -85,7 +139,7 @@ chmod +x .claude/hooks/*.sh
|
|||||||
├── CLAUDE.md # contexte projet + heuristiques de délégation
|
├── CLAUDE.md # contexte projet + heuristiques de délégation
|
||||||
├── settings.json # hooks et permissions
|
├── settings.json # hooks et permissions
|
||||||
├── agents/
|
├── agents/
|
||||||
│ ├── qwen3-worker.md # délégation vers LM Studio (Qwen3)
|
│ ├── qwen3-worker.md # délégation vers Qwen3 via MCP
|
||||||
│ ├── code-reviewer.md # révision code (Claude Haiku)
|
│ ├── code-reviewer.md # révision code (Claude Haiku)
|
||||||
│ └── task-planner.md # planification complexe (Claude Opus)
|
│ └── task-planner.md # planification complexe (Claude Opus)
|
||||||
├── skills/
|
├── skills/
|
||||||
@@ -97,66 +151,81 @@ chmod +x .claude/hooks/*.sh
|
|||||||
│ └── pre-bash-guard.sh # blocage des commandes dangereuses
|
│ └── pre-bash-guard.sh # blocage des commandes dangereuses
|
||||||
├── mcp/
|
├── mcp/
|
||||||
│ └── qwen3-mcp/
|
│ └── qwen3-mcp/
|
||||||
│ ├── server.py # serveur MCP stdio (interface LM Studio)
|
│ ├── server.py # serveur MCP stdio : lance aichat + gère tool_calls
|
||||||
│ ├── agent_lm.py # agent loop CLI autonome
|
│ ├── agent_lm.py # agent loop CLI autonome (usage direct sans MCP)
|
||||||
│ └── requirements.txt
|
│ └── requirements.txt
|
||||||
|
├── llm-functions/ # collection d'outils pour Qwen3 (sous-repo)
|
||||||
|
│ ├── tools.txt # outils activés
|
||||||
|
│ ├── functions.json # déclarations générées (lues par server.py)
|
||||||
|
│ ├── bin/ # binaires générés par argc build
|
||||||
|
│ └── tools/ # scripts sources (.sh, .py, .js)
|
||||||
├── logs/ # produits par les hooks (ignorés par jj/git)
|
├── logs/ # produits par les hooks (ignorés par jj/git)
|
||||||
└── venv/ # virtualenv Python (ignoré par jj/git)
|
└── venv/ # virtualenv Python (ignoré par jj/git)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration LM Studio
|
## Outils disponibles pour Qwen3
|
||||||
|
|
||||||
Le serveur MCP se connecte sur `http://localhost:1248` avec le modèle `qwen/qwen3-coder-next`.
|
Les outils sont définis dans `.claude/llm-functions/tools.txt` et buildés avec `argc build`.
|
||||||
|
|
||||||
Pour modifier ces valeurs, éditez `.claude/mcp/qwen3-mcp/server.py` :
|
| Outil | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `fs_cat` | Lit un fichier |
|
||||||
|
| `fs_ls` | Liste un répertoire |
|
||||||
|
| `fs_mkdir` | Crée un répertoire |
|
||||||
|
| `fs_write` | Écrit/crée un fichier |
|
||||||
|
| `fs_patch` | Modifie un fichier par patch |
|
||||||
|
| `execute_command` | Exécute une commande shell |
|
||||||
|
| `fetch_url_via_curl` | Requête HTTP GET |
|
||||||
|
| `web_search_aichat` | Recherche web |
|
||||||
|
| `execute_py_code` | Exécute du code Python |
|
||||||
|
|
||||||
|
Pour ajouter un outil : éditez `tools.txt` et relancez `argc build`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration du modèle
|
||||||
|
|
||||||
|
Le modèle Qwen3 est configuré dans `.claude/mcp/qwen3-mcp/server.py` :
|
||||||
|
|
||||||
```python
|
```python
|
||||||
LMSTUDIO_URL = "http://localhost:1248/v1/chat/completions"
|
QWEN3_MODEL = "LMStudio:qwen/qwen3-coder-next"
|
||||||
QWEN3_MODEL = "qwen/qwen3-coder-next"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Le format `Provider:model-id` suit la convention aichat. Adaptez selon votre installation LM Studio.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fichiers à ignorer (jj / git)
|
## Fichiers à ignorer (jj / git)
|
||||||
|
|
||||||
Ajoutez ces entrées dans votre fichier d'ignore :
|
|
||||||
|
|
||||||
```
|
```
|
||||||
.claude/venv/
|
.claude/venv/
|
||||||
.claude/logs/
|
.claude/logs/
|
||||||
|
.claude/llm-functions/bin/
|
||||||
|
.claude/llm-functions/cache/
|
||||||
```
|
```
|
||||||
|
|
||||||
Avec Jujutsu :
|
Avec Jujutsu :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
echo ".claude/venv/" >> .gitignore
|
printf ".claude/venv/\n.claude/logs/\n.claude/llm-functions/bin/\n.claude/llm-functions/cache/\n" >> .gitignore
|
||||||
echo ".claude/logs/" >> .gitignore
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mise à jour
|
## Mise à jour
|
||||||
|
|
||||||
Avec Jujutsu (workflow colocalisé git) :
|
Avec Jujutsu colocalisé (`jj git init --colocate`), la mise à jour est gérée côté git :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git subtree pull --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
|
git subtree pull --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
|
||||||
```
|
```
|
||||||
|
|
||||||
Ou si vous avez cloné séparément, tirez les changements puis recopiez :
|
jj voit le commit résultant comme un commit ordinaire dans son graphe.
|
||||||
|
|
||||||
|
Après mise à jour, reconstruire llm-functions si `tools.txt` a changé :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd .claude-crazy && jj git fetch && jj new main
|
cd .claude/llm-functions && argc build
|
||||||
cp -r .claude/* ../.claude/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Outils necessaire
|
|
||||||
|
|
||||||
### aichat
|
|
||||||
|
|
||||||
### jq
|
|
||||||
|
|
||||||
### argc
|
|
||||||
|
|||||||
Reference in New Issue
Block a user