-
Notifications
You must be signed in to change notification settings - Fork 6
feat: implement Claude Skills #145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,289 @@ | ||
| #' @include tool-result.R | ||
| NULL | ||
|
|
||
| #' Tool: Fetch a skill | ||
| #' | ||
| #' @description | ||
| #' Fetch a skill's instructions and list its bundled resources. | ||
| #' | ||
| #' Skills are modular capabilities that extend Claude's functionality with | ||
| #' specialized knowledge, workflows, and tools. Each skill is a directory | ||
| #' containing a `SKILL.md` file with instructions and optional bundled | ||
| #' resources (scripts, references, assets). | ||
| #' | ||
| #' @param skill_name The name of the skill to fetch. | ||
| #' @inheritParams btw_tool_docs_package_news | ||
| #' | ||
| #' @return A `btw_tool_result` containing the skill instructions and a listing | ||
| #' of bundled resources with their paths. | ||
| #' | ||
| #' @family skills | ||
| #' @export | ||
| btw_tool_fetch_skill <- function(skill_name, `_intent`) {} | ||
|
|
||
| btw_tool_fetch_skill_impl <- function(skill_name) { | ||
|
|
||
| check_string(skill_name) | ||
|
|
||
| skill_info <- find_skill(skill_name) | ||
|
|
||
| if (is.null(skill_info)) { | ||
| available <- btw_list_skills() | ||
| skill_names <- vapply(available, function(x) x$name, character(1)) | ||
| cli::cli_abort( | ||
| c( | ||
| "Skill {.val {skill_name}} not found.", | ||
| "i" = "Available skills: {.val {skill_names}}" | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| skill_content <- readLines(skill_info$path, warn = FALSE) | ||
|
|
||
| content_start <- 1 | ||
| if (length(skill_content) > 0 && skill_content[1] == "---") { | ||
| yaml_end <- which(skill_content == "---") | ||
| if (length(yaml_end) >= 2) { | ||
| content_start <- yaml_end[2] + 1 | ||
| } | ||
| } | ||
|
|
||
| skill_text <- paste( | ||
| skill_content[content_start:length(skill_content)], | ||
| collapse = "\n" | ||
| ) | ||
|
|
||
| resources <- list_skill_resources(skill_info$base_dir) | ||
| resources_listing <- format_resources_listing(resources, skill_info$base_dir) | ||
|
|
||
| full_content <- paste0(skill_text, resources_listing) | ||
|
|
||
| btw_tool_result( | ||
| value = full_content, | ||
| data = list( | ||
| name = skill_name, | ||
| path = skill_info$path, | ||
| base_dir = skill_info$base_dir, | ||
| resources = resources | ||
| ), | ||
| display = list( | ||
| title = sprintf("Skill: %s", skill_name), | ||
| markdown = full_content | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| .btw_add_to_tools( | ||
| name = "btw_tool_fetch_skill", | ||
| group = "skills", | ||
| tool = function() { | ||
| ellmer::tool( | ||
| btw_tool_fetch_skill_impl, | ||
| name = "btw_tool_fetch_skill", | ||
| description = paste( | ||
| "Fetch a skill's instructions and list its bundled resources.", | ||
| "Skills provide specialized guidance for specific tasks.", | ||
| "After fetching, use file read tools to access references,", | ||
| "or bash/code tools to run scripts." | ||
| ), | ||
| annotations = ellmer::tool_annotations( | ||
| title = "Fetch Skill", | ||
| read_only_hint = TRUE, | ||
| open_world_hint = FALSE, | ||
| btw_can_register = function() length(btw_list_skills()) > 0 | ||
| ), | ||
| arguments = list( | ||
| skill_name = ellmer::type_string( | ||
| "The name of the skill to fetch" | ||
| ) | ||
| ) | ||
| ) | ||
| } | ||
| ) | ||
|
|
||
| # Skill Discovery ---------------------------------------------------------- | ||
|
|
||
| btw_skill_directories <- function() { | ||
|
|
||
| dirs <- character() | ||
|
|
||
|
|
||
| package_skills <- system.file("skills", package = "btw") | ||
| if (nzchar(package_skills) && dir.exists(package_skills)) { | ||
| dirs <- c(dirs, package_skills) | ||
| } | ||
|
|
||
|
|
||
| user_skills_dir <- file.path( | ||
| tools::R_user_dir("btw", "config"), | ||
| "skills" | ||
| ) | ||
| if (dir.exists(user_skills_dir)) { | ||
| dirs <- c(dirs, user_skills_dir) | ||
| } | ||
|
|
||
| project_skills_dir <- file.path(getwd(), ".btw", "skills") | ||
| if (dir.exists(project_skills_dir)) { | ||
| dirs <- c(dirs, project_skills_dir) | ||
| } | ||
|
|
||
| dirs | ||
| } | ||
|
|
||
| btw_list_skills <- function() { | ||
| skill_dirs <- btw_skill_directories() | ||
| all_skills <- list() | ||
|
|
||
| for (dir in skill_dirs) { | ||
| if (!dir.exists(dir)) { | ||
| next | ||
| } | ||
|
|
||
| subdirs <- list.dirs(dir, full.names = TRUE, recursive = FALSE) | ||
|
|
||
| for (subdir in subdirs) { | ||
| skill_md_path <- file.path(subdir, "SKILL.md") | ||
| if (file.exists(skill_md_path)) { | ||
| metadata <- extract_skill_metadata(skill_md_path) | ||
| skill_name <- basename(subdir) | ||
|
|
||
| all_skills[[skill_name]] <- list( | ||
| name = skill_name, | ||
| description = metadata$description %||% "No description available", | ||
| path = skill_md_path | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| all_skills | ||
| } | ||
|
|
||
| find_skill <- function(skill_name) { | ||
| skill_dirs <- btw_skill_directories() | ||
|
|
||
| for (dir in skill_dirs) { | ||
| skill_dir <- file.path(dir, skill_name) | ||
| skill_md_path <- file.path(skill_dir, "SKILL.md") | ||
| if (dir.exists(skill_dir) && file.exists(skill_md_path)) { | ||
| return(list( | ||
| path = skill_md_path, | ||
| base_dir = skill_dir | ||
| )) | ||
| } | ||
| } | ||
|
|
||
| NULL | ||
| } | ||
|
|
||
| extract_skill_metadata <- function(skill_path) { | ||
| lines <- readLines(skill_path, warn = FALSE) | ||
|
|
||
| if (length(lines) == 0 || lines[1] != "---") { | ||
| return(list()) | ||
| } | ||
|
|
||
| yaml_end_indices <- which(lines == "---") | ||
| if (length(yaml_end_indices) < 2) { | ||
| return(list()) | ||
| } | ||
|
|
||
| yaml_lines <- lines[2:(yaml_end_indices[2] - 1)] | ||
| yaml_text <- paste(yaml_lines, collapse = "\n") | ||
|
|
||
| check_installed("yaml") | ||
| tryCatch( | ||
| yaml::yaml.load(yaml_text), | ||
| error = function(e) list() | ||
| ) | ||
| } | ||
|
|
||
| # Skill Resources ---------------------------------------------------------- | ||
|
|
||
| list_skill_resources <- function(skill_dir) { | ||
| list( | ||
| scripts = list_files_in_subdir(skill_dir, "scripts"), | ||
| references = list_files_in_subdir(skill_dir, "references"), | ||
| assets = list_files_in_subdir(skill_dir, "assets") | ||
| ) | ||
| } | ||
|
|
||
| list_files_in_subdir <- function(base_dir, subdir) { | ||
| full_path <- file.path(base_dir, subdir) | ||
| if (!dir.exists(full_path)) { | ||
| return(character(0)) | ||
| } | ||
| list.files(full_path, full.names = FALSE) | ||
| } | ||
|
|
||
| has_skill_resources <- function(resources) { | ||
| length(resources$scripts) > 0 || | ||
| length(resources$references) > 0 || | ||
| length(resources$assets) > 0 | ||
| } | ||
|
|
||
| format_resources_listing <- function(resources, base_dir) { | ||
| if (!has_skill_resources(resources)) { | ||
| return("") | ||
| } | ||
|
|
||
| parts <- character() | ||
| parts <- c(parts, "\n\n---\n\n## Bundled Resources\n") | ||
|
|
||
| if (length(resources$scripts) > 0) { | ||
| parts <- c(parts, "\n**Scripts:**\n") | ||
| script_paths <- file.path(base_dir, "scripts", resources$scripts) | ||
| parts <- c(parts, paste0("- ", script_paths, collapse = "\n")) | ||
| } | ||
|
|
||
| if (length(resources$references) > 0) { | ||
| parts <- c(parts, "\n\n**References:**\n") | ||
| ref_paths <- file.path(base_dir, "references", resources$references) | ||
| parts <- c(parts, paste0("- ", ref_paths, collapse = "\n")) | ||
| } | ||
|
|
||
| if (length(resources$assets) > 0) { | ||
| parts <- c(parts, "\n\n**Assets:**\n") | ||
| asset_paths <- file.path(base_dir, "assets", resources$assets) | ||
| parts <- c(parts, paste0("- ", asset_paths, collapse = "\n")) | ||
| } | ||
|
|
||
| paste(parts, collapse = "") | ||
| } | ||
|
|
||
| # System Prompt ------------------------------------------------------------ | ||
|
|
||
| btw_skills_system_prompt <- function() { | ||
| skills <- btw_list_skills() | ||
|
|
||
| if (length(skills) == 0) { | ||
| return("") | ||
| } | ||
|
|
||
|
|
||
| skills_prompt_path <- system.file("prompts", "skills.md", package = "btw") | ||
| explanation <- if (file.exists(skills_prompt_path)) { | ||
| paste(readLines(skills_prompt_path, warn = FALSE), collapse = "\n") | ||
| } else { | ||
| "## Skills\n\nYou have access to specialized skills that provide detailed guidance for specific tasks." | ||
| } | ||
|
|
||
| skill_items <- vapply( | ||
| skills, | ||
| function(skill) { | ||
| sprintf( | ||
| "<skill>\n<name>%s</name>\n<description>%s</description>\n</skill>", | ||
| skill$name, | ||
| skill$description | ||
| ) | ||
| }, | ||
| character(1) | ||
| ) | ||
|
|
||
| paste0( | ||
| explanation, | ||
| "\n\n<available_skills>\n", | ||
| paste(skill_items, collapse = "\n"), | ||
| "\n</available_skills>" | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| ## Skills | ||
|
|
||
| You have access to specialized skills that provide detailed guidance for specific tasks. Skills are loaded on-demand to provide domain-specific expertise without consuming context until needed. | ||
|
|
||
| ### Using Skills | ||
|
|
||
| 1. **Check available skills**: Review the `<available_skills>` listing below | ||
| 2. **Fetch when relevant**: Call `btw_tool_fetch_skill(skill_name)` when a task matches a skill's description | ||
| 3. **Access resources**: After fetching, use file read tools to access references or bash/code tools to run bundled scripts | ||
|
|
||
| Skills may include bundled resources: | ||
| - **Scripts**: Executable code (R, Python, bash) for automated tasks | ||
| - **References**: Additional documentation to consult as needed | ||
| - **Assets**: Templates and files for use in outputs | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is auto-injected into
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be open to some sort of preferential treatment given to https://github.com/posit-dev/skills as well |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some existing tools have
tools::R_user_dir("btw", which = "cache"). Is this a reasonable directory to look in? I've used~/.config/{pkg}/in a few packages, but don't know if this is a Simon-ism. Whatever directory we land on, it should ofc be documented. :)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a helper that wraps
tools::R_user_dir()--path_btw_cache(). My take is: this is for cached files used and written by btw. I'm currently only using it for some code related to Shiny bookmarks, but I have plans to havebtw_app()remember conversation history and we'd use it there.Separately, there's
path_find_user()that considers these locations:So
~/.config/btw/skillswould be a natural place to put this that fits in with currently places we look forbtw.md. (This is only briefly mentioned in?btw_clientand?edit_btw_md.)On the other hand, I could also see
~/.btw/skillsmaking sense.What do you think about inheriting installed Claude skills too from
~/.claude/skills?Also: I like using
inst/skillsjust likeinst/prompts! I wonder how hard it is to look through installed packages to auto-discover these skills. Or maybe we'd need an option that helps bring in skills from packages.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently also
.github/skillsnow too https://code.visualstudio.com/docs/copilot/customization/agent-skills