feat: improved first-time run experience and included a templated configuration file that now has comments like the config.example.yaml so users don't have to go to the repo to see all the knobs
CI / All (ubuntu-latest) (push) Failing after 29s
CI / All (macos-latest) (push) Canceled after 0s
CI / All (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-28 15:15:10 -06:00
parent cebc32f70a
commit 176a81412a
6 changed files with 688 additions and 317 deletions
+279
View File
@@ -0,0 +1,279 @@
# Coyote configuration. Generated by the first-run wizard.
# Every setting is listed with its effective value and a short description.
# For richer examples of each section, see
# https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml
# ---- LLM ----
__MODEL_BLOCK__
temperature: null # Set default temperature parameter (0, 1)
top_p: null # Set default top-p parameter, with a range of (0, 1) or (0, 2) depending on the model
# ---- Behavior ----
dry_run: false # Display the messages that would be sent to the LLM without actually sending them
stream: true # Controls whether to use the stream-style APIs when querying for completions from LLM clients
save: true # Indicates whether to persist the conversation to messages.md for posterity
keybindings: emacs # Choose keybinding style (emacs, vi)
editor: null # Specifies the editor used to edit the input buffer or session. (e.g. vim, emacs, nano, hx). Defaults to $EDITOR
wrap: auto # Controls text wrapping (no, auto, <max-width>)
wrap_code: false # Enables or disables the wrapping of code blocks
# ---- Vault ----
# See the [Vault documentation](https://github.com/Dark-Alex-17/coyote/wiki/Vault) for more information on the Coyote vault.
#
# The secrets_provider tells Coyote where to read and write secrets referenced via {{SECRET_NAME}} syntax.
#
# Shorthand: set vault_password_file to enable the local provider with that password
# file (it cannot be a secret template).
#
# Explicit: set secrets_provider to one of the supported types below. When secrets_provider is set,
# vault_password_file is ignored. Note: secrets_provider itself cannot use secret template syntax.
# The vault must be initialized before any secrets can be resolved.
#
# Local (same as the shorthand above):
# secrets_provider:
# type: local
# password_file: ~/.coyote_password
#
# AWS Secrets Manager (requires an authenticated AWS CLI; see `aws sso login` or `aws configure`):
# secrets_provider:
# type: aws_secrets_manager
# aws_profile: default
# aws_region: us-east-1
#
# GCP Secret Manager (requires `gcloud auth application-default login`):
# secrets_provider:
# type: gcp_secret_manager
# gcp_project_id: my-project-id
#
# Azure Key Vault (requires `az login`):
# secrets_provider:
# type: azure_key_vault
# vault_name: my-vault-name
#
# gopass (requires the `gopass` CLI to be installed and initialized):
# secrets_provider:
# type: gopass
# store: my-store # Optional; omit to use the default store
#
# 1Password (requires the `op` CLI to be installed and signed in via `op signin`):
# secrets_provider:
# type: one_password
# vault: Production # Optional; omit to use the default vault
# account: my.1password.com # Optional; omit to use the default account
__SECRETS_BLOCK__
# ---- Function Calling ----
# See the [Tools documentation](https://github.com/Dark-Alex-17/coyote/wiki/Tools) for more details
function_calling_support: true # Enables or disables function calling (globally)
mapping_tools: {} # Alias for a tool or toolset
# Example:
# mapping_tools:
# fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep'
enabled_tools: null # Which tools to enable by default.
# Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything.
# Example (list form):
# enabled_tools:
# - fs
# - web_search_coyote
# Example (comma-separated form):
# enabled_tools: fs,web_search_coyote
visible_tools: null # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools').
# Null/missing = all tools in the global tools dir are visible; [] = none;
# an explicit list makes only those tools visible.
# Example:
# visible_tools:
# - execute_command.sh
# - fs_cat.sh
# - fs_ls.sh
# ---- Skills ----
# Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation.
# See the [Skills documentation](https://github.com/Dark-Alex-17/coyote/wiki/Skills) for more details.
skills_enabled: true # Master switch. Set to false to hide all skill management tools from the model.
# Skills also require `function_calling_support: true` above to work at all.
enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible.
# Accepts either a YAML list or a comma-separated string.
# Example (list form):
# enabled_skills:
# - git-master
# - ai-slop-remover
# Example (comma-separated form):
# enabled_skills: git-master,ai-slop-remover
visible_skills: null # The universe of skills allowed to be enabled in any context. null = all installed.
# Example:
# visible_skills:
# - ai-slop-remover
# - code-review
# - git-master
# ---- Macros ----
# Macros are Coyote's custom commands: named sequences of REPL commands and prompts, invoked directly by name
# (a macro file named `review.yaml` runs as `.review [args]`; built-in commands always win a name collision).
# Workspace-local macros in `.coyote/macros/` shadow same-named global macros (skip them with --no-workspace-macros).
# See the [Macros documentation](https://github.com/Dark-Alex-17/coyote/wiki/Macros) for more details.
enabled_macros: null # Which macros are invocable by default (no role/agent/session active). null = all visible.
# An empty list means NO macros are invocable. Accepts either a YAML list or a
# comma-separated string. Roles, agents, and sessions may define their own
# `enabled_macros`; the most specific active one wins (session > agent > role > global).
# Example (list form):
# enabled_macros:
# - generate-commit-message
# Example (comma-separated form):
# enabled_macros: generate-commit-message,review
# ---- MCP Servers ----
# See the [MCP Servers documentation](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) for more details
mcp_server_support: true # Enables or disables MCP servers (globally)
mapping_mcp_servers: {} # Alias for an MCP server or set of servers
# Example:
# mapping_mcp_servers:
# git: github,gitmcp
enabled_mcp_servers: null # Which MCP servers to enable by default.
# Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything.
# Example (list form):
# enabled_mcp_servers:
# - github
# - slack
# Example (comma-separated form):
# enabled_mcp_servers: github,slack,ddg-search
mcp_tools: null # Per-server MCP tool allowlists (glob patterns: * and ? supported).
# Tools that match no pattern are hidden from the model as if they
# don't exist. Stacks with the other allowlist layers (mcp.json
# `allowedTools`, role, agent, session, skill, graph node). Every
# configured layer must allow a tool, so layers only ever narrow.
# An empty list blocks all of a server's tools.
# Example:
# mcp_tools:
# github:
# - get_*
# - list_*
# slack: []
# ---- Auto-Continue (Todo System) ----
# The auto-continue system provides built-in task tracking for improved reliability.
# When enabled, the model can create todo lists and the system will automatically
# prompt it to continue when incomplete tasks remain.
# See the [Todo System documentation](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System) for more information
auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false)
max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10)
inject_todo_instructions: true # Inject default todo usage instructions into the system prompt (default: true)
continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default
inject_skill_instructions: true # Inject a short hint pointing the model at `skill__list` when skills are enabled
# in this context. Only injected if `function_calling_support`, `skills_enabled`, and the
# effective enabled skill set is non-empty (default: true)
skill_instructions: null # Custom text used for the skill hint when injected. If null, uses built-in default
# ---- Prelude ----
repl_prelude: null # Set a default session or role for REPL mode to use (e.g. role:<name>, session:<name>, <session>:<role>)
cmd_prelude: null # Set a default session or role for CMD mode to use (e.g. role:<name>, session:<name>, <session>:<role>)
agent_session: null # Set a session to use when starting an agent (e.g. temp, default)
# ---- Session ----
# See the [Session documentation](https://github.com/Dark-Alex-17/coyote/wiki/Sessions) for more information
save_session: null # Controls the persistence of the session. If true, auto save; if false, don't auto-save; if null, ask the user what to do
compression_threshold: 4000 # Compress the session when the token count reaches or exceeds this threshold
compression_keep_last: 0 # Number of most-recent messages to keep visible after compression (0 = compress all messages)
summarization_prompt: null # The text prompt used for creating a concise summary of session messages. If null, uses built-in default
summary_context_prompt: null # The text prompt used for including the summary of the entire session as context to the model. If null, uses built-in default
max_tool_result_chars: null # Cap on tool result characters forwarded to the model per call (null = no cap)
max_concurrent_jobs: null # Max background jobs (`job__*` tools) running at once per context (null = 5; 0 disables background jobs entirely)
# ---- Memory ----
# See the [Memory documentation](https://github.com/Dark-Alex-17/coyote/wiki/Memory) for more information.
# Memory is opt-in by workspace presence (`.coyote/memory/MEMORY.md`) and global
# presence (`<config_dir>/memory/MEMORY.md`). Set `memory: false` to disable
# even when memory files exist. The cascade is: agent > session > role > app.
# Bootstrap with `coyote --init-memory [global|workspace]` to create the marker file
# the LLM needs before it will write any memory.
memory: null # null = enabled when memory exists on disk; true = force on; false = force off
memory_cap_with_tools: null # Char cap for injected memory when function calling is available (null = 6000).
# Only MEMORY.md indexes are injected; the LLM uses memory__read to fetch drill files.
memory_cap_without_tools: null # Char cap when function calling is unavailable (null = 12000).
# Indexes plus drill file bodies are injected up to this cap.
# ---- Workspace Instructions ----
# Human-curated project instructions injected read-only into the system prompt, in full.
# Coyote walks up from the current directory and injects the first match from the file
# chain below (per directory, in order). Scaffold with `coyote --init-instructions`.
# Disable per-invocation with --no-workspace-instructions, or override the chain with
# repeatable --workspace-instructions-file flags.
workspace_instructions: null # null/true = inject when an instructions file exists; false = never inject
workspace_instructions_files: null # File name chain to search, in priority order.
# Default: [COYOTE.md, AGENTS.md, CLAUDE.md, GEMINI.md]
# Set to a custom list to reorder or drop fallbacks, e.g.:
# workspace_instructions_files: [COYOTE.md]
# ---- RAG ----
# See the [RAG Docs](https://github.com/Dark-Alex-17/coyote/wiki/RAG) for more details.
rag_embedding_model: null # Specifies the embedding model used for context retrieval
rag_reranker_model: null # Specifies the reranker model used for sorting retrieved documents; Coyote uses Reciprocal Rank Fusion by default
rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries
rag_chunk_size: null # Defines the size of chunks for document processing in characters
rag_chunk_overlap: null # Defines the overlap between chunks
rag_template: null # Defines the query structure using variables like __CONTEXT__, __SOURCES__, and __INPUT__
# to tailor searches to specific needs. If null, uses built-in default
rag_extractor_model: null # LLM model for graph-based entity/relationship extraction; when set, enables a graph RAG signal alongside vector and BM25
rag_extractor_prompt: null # Custom extraction prompt template; must contain __CHUNK__ placeholder; defaults to built-in prompt when null
rag_graph_hops: 1 # Number of hops to expand from matched entities at query time (0 = seed nodes only; 1 = direct neighbors; increase for denser graphs)
# Define document loaders to control how RAG and `.file`/`--file` load files of specific formats.
document_loaders: {}
# You can add custom loaders using the following syntax:
# <file-extension>: <command-to-load-the-file>
# Note: Use `$1` for input file and `$2` for output file. If `$2` is omitted, use stdout as output.
# Examples:
# document_loaders:
# pdf: 'pdftotext $1 -' # https://poppler.freedesktop.org
# docx: 'pandoc --to plain $1' # https://pandoc.org
# jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}"' # Requires a Jina API key in the Coyote vault
# ---- Appearance ----
highlight: true # Controls syntax highlighting
raw_markdown: false # When true, render markdown as raw text with syntax highlighting only. When false (default), transforms markdown syntax (headings, bold, lists, etc.) into styled terminal output
theme: null # null = the built-in dark theme; set to `light` for the built-in light theme.
# Custom themes: place a `dark.tmTheme` or `light.tmTheme` file in the Coyote config
# directory and it is used in place of the corresponding built-in.
# ---- REPL Prompt ----
# Custom REPL left/right prompts; see the [REPL Prompt Documentation](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt) for more information
left_prompt: null # If null, uses the built-in default:
# '{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} '
right_prompt: null # If null, uses the built-in default:
# '{color.cyan}{?reasoning_effort [{reasoning_effort}] }{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}'
# ---- Miscellaneous ----
user_agent: null # Set User-Agent HTTP header, use `auto` for coyote/<current-version>
save_shell_history: true # Whether to save shell execution command to the history file
sync_models_url: null # URL to sync model changes from. If null, uses the built-in default:
# https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/models.yaml
# ---- Clients ----
# See the [Clients documentation](https://github.com/Dark-Alex-17/coyote/wiki/Clients) for more details
#
# All clients have the following configuration:
# - type: xxxx
# name: xxxx # Only use it to distinguish clients with the same client type. Optional
# models:
# - name: xxxx # Chat model
# max_input_tokens: 100000
# supports_vision: true
# supports_function_calling: true
# - name: xxxx # Embedding model
# type: embedding
# default_chunk_size: 1500
# max_batch_size: 100
# - name: xxxx # Reranker model
# type: reranker
# patch: # Patch API calls
# chat_completions: # API type; Possible values: chat_completions, embeddings, and rerank
# <regex>: # The regex to match model names, e.g. '.*' 'gpt-4o' 'gpt-4o|gpt-4-.*'
# url: '' # Patch request URL
# body: # Patch request body
# <json>
# headers: # Patch request headers
# <key>: <value>
# extra:
# proxy: socks5://127.0.0.1:1080 # Set proxy
# connect_timeout: 10 # Set timeout in seconds for connect to api
# read_timeout: 300 # Set timeout in seconds for a read stall (no bytes received); 0 disables (default: 300)
__CLIENTS_BLOCK__
+181 -195
View File
@@ -1,39 +1,40 @@
# ---- LLM ---- # ---- LLM ----
model: openai:gpt-4o # Specify the LLM to use model: openai:gpt-4o # Specify the LLM to use
temperature: null # Set default temperature parameter (0, 1) temperature: null # Set default temperature parameter (0, 1)
top_p: null # Set default top-p parameter, with a range of (0, 1) or (0, 2) depending on the model top_p: null # Set default top-p parameter, with a range of (0, 1) or (0, 2) depending on the model
reasoning_effort: reasoning_effort: null # Reasoning effort level for models that support it (e.g. low, medium, high).
null # Reasoning effort level for models that support it (e.g. low, medium, high). # Only valid when the active model declares reasoning_levels. See the Clients docs.
# Only valid when the active model declares reasoning_levels. See the Clients docs.
# ---- Behavior ---- # ---- Behavior ----
stream: true # Controls whether to use the stream-style APIs when querying for completions from LLM clients. stream: true # Controls whether to use the stream-style APIs when querying for completions from LLM clients.
save: true # Indicates whether to persist the conversation to messages.md for posterity save: true # Indicates whether to persist the conversation to messages.md for posterity
keybindings: emacs # Choose keybinding style (emacs, vi) keybindings: emacs # Choose keybinding style (emacs, vi)
editor: null # Specifies the editor used to edit the input buffer or session. (e.g. vim, emacs, nano, hx). Defaults to $EDITOR editor: null # Specifies the editor used to edit the input buffer or session. (e.g. vim, emacs, nano, hx). Defaults to $EDITOR
wrap: no # Controls text wrapping (no, auto, <max-width>) wrap: no # Controls text wrapping (no, auto, <max-width>)
wrap_code: false # Enables or disables the wrapping of code blocks wrap_code: false # Enables or disables the wrapping of code blocks
# ---- Prelude ---- # ---- Prelude ----
repl_prelude: null # Set a default session or role for REPL mode to use (e.g. role:<name>, session:<name>, <session>:<role>) repl_prelude: null # Set a default session or role for REPL mode to use (e.g. role:<name>, session:<name>, <session>:<role>)
cmd_prelude: null # Set a default session or role for CMD mode to use (e.g. role:<name>, session:<name>, <session>:<role>) cmd_prelude: null # Set a default session or role for CMD mode to use (e.g. role:<name>, session:<name>, <session>:<role>)
agent_session: null # Set a session to use when starting an agent (e.g. temp, default) agent_session: null # Set a session to use when starting an agent (e.g. temp, default)
# ---- Appearance ---- # ---- Appearance ----
highlight: true # Controls syntax highlighting highlight: true # Controls syntax highlighting
raw_markdown: false # When true, render markdown as raw text with syntax highlighting only. When false (default), transforms markdown syntax (headings, bold, lists, etc.) into styled terminal output raw_markdown: false # When true, render markdown as raw text with syntax highlighting only. When false (default), transforms markdown syntax (headings, bold, lists, etc.) into styled terminal output
light_theme: false # Activates a light color theme when true. env: COYOTE_LIGHT_THEME light_theme: false # Activates a light color theme when true. env: COYOTE_LIGHT_THEME
# ---- Miscellaneous ---- # ---- Miscellaneous ----
user_agent: null # Set User-Agent HTTP header, use `auto` for coyote/<current-version> user_agent: null # Set User-Agent HTTP header, use `auto` for coyote/<current-version>
save_shell_history: true # Whether to save shell execution command to the history file save_shell_history: true # Whether to save shell execution command to the history file
sync_models_url: > # URL to sync model changes from sync_models_url: > # URL to sync model changes from
https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/models.yaml https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/models.yaml
# ---- REPL Prompt ---- # ---- REPL Prompt ----
# Custom REPL left/right prompts; see the [REPL Prompt Documentation](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt) for more information # Custom REPL left/right prompts; see the [REPL Prompt Documentation](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt) for more information
left_prompt: '{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} ' left_prompt:
right_prompt: '{color.cyan}{?reasoning_effort [{reasoning_effort}] }{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}' '{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} '
right_prompt:
'{color.cyan}{?reasoning_effort [{reasoning_effort}] }{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}'
# ---- Vault ---- # ---- Vault ----
# See the [Vault documentation](https://github.com/Dark-Alex-17/coyote/wiki/Vault) for more information on the Coyote vault. # See the [Vault documentation](https://github.com/Dark-Alex-17/coyote/wiki/Vault) for more information on the Coyote vault.
@@ -41,7 +42,7 @@ right_prompt: '{color.cyan}{?reasoning_effort [{reasoning_effort}] }{color.purpl
# The secrets_provider tells Coyote where to read and write secrets referenced via {{SECRET_NAME}} syntax. # The secrets_provider tells Coyote where to read and write secrets referenced via {{SECRET_NAME}} syntax.
# #
# Shorthand: set vault_password_file to enable the local provider with that password file. # Shorthand: set vault_password_file to enable the local provider with that password file.
vault_password_file: null # Path to a file containing the password for the Coyote vault (cannot be a secret template) vault_password_file: null # Path to a file containing the password for the Coyote vault (cannot be a secret template)
# #
# Explicit: set secrets_provider to one of the supported types below. When secrets_provider is set, # Explicit: set secrets_provider to one of the supported types below. When secrets_provider is set,
# vault_password_file is ignored. Note: secrets_provider itself cannot use {{SECRET}} template syntax. # vault_password_file is ignored. Note: secrets_provider itself cannot use {{SECRET}} template syntax.
@@ -81,39 +82,39 @@ vault_password_file: null # Path to a file containing the password for the Coyot
# ---- Function Calling ---- # ---- Function Calling ----
# See the [Tools documentation](https://github.com/Dark-Alex-17/coyote/wiki/Tools) for more details # See the [Tools documentation](https://github.com/Dark-Alex-17/coyote/wiki/Tools) for more details
function_calling_support: true # Enables or disables function calling (Globally). function_calling_support: true # Enables or disables function calling (Globally).
mapping_tools: # Alias for a tool or toolset mapping_tools: # Alias for a tool or toolset
fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep' fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep'
enabled_tools: enabled_tools: null # Which tools to enable by default.
null # Which tools to enable by default. # Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything.
# Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything. # Example (list form):
# Example (list form): # enabled_tools:
# enabled_tools: # - fs
# - fs # - web_search_coyote
# - web_search_coyote # Example (comma-separated form):
# Example (comma-separated form): # enabled_tools: fs,web_search_coyote
# enabled_tools: fs,web_search_coyote visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools').
visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools') # Null/missing = all tools in the global tools dir are visible; [] = none.
# - ast_grep.sh # - ast_grep.sh
# - demo_py.py # - demo_py.py
# - demo_sh.sh # - demo_sh.sh
# - demo_ts.ts # - demo_ts.ts
- execute_command.sh - execute_command.sh
# - execute_py_code.py # - execute_py_code.py
# - execute_sql_code.sh # - execute_sql_code.sh
# - fetch_url_via_curl.sh # - fetch_url_via_curl.sh
# - fetch_url_via_jina.sh # - fetch_url_via_jina.sh
- fs_cat.sh - fs_cat.sh
- fs_ls.sh - fs_ls.sh
# - fs_read.sh # - fs_read.sh
# - fs_glob.sh # - fs_glob.sh
# - fs_grep.sh # - fs_grep.sh
# - fs_mkdir.sh # - fs_mkdir.sh
# - fs_patch.sh # - fs_patch.sh
# - fs_write.sh # - fs_write.sh
- get_current_time.sh - get_current_time.sh
# - get_current_weather.py # - get_current_weather.py
# - get_current_weather.ts # - get_current_weather.ts
- get_current_weather.sh - get_current_weather.sh
# - search_arxiv.sh # - search_arxiv.sh
# - search_wikipedia.sh # - search_wikipedia.sh
@@ -126,106 +127,97 @@ visible_tools: # Which tools are visible to be compiled (and are thus able to be
# ---- MCP Servers ---- # ---- MCP Servers ----
# See the [MCP Servers documentation](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) for more details # See the [MCP Servers documentation](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) for more details
mcp_server_support: true # Enables or disables MCP servers (globally). mcp_server_support: true # Enables or disables MCP servers (globally).
mapping_mcp_servers: # Alias for an MCP server or set of servers mapping_mcp_servers: # Alias for an MCP server or set of servers
git: github,gitmcp git: github,gitmcp
enabled_mcp_servers: enabled_mcp_servers: null # Which MCP servers to enable by default.
null # Which MCP servers to enable by default. # Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything.
# Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything. # Example (list form):
# Example (list form): # enabled_mcp_servers:
# enabled_mcp_servers: # - github
# - github # - slack
# - slack # Example (comma-separated form):
# Example (comma-separated form): # enabled_mcp_servers: github,slack,ddg-search
# enabled_mcp_servers: github,slack,ddg-search mcp_tools: null # Per-server MCP tool allowlists (glob patterns: * and ? supported).
mcp_tools: # Tools that match no pattern are hidden from the model as if they
null # Per-server MCP tool allowlists (glob patterns: * and ? supported). # don't exist. Stacks with the other allowlist layers (mcp.json
# Tools that match no pattern are hidden from the model as if they # `allowedTools`, role, agent, session, skill, graph node). Every
# don't exist. Stacks with the other allowlist layers (mcp.json # configured layer must allow a tool, so layers only ever narrow.
# `allowedTools`, role, agent, session, skill, graph node). Every # An empty list blocks all of a server's tools.
# configured layer must allow a tool, so layers only ever narrow. # Example:
# An empty list blocks all of a server's tools. # mcp_tools:
# Example: # github:
# mcp_tools: # - get_*
# github: # - list_*
# - get_* # slack: []
# - list_* no_workspace_mcp: false # Disable loading workspace-local MCP servers (default: false).
# slack: [] # When false (the default), Coyote merges the first workspace MCP config it finds
no_workspace_mcp: # into the global MCP registry at startup, checking in order:
false # Disable loading workspace-local MCP servers (default: false). # 1. .coyote/mcp.json
# When false (the default), Coyote merges the first workspace MCP config it finds # 2. .coyote/.mcp.json (Claude-style file name)
# into the global MCP registry at startup, checking in order: # 3. .mcp.json (project root; Claude Code convention)
# 1. .coyote/mcp.json # Workspace entries shadow global ones on name collision.
# 2. .coyote/.mcp.json (Claude-style file name) # Set to true (or pass --no-workspace-mcp) to skip this entirely.
# 3. .mcp.json (project root; Claude Code convention)
# Workspace entries shadow global ones on name collision.
# Set to true (or pass --no-workspace-mcp) to skip this entirely.
# ---- Skills ---- # ---- Skills ----
# Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation. # Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation.
# See the [Skills documentation](https://github.com/Dark-Alex-17/coyote/wiki/Skills) for more details. # See the [Skills documentation](https://github.com/Dark-Alex-17/coyote/wiki/Skills) for more details.
skills_enabled: skills_enabled: true # Master switch. Set to false to hide all skill management tools from the model.
true # Master switch. Set to false to hide all skill management tools from the model. # Skills also require `function_calling_support: true` above to work at all.
# Skills also require `function_calling_support: true` above to work at all. visible_skills: # The universe of skills allowed to be enabled in any context. Omit (null) for "all installed".
visible_skills: # The universe of skills allowed to be enabled in any context. Omit (null) for "all installed".
- ai-slop-remover - ai-slop-remover
- code-review - code-review
- frontend-ui-ux - frontend-ui-ux
- git-master - git-master
enabled_skills: enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible.
null # Which skills are available by default (no role/agent/session active). null = all visible. # Accepts either a YAML list or a comma-separated string.
# Accepts either a YAML list or a comma-separated string. # Example (list form):
# Example (list form): # enabled_skills:
# enabled_skills: # - git-master
# - git-master # - ai-slop-remover
# - ai-slop-remover # Example (comma-separated form):
# Example (comma-separated form): # enabled_skills: git-master,ai-slop-remover
# enabled_skills: git-master,ai-slop-remover inject_skill_instructions: true # Inject a short hint pointing the model at `skill__list` when skills are enabled in
inject_skill_instructions: # this context. Only injected if `function_calling_support`, `skills_enabled`, and the
true # Inject a short hint pointing the model at `skill__list` when skills are enabled in # effective enabled skill set is non-empty (default: true).
# this context. Only injected if `function_calling_support`, `skills_enabled`, and the skill_instructions: null # Custom text used for the skill hint when injected. If null, uses built-in default.
# effective enabled skill set is non-empty (default: true).
skill_instructions: null # Custom text used for the skill hint when injected. If null, uses built-in default.
# ---- Macros ---- # ---- Macros ----
# Macros are Coyote's custom commands: named sequences of REPL commands and prompts, invoked directly by name # Macros are Coyote's custom commands: named sequences of REPL commands and prompts, invoked directly by name
# (a macro file named `review.yaml` runs as `.review [args]`; built-in commands always win a name collision). # (a macro file named `review.yaml` runs as `.review [args]`; built-in commands always win a name collision).
# Workspace-local macros in `.coyote/macros/` shadow same-named global macros (skip them with --no-workspace-macros). # Workspace-local macros in `.coyote/macros/` shadow same-named global macros (skip them with --no-workspace-macros).
# See the [Macros documentation](https://github.com/Dark-Alex-17/coyote/wiki/Macros) for more details. # See the [Macros documentation](https://github.com/Dark-Alex-17/coyote/wiki/Macros) for more details.
enabled_macros: enabled_macros: null # Which macros are invocable by default (no role/agent/session active). null = all visible.
null # Which macros are invocable by default (no role/agent/session active). null = all visible. # An empty list means NO macros are invocable. Accepts either a YAML list or a
# An empty list means NO macros are invocable. Accepts either a YAML list or a # comma-separated string. Roles, agents, and sessions may define their own
# comma-separated string. Roles, agents, and sessions may define their own # `enabled_macros`; the most specific active one wins (session > agent > role > global).
# `enabled_macros`; the most specific active one wins (session > agent > role > global). # Example (list form):
# Example (list form): # enabled_macros:
# enabled_macros: # - generate-commit-message
# - generate-commit-message # Example (comma-separated form):
# Example (comma-separated form): # enabled_macros: generate-commit-message,review
# enabled_macros: generate-commit-message,review
# ---- Auto-Continue (Todo System) ---- # ---- Auto-Continue (Todo System) ----
# The auto-continue system provides built-in task tracking for improved reliability. # The auto-continue system provides built-in task tracking for improved reliability.
# When enabled, the model can create todo lists and the system will automatically # When enabled, the model can create todo lists and the system will automatically
# prompt it to continue when incomplete tasks remain. # prompt it to continue when incomplete tasks remain.
# See the [Todo System documentation](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System) for more information # See the [Todo System documentation](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System) for more information
auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false) auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false)
max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10) max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10)
inject_todo_instructions: true # Inject default todo usage instructions into the system prompt (default: true) inject_todo_instructions: true # Inject default todo usage instructions into the system prompt (default: true)
continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default
# ---- Session ---- # ---- Session ----
# See the [Session documentation](https://github.com/Dark-Alex-17/coyote/wiki/Sessions) for more information # See the [Session documentation](https://github.com/Dark-Alex-17/coyote/wiki/Sessions) for more information
save_session: null # Controls the persistence of the session. If true, auto save; if false, don't auto-save save; if null, ask the user what to do save_session: null # Controls the persistence of the session. If true, auto save; if false, don't auto-save save; if null, ask the user what to do
compression_threshold: 4000 # Compress the session when the token count reaches or exceeds this threshold compression_threshold: 4000 # Compress the session when the token count reaches or exceeds this threshold
summarization_prompt: summarization_prompt: > # The text prompt used for creating a concise summary of session message
> # The text prompt used for creating a concise summary of session message
'Summarize the discussion briefly in 200 words or less to use as a prompt for future context.' 'Summarize the discussion briefly in 200 words or less to use as a prompt for future context.'
summary_context_prompt: summary_context_prompt: > # The text prompt used for including the summary of the entire session as context to the model
> # The text prompt used for including the summary of the entire session as context to the model
'This is a summary of the chat history as a recap: ' 'This is a summary of the chat history as a recap: '
compression_keep_last: 0 # Number of most-recent messages to keep visible after compression (0 = compress all messages) compression_keep_last: 0 # Number of most-recent messages to keep visible after compression (0 = compress all messages)
max_tool_result_chars: null # Cap on tool result characters forwarded to the model per call (null = no cap) max_tool_result_chars: null # Cap on tool result characters forwarded to the model per call (null = no cap)
max_concurrent_jobs: 5 # Max background jobs (`job__*` tools) running at once per context (default: 5; 0 disables background jobs entirely) max_concurrent_jobs: 5 # Max background jobs (`job__*` tools) running at once per context (default: 5; 0 disables background jobs entirely)
# ---- Memory ---- # ---- Memory ----
# See the [Memory documentation](https://github.com/Dark-Alex-17/coyote/wiki/Memory) for more information. # See the [Memory documentation](https://github.com/Dark-Alex-17/coyote/wiki/Memory) for more information.
@@ -234,13 +226,11 @@ max_concurrent_jobs: 5 # Max background jobs (`job__*` tools) running at once pe
# even when memory files exist. The cascade is: agent > session > role > app. # even when memory files exist. The cascade is: agent > session > role > app.
# Bootstrap with `coyote --init-memory [global|workspace]` to create the marker file # Bootstrap with `coyote --init-memory [global|workspace]` to create the marker file
# the LLM needs before it will write any memory. # the LLM needs before it will write any memory.
memory: null # null = enabled when memory exists on disk; true = force on; false = force off memory: null # null = enabled when memory exists on disk; true = force on; false = force off
memory_cap_with_tools: memory_cap_with_tools: null # Char cap for injected memory when function calling is available (default: 6000).
null # Char cap for injected memory when function calling is available (default: 6000). # Only MEMORY.md indexes are injected; the LLM uses memory__read to fetch drill files.
# Only MEMORY.md indexes are injected; the LLM uses memory__read to fetch drill files. memory_cap_without_tools: null # Char cap when function calling is unavailable (default: 12000).
memory_cap_without_tools: # Indexes plus drill file bodies are injected up to this cap.
null # Char cap when function calling is unavailable (default: 12000).
# Indexes plus drill file bodies are injected up to this cap.
# ---- Workspace Instructions ---- # ---- Workspace Instructions ----
# Human-curated project instructions injected read-only into the system prompt, in full. # Human-curated project instructions injected read-only into the system prompt, in full.
@@ -248,23 +238,22 @@ memory_cap_without_tools:
# chain below (per directory, in order). Scaffold with `coyote --init-instructions`. # chain below (per directory, in order). Scaffold with `coyote --init-instructions`.
# Disable per-invocation with --no-workspace-instructions, or override the chain with # Disable per-invocation with --no-workspace-instructions, or override the chain with
# repeatable --workspace-instructions-file flags. # repeatable --workspace-instructions-file flags.
workspace_instructions: null # null/true = inject when an instructions file exists; false = never inject workspace_instructions: null # null/true = inject when an instructions file exists; false = never inject
workspace_instructions_files: workspace_instructions_files: null # File name chain to search, in priority order.
null # File name chain to search, in priority order. # Default: [COYOTE.md, AGENTS.md, CLAUDE.md, GEMINI.md]
# Default: [COYOTE.md, AGENTS.md, CLAUDE.md, GEMINI.md] # Set to a custom list to reorder or drop fallbacks, e.g.:
# Set to a custom list to reorder or drop fallbacks, e.g.: # workspace_instructions_files: [COYOTE.md]
# workspace_instructions_files: [COYOTE.md]
# ---- RAG ---- # ---- RAG ----
# See the [RAG Docs](https://github.com/Dark-Alex-17/coyote/wiki/RAG) for more details. # See the [RAG Docs](https://github.com/Dark-Alex-17/coyote/wiki/RAG) for more details.
rag_embedding_model: null # Specifies the embedding model used for context retrieval rag_embedding_model: null # Specifies the embedding model used for context retrieval
rag_reranker_model: null # Specifies the reranker model used for sorting retrieved documents; Coyote uses Reciprocal Rank Fusion by default rag_reranker_model: null # Specifies the reranker model used for sorting retrieved documents; Coyote uses Reciprocal Rank Fusion by default
rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries
rag_chunk_size: null # Defines the size of chunks for document processing in characters rag_chunk_size: null # Defines the size of chunks for document processing in characters
rag_chunk_overlap: null # Defines the overlap between chunks rag_chunk_overlap: null # Defines the overlap between chunks
rag_extractor_model: null # LLM model for graph-based entity/relationship extraction; when set, enables a graph RAG signal alongside vector and BM25 rag_extractor_model: null # LLM model for graph-based entity/relationship extraction; when set, enables a graph RAG signal alongside vector and BM25
rag_extractor_prompt: null # Custom extraction prompt template; must contain __CHUNK__ placeholder; defaults to built-in prompt when null rag_extractor_prompt: null # Custom extraction prompt template; must contain __CHUNK__ placeholder; defaults to built-in prompt when null
rag_graph_hops: 1 # Number of hops to expand from matched entities at query time (0 = seed nodes only; 1 = direct neighbors; increase for denser graphs) rag_graph_hops: 1 # Number of hops to expand from matched entities at query time (0 = seed nodes only; 1 = direct neighbors; increase for denser graphs)
# Defines the query structure using variables like __CONTEXT__, __SOURCES__, and __INPUT__ to tailor searches to specific needs # Defines the query structure using variables like __CONTEXT__, __SOURCES__, and __INPUT__ to tailor searches to specific needs
rag_template: | rag_template: |
Answer the query based on the context while respecting the rules. (user query, some textual context and rules, all inside xml tags) Answer the query based on the context while respecting the rules. (user query, some textual context and rules, all inside xml tags)
@@ -295,14 +284,13 @@ document_loaders:
# You can add custom loaders using the following syntax: # You can add custom loaders using the following syntax:
# <file-extension>: <command-to-load-the-file> # <file-extension>: <command-to-load-the-file>
# Note: Use `$1` for input file and `$2` for output file. If `$2` is omitted, use stdout as output. # Note: Use `$1` for input file and `$2` for output file. If `$2` is omitted, use stdout as output.
pdf: 'pdftotext $1 -' # Use pdftotext to convert a PDF file to text pdf: 'pdftotext $1 -' # Use pdftotext to convert a PDF file to text
# (see https://poppler.freedesktop.org for details on how to install pdftotext) # (see https://poppler.freedesktop.org for details on how to install pdftotext)
docx: 'pandoc --to plain $1' # Use pandoc to convert a .docx file to text docx: 'pandoc --to plain $1' # Use pandoc to convert a .docx file to text
# (see https://pandoc.org for details on how to install pandoc) # (see https://pandoc.org for details on how to install pandoc)
jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}' # Use Jina to translate a website into text; jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}' # Use Jina to translate a website into text;
# Requires a Jina API key to be added to the Coyote vault # Requires a Jina API key to be added to the Coyote vault
git: git: > # Use yek to load a git repository into the knowledgebase (https://github.com/bodo-run/yek)
> # Use yek to load a git repository into the knowledgebase (https://github.com/bodo-run/yek)
sh -c "yek $1 --json | jq 'map({ path: .filename, contents: .content })'" sh -c "yek $1 --json | jq 'map({ path: .filename, contents: .content })'"
# ---- Clients ---- # ---- Clients ----
@@ -337,15 +325,15 @@ clients:
# See https://platform.openai.com/docs/quickstart # See https://platform.openai.com/docs/quickstart
- type: openai - type: openai
api_base: https://api.openai.com/v1 # Optional api_base: https://api.openai.com/v1 # Optional
api_key: '{{OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
organization_id: org-xxx # Optional organization_id: org-xxx # Optional
# For any platform compatible with OpenAI's API # For any platform compatible with OpenAI's API
- type: openai-compatible - type: openai-compatible
name: ollama name: ollama
api_base: http://localhost:11434/v1 api_base: http://localhost:11434/v1
api_key: '{{OLLAMA_API_KEY}}' # Optional; You can either hard-code or inject secrets from the Coyote vault api_key: '{{OLLAMA_API_KEY}}' # Optional; You can either hard-code or inject secrets from the Coyote vault
models: models:
- name: deepseek-r1 - name: deepseek-r1
max_input_tokens: 131072 max_input_tokens: 131072
@@ -363,10 +351,9 @@ clients:
# See https://ai.google.dev/docs # See https://ai.google.dev/docs
- type: gemini - type: gemini
api_base: https://generativelanguage.googleapis.com/v1beta api_base: https://generativelanguage.googleapis.com/v1beta
api_key: '{{GEMINI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{GEMINI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
auth: auth: null # When set to 'oauth', Coyote will use OAuth instead of an API key
null # When set to 'oauth', Coyote will use OAuth instead of an API key # Authenticate with `coyote --authenticate` or `.authenticate` in the REPL
# Authenticate with `coyote --authenticate` or `.authenticate` in the REPL
patch: patch:
chat_completions: chat_completions:
'.*': '.*':
@@ -383,27 +370,25 @@ clients:
# See https://docs.anthropic.com/claude/reference/getting-started-with-the-api # See https://docs.anthropic.com/claude/reference/getting-started-with-the-api
- type: claude - type: claude
api_base: https://api.anthropic.com/v1 # Optional api_base: https://api.anthropic.com/v1 # Optional
api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
auth: auth: null # When set to 'oauth', Coyote will use OAuth instead of an API key
null # When set to 'oauth', Coyote will use OAuth instead of an API key # Authenticate with `coyote --authenticate` or `.authenticate` in the REPL
# Authenticate with `coyote --authenticate` or `.authenticate` in the REPL
# See https://docs.mistral.ai/ # See https://docs.mistral.ai/
- type: openai-compatible - type: openai-compatible
name: mistral name: mistral
api_base: https://api.mistral.ai/v1 api_base: https://api.mistral.ai/v1
api_key: '{{MISTRAL_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{MISTRAL_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://docs.x.ai/docs - OAuth via SuperGrok / X Premium+ subscription # See https://docs.x.ai/docs - OAuth via SuperGrok / X Premium+ subscription
- type: openai-compatible - type: openai-compatible
name: xai name: xai
api_base: https://api.x.ai/v1 api_base: https://api.x.ai/v1
api_key: '{{XAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{XAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
auth: auth: null # When set to 'oauth', Coyote will use OAuth instead of an API key
null # When set to 'oauth', Coyote will use OAuth instead of an API key # Authenticate with `coyote --authenticate` or `.authenticate` in the REPL
# Authenticate with `coyote --authenticate` or `.authenticate` in the REPL # Note: Oauth requires SuperGrok/X Premium+ subscription
# Note: Oauth requires SuperGrok/X Premium+ subscription
# Example: private OpenAI-compatible gateway with client_credentials OAuth # Example: private OpenAI-compatible gateway with client_credentials OAuth
# - type: openai-compatible # - type: openai-compatible
@@ -433,31 +418,31 @@ clients:
- type: openai-compatible - type: openai-compatible
name: ai12 name: ai12
api_base: https://api.ai21.com/studio/v1 api_base: https://api.ai21.com/studio/v1
api_key: '{{AI21_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{AI21_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://docs.cohere.com/docs/the-cohere-platform # See https://docs.cohere.com/docs/the-cohere-platform
- type: cohere - type: cohere
api_base: https://api.cohere.ai/v2 # Optional api_base: https://api.cohere.ai/v2 # Optional
api_key: '{{COHERE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{COHERE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://docs.perplexity.ai/getting-started/overview # See https://docs.perplexity.ai/getting-started/overview
- type: openai-compatible - type: openai-compatible
name: perplexity name: perplexity
api_base: https://api.perplexity.ai api_base: https://api.perplexity.ai
api_key: '{{PERPLEXITY_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{PERPLEXITY_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://console.groq.com/docs/quickstart # See https://console.groq.com/docs/quickstart
- type: openai-compatible - type: openai-compatible
name: groq name: groq
api_base: https://api.groq.com/openai/v1 api_base: https://api.groq.com/openai/v1
api_key: '{{GROQ_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{GROQ_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart # See https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart
- type: azure-openai - type: azure-openai
api_base: https://{RESOURCE}.openai.azure.com api_base: https://{RESOURCE}.openai.azure.com
api_key: '{{AZURE_OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{AZURE_OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
models: models:
- name: gpt-4o # Model deployment name - name: gpt-4o # Model deployment name
max_input_tokens: 128000 max_input_tokens: 128000
supports_vision: true supports_vision: true
supports_function_calling: true supports_function_calling: true
@@ -469,7 +454,7 @@ clients:
# Specifies an application default credentials (adc) file # Specifies an application default credentials (adc) file
# Run `gcloud auth application-default login` to initialize the ADC file # Run `gcloud auth application-default login` to initialize the ADC file
# see https://cloud.google.com/docs/authentication/external/set-up-adc for more information # see https://cloud.google.com/docs/authentication/external/set-up-adc for more information
adc_file: <gcloud-config-dir>/application_default_credentials.json # Optional adc_file: <gcloud-config-dir>/application_default_credentials.json # Optional
patch: patch:
chat_completions: chat_completions:
'gemini-.*': 'gemini-.*':
@@ -486,76 +471,77 @@ clients:
# See https://docs.aws.amazon.com/bedrock/latest/userguide/ # See https://docs.aws.amazon.com/bedrock/latest/userguide/
- type: bedrock - type: bedrock
access_key_id: '{{AWS_ACCESS_KEY_ID}}' # You can either hard-code or inject secrets from the Coyote vault access_key_id: '{{AWS_ACCESS_KEY_ID}}' # You can either hard-code or inject secrets from the Coyote vault
secret_access_key: '{{AWS_SECRET_ACCESS_KEY}}' # You can either hard-code or inject secrets from the Coyote vault secret_access_key: '{{AWS_SECRET_ACCESS_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
region: xxx region: xxx
session_token: xxx # Optional, only needed for temporary credentials session_token: xxx # Optional, only needed for temporary credentials
# See https://developers.cloudflare.com/workers-ai/ # See https://developers.cloudflare.com/workers-ai/
- type: openai-compatible - type: openai-compatible
name: cloudflare name: cloudflare
api_base: https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/v1 api_base: https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/v1
api_key: '{{CLOUDFLARE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{CLOUDFLARE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html # See https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html
- type: openai-compatible - type: openai-compatible
name: ernie name: ernie
api_base: https://qianfan.baidubce.com/v2 api_base: https://qianfan.baidubce.com/v2
api_key: '{{BAIDU_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{BAIDU_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://dashscope.aliyun.com/ # See https://dashscope.aliyun.com/
- type: openai-compatible - type: openai-compatible
name: qianwen name: qianwen
api_base: https://dashscope.aliyuncs.com/compatible-mode/v1 api_base: https://dashscope.aliyuncs.com/compatible-mode/v1
api_key: '{{ALIYUN_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{ALIYUN_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://cloud.tencent.com/product/hunyuan # See https://cloud.tencent.com/product/hunyuan
- type: openai-compatible - type: openai-compatible
name: hunyuan name: hunyuan
api_base: https://api.hunyuan.cloud.tencent.com/v1 api_base: https://api.hunyuan.cloud.tencent.com/v1
api_key: '{{TENCENT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{TENCENT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://platform.moonshot.cn/docs/intro # See https://platform.moonshot.cn/docs/intro
- type: openai-compatible - type: openai-compatible
name: moonshot name: moonshot
api_base: https://api.moonshot.cn/v1 api_base: https://api.moonshot.cn/v1
api_key: '{{MOONSHOT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{MOONSHOT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://platform.deepseek.com/api-docs/ # See https://platform.deepseek.com/api-docs/
- type: openai-compatible - type: openai-compatible
name: deepseek name: deepseek
api_base: https://api.deepseek.com api_base: https://api.deepseek.com
api_key: '{{DEEPSEEK_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{DEEPSEEK_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://open.bigmodel.cn/dev/howuse/introduction # See https://open.bigmodel.cn/dev/howuse/introduction
- type: openai-compatible - type: openai-compatible
name: zhipuai name: zhipuai
api_base: https://open.bigmodel.cn/api/paas/v4 api_base: https://open.bigmodel.cn/api/paas/v4
api_key: '{{ZHIPUAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{ZHIPUAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://platform.minimaxi.com/document/Fast%20access # See https://platform.minimaxi.com/document/Fast%20access
- type: openai-compatible - type: openai-compatible
name: minimax name: minimax
api_base: https://api.minimax.chat/v1 api_base: https://api.minimax.chat/v1
api_key: '{{MINIMAX_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{MINIMAX_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://openrouter.ai/docs#quick-start # See https://openrouter.ai/docs#quick-start
- type: openai-compatible - type: openai-compatible
name: openrouter name: openrouter
api_base: https://openrouter.ai/api/v1 api_base: https://openrouter.ai/api/v1
api_key: '{{OPENROUTER_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{OPENROUTER_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://github.com/marketplace/models # See https://github.com/marketplace/models
- type: openai-compatible - type: openai-compatible
name: github name: github
api_base: https://models.inference.ai.azure.com api_base: https://models.inference.ai.azure.com
api_key: '{{GITHUB_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{GITHUB_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://deepinfra.com/docs # See https://deepinfra.com/docs
- type: openai-compatible - type: openai-compatible
name: deepinfra name: deepinfra
api_base: https://api.deepinfra.com/v1/openai api_base: https://api.deepinfra.com/v1/openai
api_key: '{{DEEPINFRA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{DEEPINFRA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# ----- RAG dedicated ----- # ----- RAG dedicated -----
@@ -563,10 +549,10 @@ clients:
- type: openai-compatible - type: openai-compatible
name: jina name: jina
api_base: https://api.jina.ai/v1 api_base: https://api.jina.ai/v1
api_key: '{{JINA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{JINA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
# See https://docs.voyageai.com/docs/introduction # See https://docs.voyageai.com/docs/introduction
- type: openai-compatible - type: openai-compatible
name: voyageai name: voyageai
api_base: https://api.voyageai.com/v1 api_base: https://api.voyageai.com/v1
api_key: '{{VOYAGEAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault api_key: '{{VOYAGEAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
+1 -1
View File
@@ -69,7 +69,7 @@ impl AppState {
} }
} }
let mut functions = Functions::init(config.visible_tools.as_ref().unwrap_or(&Vec::new()))?; let mut functions = Functions::init(config.visible_tools.as_deref())?;
if !mcp_registry.is_empty() && config.mcp_server_support { if !mcp_registry.is_empty() && config.mcp_server_support {
functions.append_mcp_meta_functions(mcp_registry.server_features()); functions.append_mcp_meta_functions(mcp_registry.server_features());
} }
+91 -83
View File
@@ -150,6 +150,9 @@ fn validate_no_template_in_secrets_provider(content: &str) -> Result<()> {
const DARK_THEME: &[u8] = include_bytes!("../../assets/monokai-extended.theme.bin"); const DARK_THEME: &[u8] = include_bytes!("../../assets/monokai-extended.theme.bin");
const LIGHT_THEME: &[u8] = include_bytes!("../../assets/monokai-extended-light.theme.bin"); const LIGHT_THEME: &[u8] = include_bytes!("../../assets/monokai-extended-light.theme.bin");
/// Fully documented config skeleton the first-run wizard splices dynamic values into.
const CONFIG_TEMPLATE: &str = include_str!("../../assets/config-template.yaml");
const CONFIG_FILE_NAME: &str = "config.yaml"; const CONFIG_FILE_NAME: &str = "config.yaml";
const AGENT_GRAPH_FILE_NAME: &str = "graph.yaml"; const AGENT_GRAPH_FILE_NAME: &str = "graph.yaml";
const ROLES_DIR_NAME: &str = "roles"; const ROLES_DIR_NAME: &str = "roles";
@@ -180,27 +183,6 @@ const BUNDLE_MANIFEST_FILE: &str = "coyote-bundle.yaml";
const SBX_MIXIN_KITS_DIR_NAME: &str = "sbx-mixin-kits"; const SBX_MIXIN_KITS_DIR_NAME: &str = "sbx-mixin-kits";
const GIT_DIR_NAME: &str = ".git"; const GIT_DIR_NAME: &str = ".git";
const GITIGNORE_FILE_NAME: &str = ".gitignore"; const GITIGNORE_FILE_NAME: &str = ".gitignore";
const DEFAULT_VISIBLE_TOOLS: [&str; 19] = [
"execute_command.sh",
"execute_py_code.py",
"execute_sql_code.sh",
"fetch_url_via_curl.sh",
"fs_cat.sh",
"fs_glob.sh",
"fs_grep.sh",
"fs_ls.sh",
"fs_mkdir.sh",
"fs_patch.sh",
"fs_read.sh",
"fs_rm.sh",
"fs_write.sh",
"ast_grep.sh",
"get_current_time.sh",
"get_current_weather.sh",
"search_wikipedia.sh",
"search_arxiv.sh",
"web_search_coyote.sh",
];
const CLIENTS_FIELD: &str = "clients"; const CLIENTS_FIELD: &str = "clients";
@@ -811,51 +793,20 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> {
let client = Select::new("API Provider (required):", list_client_types()).prompt()?; let client = Select::new("API Provider (required):", list_client_types()).prompt()?;
let mut config = json!({});
let (model, clients_config) = create_client_config(client, &vault).await?; let (model, clients_config) = create_client_config(client, &vault).await?;
config["model"] = model.into(); let secrets = match &provider_choice {
match &provider_choice { None => json!({
None => { "vault_password_file": vault.local_password_file()?.display().to_string()
config["vault_password_file"] = }),
vault.local_password_file()?.display().to_string().into();
}
Some(provider) => { Some(provider) => {
config["secrets_provider"] = serde_json::to_value(provider) let provider = serde_json::to_value(provider)
.with_context(|| "failed to serialize secrets_provider config")?; .with_context(|| "failed to serialize secrets_provider config")?;
json!({ "secrets_provider": provider })
} }
} };
config["stream"] = json!(true);
config["save"] = json!(true);
config["keybindings"] = json!("vi");
config["wrap"] = json!("auto");
config["wrap_code"] = json!(false);
config["function_calling_support"] = json!(true);
config["enabled_tools"] = json!(null);
config["visible_tools"] = json!(DEFAULT_VISIBLE_TOOLS);
config["mcp_server_support"] = json!(true);
config["enabled_mcp_servers"] = json!(null);
config["highlight"] = json!(true);
config["light_theme"] = json!(false);
config[CLIENTS_FIELD] = clients_config;
let config_data = serde_yaml::to_string(&config).with_context(|| "Failed to create config")?; let config_data = render_config_template(&model, Some(&secrets), &clients_config)?;
let config_data = format!( write_config_file(config_path, &config_data)
"# see https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml\n\n{config_data}"
);
ensure_parent_exists(config_path)?;
std::fs::write(config_path, config_data)
.with_context(|| format!("Failed to write to '{}'", config_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::prelude::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(config_path, perms)?;
}
println!("✓ Saved the config file to '{}'.\n", config_path.display());
Ok(())
} }
async fn create_config_file_sandbox(config_path: &Path) -> Result<()> { async fn create_config_file_sandbox(config_path: &Path) -> Result<()> {
@@ -865,7 +816,7 @@ async fn create_config_file_sandbox(config_path: &Path) -> Result<()> {
"Running in sandbox mode — your API provider credentials are managed by your host Coyote configuration if configured." "Running in sandbox mode — your API provider credentials are managed by your host Coyote configuration if configured."
); );
let oai_api_base = client::OPENAI_COMPATIBLE_PROVIDERS let oai_api_base = OPENAI_COMPATIBLE_PROVIDERS
.iter() .iter()
.find(|(name, _)| *name == client) .find(|(name, _)| *name == client)
.map(|(_, url)| *url); .map(|(_, url)| *url);
@@ -878,13 +829,13 @@ async fn create_config_file_sandbox(config_path: &Path) -> Result<()> {
} else { } else {
api_base.to_string() api_base.to_string()
}; };
serde_json::json!({ json!({
"type": "openai-compatible", "type": "openai-compatible",
"name": client, "name": client,
"api_base": api_base_str, "api_base": api_base_str,
}) })
} else { } else {
serde_json::json!({ "type": client }) json!({ "type": client })
}; };
if client::client_type_supports_oauth(client) { if client::client_type_supports_oauth(client) {
@@ -898,27 +849,35 @@ async fn create_config_file_sandbox(config_path: &Path) -> Result<()> {
let model = set_client_models_config(&mut client_config, client).await?; let model = set_client_models_config(&mut client_config, client).await?;
let mut config = serde_json::json!({}); let config_data = render_config_template(&model, None, &json!([client_config]))?;
config["model"] = model.into(); write_config_file(config_path, &config_data)
config["stream"] = serde_json::json!(true); }
config["save"] = serde_json::json!(true);
config["keybindings"] = serde_json::json!("vi");
config["wrap"] = serde_json::json!("auto");
config["wrap_code"] = serde_json::json!(false);
config["function_calling_support"] = serde_json::json!(true);
config["enabled_tools"] = serde_json::json!(null);
config["visible_tools"] = serde_json::json!(DEFAULT_VISIBLE_TOOLS);
config["mcp_server_support"] = serde_json::json!(true);
config["enabled_mcp_servers"] = serde_json::json!(null);
config["highlight"] = serde_json::json!(true);
config["light_theme"] = serde_json::json!(false);
config[CLIENTS_FIELD] = serde_json::json!(vec![client_config]);
let config_data = serde_yaml::to_string(&config).with_context(|| "Failed to create config")?; fn render_config_template(
let config_data = format!( model: &str,
"# see https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml\n\n{config_data}" secrets: Option<&serde_json::Value>,
); clients: &serde_json::Value,
) -> Result<String> {
let to_yaml = |value: &serde_json::Value| {
serde_yaml::to_string(value).with_context(|| "Failed to create config")
};
let model_block = to_yaml(&json!({ "model": model }))?;
let secrets_block = match secrets {
Some(value) => to_yaml(value)?,
None => "# Sandbox mode: no vault provider is configured; secrets are provisioned\n\
# from the host when the sandbox is created.\n"
.to_string(),
};
let clients_block = to_yaml(&json!({ CLIENTS_FIELD: clients }))?;
Ok(CONFIG_TEMPLATE
.replacen("__MODEL_BLOCK__\n", &model_block, 1)
.replacen("__SECRETS_BLOCK__\n", &secrets_block, 1)
.replacen("__CLIENTS_BLOCK__\n", &clients_block, 1))
}
fn write_config_file(config_path: &Path, config_data: &str) -> Result<()> {
ensure_parent_exists(config_path)?; ensure_parent_exists(config_path)?;
std::fs::write(config_path, config_data) std::fs::write(config_path, config_data)
.with_context(|| format!("Failed to write to '{}'", config_path.display()))?; .with_context(|| format!("Failed to write to '{}'", config_path.display()))?;
@@ -1174,6 +1133,55 @@ clients:
assert_eq!(cfg.enabled_macros, None); assert_eq!(cfg.enabled_macros, None);
} }
#[test]
fn config_template_renders_parseable_config() {
let secrets = json!({ "vault_password_file": "/home/user/.coyote_password" });
let clients = json!([{ "type": "openai", "api_key": "sk-test" }]);
let rendered = render_config_template("openai:gpt-4o", Some(&secrets), &clients).unwrap();
assert!(!rendered.contains("__MODEL_BLOCK__"));
assert!(!rendered.contains("__SECRETS_BLOCK__"));
assert!(!rendered.contains("__CLIENTS_BLOCK__"));
let cfg = Config::load_from_str(&rendered).unwrap();
assert_eq!(cfg.model_id, "openai:gpt-4o");
assert_eq!(
cfg.vault_password_file,
Some(PathBuf::from("/home/user/.coyote_password"))
);
assert!(cfg.secrets_provider.is_none());
assert_eq!(cfg.keybindings, "emacs");
assert!(cfg.save);
assert_eq!(cfg.wrap.as_deref(), Some("auto"));
assert!(cfg.visible_tools.is_none());
assert!(cfg.mapping_tools.is_empty());
assert!(cfg.document_loaders.is_empty());
assert_eq!(cfg.compression_threshold, 4000);
assert!(cfg.theme.is_none());
assert_eq!(cfg.clients.len(), 1);
}
#[test]
fn config_template_renders_parseable_sandbox_config() {
let clients = json!([{ "type": "claude" }]);
let rendered =
render_config_template("claude:claude-sonnet-4-20250514", None, &clients).unwrap();
assert!(!rendered.contains("__SECRETS_BLOCK__"));
let cfg = Config::load_from_str(&rendered).unwrap();
assert_eq!(cfg.model_id, "claude:claude-sonnet-4-20250514");
assert!(cfg.vault_password_file.is_none());
assert!(cfg.secrets_provider.is_none());
assert_eq!(cfg.keybindings, "emacs");
assert!(cfg.save);
assert!(cfg.visible_tools.is_none());
assert_eq!(cfg.compression_threshold, 4000);
assert_eq!(cfg.clients.len(), 1);
}
#[test] #[test]
fn config_enabled_macros_empty_string_is_some_empty() { fn config_enabled_macros_empty_string_is_some_empty() {
let cfg: Config = serde_yaml::from_str("enabled_macros: \"\"").unwrap(); let cfg: Config = serde_yaml::from_str("enabled_macros: \"\"").unwrap();
+2 -2
View File
@@ -4266,7 +4266,7 @@ impl RequestContext {
} }
} }
let mut functions = Functions::init(app.visible_tools.as_ref().unwrap_or(&Vec::new()))?; let mut functions = Functions::init(app.visible_tools.as_deref())?;
if self.working_mode.is_repl() { if self.working_mode.is_repl() {
functions.append_user_interaction_functions(); functions.append_user_interaction_functions();
} }
@@ -4687,7 +4687,7 @@ impl RequestContext {
pub fn exit_agent(&mut self, app: &AppConfig) -> Result<()> { pub fn exit_agent(&mut self, app: &AppConfig) -> Result<()> {
self.exit_session()?; self.exit_session()?;
let mut functions = Functions::init(app.visible_tools.as_ref().unwrap_or(&Vec::new()))?; let mut functions = Functions::init(app.visible_tools.as_deref())?;
if self.working_mode.is_repl() { if self.working_mode.is_repl() {
functions.append_user_interaction_functions(); functions.append_user_interaction_functions();
} }
+132 -34
View File
@@ -214,6 +214,57 @@ fn tool_source_stems() -> Result<HashSet<String>> {
Ok(stems) Ok(stems)
} }
fn all_tool_source_files() -> Result<Vec<String>> {
let tools_dir = paths::global_tools_dir();
if !tools_dir.exists() {
return Ok(Vec::new());
}
let mut file_names = Vec::new();
for entry in fs::read_dir(&tools_dir)? {
let path = entry?.path();
if path.is_file()
&& let Some(name) = path.file_name().and_then(OsStr::to_str)
{
file_names.push(name.to_string());
}
}
Ok(dedupe_tool_files_by_stem(file_names))
}
fn dedupe_tool_files_by_stem(file_names: Vec<String>) -> Vec<String> {
fn extension_rank(name: &str) -> Option<usize> {
let ext = Path::new(name).extension().and_then(OsStr::to_str)?;
match Language::from_extension(ext) {
Language::Bash => Some(0),
Language::Python => Some(1),
Language::TypeScript => Some(2),
Language::Unsupported => None,
}
}
let mut best: HashMap<String, (usize, String)> = HashMap::new();
for name in file_names {
let Some(rank) = extension_rank(&name) else {
continue;
};
let Some(stem) = Path::new(&name).file_stem().and_then(OsStr::to_str) else {
continue;
};
match best.get(stem) {
Some((best_rank, _)) if *best_rank <= rank => {}
_ => {
best.insert(stem.to_string(), (rank, name));
}
}
}
let mut files: Vec<String> = best.into_values().map(|(_, name)| name).collect();
files.sort();
files
}
fn bin_entry_stem(file_name: &str) -> &str { fn bin_entry_stem(file_name: &str) -> &str {
let name = file_name.strip_prefix("run-").unwrap_or(file_name); let name = file_name.strip_prefix("run-").unwrap_or(file_name);
Path::new(name) Path::new(name)
@@ -587,18 +638,23 @@ impl Functions {
Ok(()) Ok(())
} }
pub fn init(visible_tools: &[String]) -> Result<Self> { pub fn init(visible_tools: Option<&[String]>) -> Result<Self> {
Self::remove_stale_global_function_binaries()?; Self::remove_stale_global_function_binaries()?;
let (visible_tools, lenient) = match visible_tools {
Some(tools) => (tools.to_vec(), false),
None => (all_tool_source_files()?, true),
};
let declarations = Self { let declarations = Self {
declarations: Self::build_global_tool_declarations(visible_tools)?, declarations: Self::build_global_tool_declarations(&visible_tools, lenient)?,
}; };
info!( info!(
"Building global function binaries in {}", "Building global function binaries in {}",
paths::functions_bin_dir().display() paths::functions_bin_dir().display()
); );
Self::build_global_function_binaries(visible_tools, None)?; Self::build_global_function_binaries(&visible_tools, None, lenient)?;
Ok(declarations) Ok(declarations)
} }
@@ -608,13 +664,13 @@ impl Functions {
let global_tools_declarations = if !global_tools.is_empty() { let global_tools_declarations = if !global_tools.is_empty() {
info!("Loading global tools for agent: {name}: {global_tools:?}"); info!("Loading global tools for agent: {name}: {global_tools:?}");
let tools_declarations = Self::build_global_tool_declarations(global_tools)?; let tools_declarations = Self::build_global_tool_declarations(global_tools, false)?;
info!( info!(
"Building global function binaries required by agent: {name} in {}", "Building global function binaries required by agent: {name} in {}",
paths::functions_bin_dir().display() paths::functions_bin_dir().display()
); );
Self::build_global_function_binaries(global_tools, Some(name))?; Self::build_global_function_binaries(global_tools, Some(name), false)?;
tools_declarations tools_declarations
} else { } else {
debug!("No global tools found for agent: {}", name); debug!("No global tools found for agent: {}", name);
@@ -964,13 +1020,17 @@ impl Functions {
fn build_global_tool_declarations( fn build_global_tool_declarations(
enabled_tools: &[String], enabled_tools: &[String],
lenient: bool,
) -> Result<Vec<FunctionDeclaration>> { ) -> Result<Vec<FunctionDeclaration>> {
let global_tools_directory = paths::global_tools_dir(); let global_tools_directory = paths::global_tools_dir();
let mut function_declarations = Vec::new(); let mut function_declarations = Vec::new();
for tool in enabled_tools { for tool in enabled_tools {
let declaration = Self::generate_declarations(&global_tools_directory.join(tool))?; match Self::generate_declarations(&global_tools_directory.join(tool)) {
function_declarations.extend(declaration); Ok(declaration) => function_declarations.extend(declaration),
Err(err) if lenient => warn!("Skipping tool {tool}: {err}"),
Err(err) => return Err(err),
}
} }
Ok(function_declarations) Ok(function_declarations)
@@ -1032,41 +1092,50 @@ impl Functions {
fn build_global_function_binaries( fn build_global_function_binaries(
enabled_tools: &[String], enabled_tools: &[String],
agent_name: Option<&str>, agent_name: Option<&str>,
lenient: bool,
) -> Result<()> { ) -> Result<()> {
for tool in enabled_tools { for tool in enabled_tools {
let language = Language::from( match Self::build_global_function_binary(tool, agent_name) {
&Path::new(&tool) Ok(()) => {}
.extension() Err(err) if lenient => warn!("Skipping binary for tool {tool}: {err}"),
.and_then(OsStr::to_str) Err(err) => return Err(err),
.map(|s| s.to_lowercase())
.ok_or_else(|| {
anyhow::format_err!("Unable to extract file extension from path: {tool:?}")
})?,
);
let binary_name = Path::new(&tool)
.file_stem()
.and_then(OsStr::to_str)
.ok_or_else(|| {
anyhow::format_err!("Unable to extract file name from path: {tool:?}")
})?;
if language == Language::Unsupported {
bail!("Unsupported tool file extension: {}", language.as_ref());
} }
let tool_path = paths::global_tools_dir().join(tool);
let custom_runtime = extract_shebang_runtime(&tool_path);
Self::build_binaries(
binary_name,
language,
BinaryType::Tool(agent_name),
custom_runtime.as_deref(),
)?;
} }
Ok(()) Ok(())
} }
fn build_global_function_binary(tool: &str, agent_name: Option<&str>) -> Result<()> {
let language = Language::from(
&Path::new(tool)
.extension()
.and_then(OsStr::to_str)
.map(|s| s.to_lowercase())
.ok_or_else(|| {
anyhow::format_err!("Unable to extract file extension from path: {tool:?}")
})?,
);
let binary_name = Path::new(tool)
.file_stem()
.and_then(OsStr::to_str)
.ok_or_else(|| {
anyhow::format_err!("Unable to extract file name from path: {tool:?}")
})?;
if language == Language::Unsupported {
bail!("Unsupported tool file extension: {}", language.as_ref());
}
let tool_path = paths::global_tools_dir().join(tool);
let custom_runtime = extract_shebang_runtime(&tool_path);
Self::build_binaries(
binary_name,
language,
BinaryType::Tool(agent_name),
custom_runtime.as_deref(),
)
}
fn remove_stale_agent_bin_entries(name: &str) -> Result<()> { fn remove_stale_agent_bin_entries(name: &str) -> Result<()> {
let agent_bin_directory = paths::agent_bin_dir(name); let agent_bin_directory = paths::agent_bin_dir(name);
@@ -2603,6 +2672,35 @@ mod tests {
use std::sync::Arc; use std::sync::Arc;
use std::{mem, process}; use std::{mem, process};
#[test]
fn dedupe_tool_files_prefers_sh_over_py_over_ts() {
let files = vec![
"get_current_weather.ts".to_string(),
"get_current_weather.py".to_string(),
"get_current_weather.sh".to_string(),
"fetch.ts".to_string(),
"fetch.py".to_string(),
"demo_ts.ts".to_string(),
];
assert_eq!(
dedupe_tool_files_by_stem(files),
vec!["demo_ts.ts", "fetch.py", "get_current_weather.sh"]
);
}
#[test]
fn dedupe_tool_files_skips_unsupported_extensions() {
let files = vec![
"notes.md".to_string(),
"tool.sh".to_string(),
"README".to_string(),
"archive.tar.gz".to_string(),
];
assert_eq!(dedupe_tool_files_by_stem(files), vec!["tool.sh"]);
}
fn call(name: &str, id: Option<&str>) -> ToolCall { fn call(name: &str, id: Option<&str>) -> ToolCall {
ToolCall::new(name.to_string(), json!({}), id.map(|s| s.to_string())) ToolCall::new(name.to_string(), json!({}), id.map(|s| s.to_string()))
} }