Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## New features

* The `btw_tool_skill` tool now supports custom skill search directories via the `btw.skills.paths` R option or `BTW_SKILLS_PATHS` environment variable. When set, the value entirely replaces all user-level and project-level skill directories; package-bundled skills are always preserved. Multiple paths can be provided as a character vector (R option) or OS-native path-separated string (env var). Values are captured at tool-registration time, so custom directories set in `btw.md` survive after `btw_client()` returns (#193).

* Added `btw_tool_files_patch()`, a new files tool that applies a structured diff-style patch envelope to make coordinated changes across multiple files in a single call. One patch can add, update, delete, and rename files atomically: all operations are validated before any file is written, so a partial failure leaves the working tree untouched (#190).

* Added two new commands to the `btw` CLI: `btw app` to launch a `btw_app()` session in the current working directory and `btw skills install` to install skills from the terminal.
Expand Down Expand Up @@ -174,4 +176,4 @@

# btw 1.0.0

* Initial CRAN submission.
* Initial CRAN submission.
110 changes: 91 additions & 19 deletions R/tool-skills.R
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ NULL
#' by btw 1.2.0 is also included at lower priority.
#' 4. Project-level skills (`.btw/skills/` or `.agents/skills/`)
#'
#' The default user-level and project-level directories can be replaced by
#' setting the `btw.skills.paths` R option or the `BTW_SKILLS_PATHS` environment
#' variable. When set, the value **entirely replaces** all user-level and project-level
#' directories (items 3 and 4 above). Package-bundled skills and skills from
#' attached packages (items 1 and 2) are always included regardless of this
#' setting. The R option takes precedence over the environment variable.
#' Multiple paths can be provided as a character vector (e.g.
#' `options(btw.skills.paths = c("/path/a", "/path/b"))`) or as a single
#' path-separator-delimited string (`:` on Unix/Mac, `;` on Windows, which is
#' the only form supported by environment variables). Non-existent paths are
#' silently skipped.
#'
#' **Resolution timing:** options and environment variables are read at
#' **tool-registration time** (i.e. when [btw_tools()] or [btw_client()] is
#' called). The resolved paths are captured in the tool's closure so that they
#' remain correct even if the options are later modified or go out of scope
#' (for example, when `btw_client()` restores options after returning). If you
#' need different directories for a new session, create a new client.
#'
#' @param name The name of the skill to load, or `""` to list all available
#' skills.
#' @inheritParams btw_tool_docs_package_news
Expand Down Expand Up @@ -117,8 +136,22 @@ btw_tool_skill_impl <- function(name) {
group = "skills",

tool = function() {
# Capture the resolved skill dir overrides at registration time so the
# tool closes over the correct paths even after options set transiently by
# btw_client() / btw_app() have been restored to their prior values.
captured_paths <- skill_dirs_from_option_or_envvar("btw.skills.paths", "BTW_SKILLS_PATHS")

# Only replay the options that were actually captured. When a captured
# value is NULL (nothing was set at registration time), leave the live
# option untouched so btw_skills_directories() sees the real environment.
impl <- function(name) {
opts <- list()
if (!is.null(captured_paths)) opts[["btw.skills.paths"]] <- captured_paths
withr::with_options(opts, btw_tool_skill_impl(name))
}

ellmer::tool(
btw_tool_skill_impl,
impl,
name = "btw_tool_skill",
description = paste(
"Load a skill's specialized instructions and list its bundled",
Expand Down Expand Up @@ -159,30 +192,69 @@ btw_skills_directories <- function(project_dir = getwd()) {
# Skills from attached packages
dirs <- c(dirs, attached_package_skill_dirs())

# Legacy: btw <= 1.2.0 install target — kept for backwards compatibility only,
# never written to by newer versions
legacy_skills_dir <- file.path(tools::R_user_dir("btw", "config"), "skills")
if (dir.exists(legacy_skills_dir)) {
dirs <- c(dirs, legacy_skills_dir)
}
# Custom paths entirely replace all user-level and project-level defaults.
# When not set, fall back to the standard user-level + project-level dirs.
custom_paths <- skill_dirs_from_option_or_envvar("btw.skills.paths", "BTW_SKILLS_PATHS")

# User-level skills from btw_user_dirs() in increasing priority order
for (user_dir in rev(btw_user_dirs())) {
user_skills_dir <- file.path(user_dir, "skills")
if (dir.exists(user_skills_dir) && !user_skills_dir %in% dirs) {
dirs <- c(dirs, user_skills_dir)
search_dirs <- custom_paths %||% c(
default_user_skill_dirs(),
default_project_skill_dirs(project_dir)
)

for (search_dir in search_dirs) {
if (dir.exists(search_dir) && !search_dir %in% dirs) {
dirs <- c(dirs, search_dir)
}
}

# Project-level skills from multiple conventions
for (project_subdir in project_skill_subdirs()) {
project_skills_dir <- file.path(project_dir, project_subdir)
if (dir.exists(project_skills_dir)) {
dirs <- c(dirs, project_skills_dir)
dirs
}

skill_dirs_from_option_or_envvar <- function(option_name, envvar_name) {
raw <- getOption(option_name, default = NULL)
if (is.null(raw)) {
env_val <- Sys.getenv(envvar_name, unset = NA_character_)
if (is.na(env_val)) {
return(NULL)
}
raw <- env_val
}
# Accept either a character vector (idiomatic R / YAML array) or a
# path-separator-delimited string (useful from env vars).
if (length(raw) > 1) {
paths <- as.character(raw)
} else {
paths <- strsplit(as.character(raw), .Platform$path.sep, fixed = TRUE)[[1]]
}
paths <- paths[!is.na(paths) & nzchar(paths)]
normalizePath(paths, mustWork = FALSE)
}

dirs
default_user_skill_dirs <- function() {
# Legacy: btw <= 1.2.0 install target — kept for backwards compatibility only,
# never written to by newer versions. Listed first (lowest priority).
legacy_skills_dir <- file.path(tools::R_user_dir("btw", "config"), "skills")

# Current user-level skill dirs in increasing priority order
current_dirs <- rev(vapply(
btw_user_dirs(),
function(d) file.path(d, "skills"),
character(1)
))

# Combine: legacy first, then current dirs in increasing priority order.
# The `%in%` guard in the calling loop prevents re-adding dirs already
# present from earlier sources (e.g. attached packages). `unique()` removes
# any duplicates within this vector itself before the loop sees them.
unique(c(legacy_skills_dir, current_dirs))
}

default_project_skill_dirs <- function(project_dir) {
vapply(
project_skill_subdirs(),
function(s) file.path(project_dir, s),
character(1)
)
}

attached_package_skill_dirs <- function() {
Expand Down Expand Up @@ -1093,4 +1165,4 @@ install_skill_from_dir <- function(
maybe_use_build_ignore(target_parent)

invisible(target_dir)
}
}
136 changes: 136 additions & 0 deletions btw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
client:
sonnet:
provider: aws_bedrock
model: us.anthropic.claude-sonnet-4-6
api_args:
additionalModelRequestFields:
thinking:
type: enabled
budget_tokens: 4000
haiku:
provider: aws_bedrock
model: us.anthropic.claude-haiku-4-5-20251001-v1:0
api_args:
additionalModelRequestFields:
thinking:
type: enabled
budget_tokens: 4000
opus:
provider: aws_bedrock
model: us.anthropic.claude-opus-4-5-20251101-v1:0
api_args:
additionalModelRequestFields:
thinking:
type: enabled
budget_tokens: 4000

sonnet-4.5:
provider: aws_bedrock
model: us.anthropic.claude-sonnet-4-5-20250929-v1:0
sonnet-4:
provider: aws_bedrock
model: us.anthropic.claude-sonnet-4-20250514-v1:0
opus-4.5:
provider: aws_bedrock
model: us.anthropic.claude-opus-4-5-20251101-v1:0

gemma4:
provider: openai_compatible
model: google/gemma-4-26b-a4b
# base_url: http://127.0.0.1:1234/v1
base_url: http://remy.local:1234/v1
name: "LM Studio"

qwen3.6:
provider: openai_compatible
model: qwen/qwen3.6-35b-a3b
base_url: http://remy.local:1234/v1
name: "LM Studio"
preserve_thinking: true

qwen-3-5-35b:
provider: openai_compatible
model: qwen/qwen3.5-35b-a3b
base_url: http://127.0.0.1:1234/v1
name: "LM Studio"

qwen-3-5-9b:
provider: openai_compatible
model: qwen/qwen3.5-9b
base_url: http://127.0.0.1:1234/v1
name: "LM Studio"
params:
reasoning_effort: medium

glm-4-7-flash:
provider: openai_compatible
model: zai-org/glm-4.7-flash
base_url: http://127.0.0.1:1234/v1
name: "LM Studio"

nemotron-3-nano-4b:
provider: openai_compatible
model: nvidia/nemotron-3-nano-4b
base_url: http://127.0.0.1:1234/v1
name: "LM Studio"

lm-glm4v:
provider: openai_compatible
model: zai-org/glm-4.6v-flash
base_url: http://127.0.0.1:1234/v1
name: "LM Studio"
qwen3:
provider: openai_compatible
model: qwen/qwen3-vl-30b
base_url: http://127.0.0.1:1234/v1
name: "LM Studio"
gpt-oss-20b:
provider: openai_compatible
model: openai/gpt-oss-20b
base_url: http://127.0.0.1:1234/v1
name: "LM Studio"

tools:
# - agent
- skills
- files_list
- files_read
- files_write
# - files_edit
- files_replace
- docs_help_page
- docs_package_help_topics

options:
skills:
paths: ["~/.agents/skills"]
subagent:
# client: openai/gpt-5.4-mini
tools_allowed: [docs, files_search, files_list]
---

## Overview

btw is an R package that helps humans and LLMs work together with R by providing utilities to describe R objects, package documentation, and workspace state in LLM-friendly plain text. The package offers a flexible collection of tools that can be used interactively (copy-paste workflows), programmatically (direct function calls), or as enhanced chat clients (via ellmer or MCP servers).

The primary goal is creating a collection of tools useful to both LLMs and humans when working together with R, with an emphasis on flexibility of usage across different workflows and platforms.

## Quick Reference

- **Project type:** R Package
- **Language:** R (≥ 4.1.0)
- **Key frameworks:** ellmer (LLM chat integration), mcptools (Model Context Protocol), shiny and shinychat (chat app)

## Purpose and Design Philosophy

btw prioritizes flexibility of usage through multiple entry points:

- **`btw()`** - Interactive copy-paste workflow: gather context from R and paste into any chat interface
- **`btw_tools()`** - Register tools with ellmer chat clients for custom applications
- **`btw_client()` / `btw_app()`** - Batteries-included chat clients with your preferred LLM provider, model, and project context
- **MCP server** - Expose tools to third-party coding agents like Claude Desktop or Continue via `btw_mcp_server()`

Project configuration via `btw.md` files provides conversation stability across sessions by defining default provider, model, tools, and project-specific instructions. These files are treated as instructions for coding assistants and help avoid repeating context.

btw also serves as a laboratory for discovering best practices in LLM tool design - output formats and approaches evolve based on experimentation with what works best across different models.
19 changes: 19 additions & 0 deletions man/btw_tool_skill.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading