Skip to content
Open
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
6 changes: 5 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# btw (development version)

* `path_find_in_project()` now stops its upward search when it reaches the home directory (either `fs::path_home()` or `fs::path_home_r()`, which differ on Windows) and never treats files there as project files, so `~/AGENTS.md` or `~/CLAUDE.md` are no longer picked up as project context when the working directory is under `~` but outside a project. Home-level files are handled only by the user-level lookup. This also fixes `~/btw.md` instructions being included twice — once as the project file and once as the user-level config (#225).
* `btw_client()` and `btw_app()` now fall back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory when no user-level `btw.md` exists, mirroring the project-level precedence `btw.md` > `AGENTS.md`. `edit_btw_md()` and `use_btw_md()` are aware of the new fallback, e.g. `use_btw_md("user")` asks before creating a `btw.md` that would take priority over an existing `AGENTS.md` (#226).

* The `path_btw` argument of `btw_client()` and `btw_app()` now also accepts a named list with `project` and/or `user` fields, each `TRUE` (search the default locations), `FALSE` (skip that scope), or a path to a specific file; a missing field keeps the default for that scope. For example, `path_btw = list(user = FALSE)` skips user-level context for a single chat. A scalar path now uses only that file, no longer combining it with user-level context; pass `list(project = "path")` to keep the user-level context. The new `btw.client.path_btw_project` and `btw.client.path_btw_user` options set the global defaults for each scope (#226).

* `path_find_in_project()` now stops its upward search at the home directory (`fs::path_home()` or `fs::path_home_r()`, which differ on Windows), so files like `~/AGENTS.md` or `~/CLAUDE.md` are no longer picked up as project context when the working directory is under `~` but outside a project. Home-level files are used only by the user-level lookup, which also fixes `~/btw.md` instructions being included twice — once as the project file and once as the user-level config (#225).

* `btw_client(path_btw = ...)` no longer duplicates instructions when `path_btw` is set explicitly to the same file as the user-level `btw.md` (#225).

Expand Down
13 changes: 13 additions & 0 deletions R/btw-config.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@
#' user-level `btw.md` exists, the highest-priority one is used and btw warns
#' once per session.
#'
#' If no user-level `btw.md` exists anywhere, [btw_client()] and [btw_app()]
#' fall back to a user-level `AGENTS.md` in the cross-tool `~/.agents/`
#' directory, a convention shared with other coding agents. This mirrors the
#' project-level precedence `btw.md` > `AGENTS.md` > `CLAUDE.md`, except a
#' user-level `CLAUDE.md` is never used.
#'
#' User-level context is combined with the project-level context file by
#' default; use `path_btw = list(user = FALSE)` in [btw_client()] or
#' [btw_app()] to skip it for a single chat, or set the
#' `btw.client.path_btw_user` option to `FALSE` to change the default. The
#' `project` scope of `path_btw` has a matching
#' `btw.client.path_btw_project` option.
#'
#' On Windows, R's notion of your home directory (`fs::path_home_r()`,
#' typically your `Documents` folder) can differ from your user profile
#' directory (`fs::path_home()`); btw searches both.
Expand Down
8 changes: 6 additions & 2 deletions R/btw_chat_history_store.R
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,12 @@ btw_conversation_store_sqlite <- function() {
}

btw_app_history_project_dir <- function(path_btw = NULL) {
if (!is.null(path_btw) && !identical(path_btw, FALSE)) {
path <- fs::path_abs(fs::path_expand(path_btw))
# Only an explicit project path scopes the chat history; TRUE/FALSE fall
# through to the marker-based detection below.
project <- normalize_path_btw(path_btw)$project

if (!isTRUE(project) && !isFALSE(project)) {
path <- fs::path_abs(fs::path_expand(project))
if (fs::dir_exists(path)) {
return(as.character(fs::path_norm(path)))
}
Expand Down
152 changes: 137 additions & 15 deletions R/btw_client.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
#' `AGENTS.md` file take precedence. If a project file doesn't specify a
#' setting, btw will fall back to settings in a user-level `btw.md` file. This
#' is `~/btw.md` if present, otherwise `~/.btw/btw.md`, `~/.config/btw/btw.md`,
#' or `tools::R_user_dir("btw")`, in that order. See `?btw-config` for the
#' complete list of user-level locations. Project-level btw tool options under
#' or `tools::R_user_dir("btw")`, in that order. If no user-level `btw.md`
#' exists anywhere, btw falls back to a user-level `AGENTS.md` in the
#' cross-tool `~/.agents/` directory. See `?btw-config` for the complete list
#' of user-level locations.
#' Project-level btw tool options under
#' the `options` key are merged with user-level options, with project-level
#' options taking precedence.
#'
Expand All @@ -46,6 +49,15 @@
#' * `btw.tools`: The btw tools to include by default when starting a new
#' btw chat, see [btw_tools()] for details.`
#'
#' Two additional options are consulted when resolving context files:
#'
#' * `btw.client.path_btw_project`: `TRUE` (default) to search the default
#' project locations, `FALSE` to skip them, or a path to a specific project
#' context file. This is the default for the `project` field of `path_btw`.
#' * `btw.client.path_btw_user`: `TRUE` (default) to include user-level context
#' in new chats, `FALSE` to skip it, or a path to a specific user-level
#' context file. This is the default for the `user` field of `path_btw`.
#'
#' ## Multiple Providers and Models
#'
#' You can configure multiple client options in your `btw.md` file. When
Expand Down Expand Up @@ -103,13 +115,25 @@
#' alternatively in the shorter form `tools = "docs_help_page"`. Finally,
#' set `tools = FALSE` to skip registering \pkg{btw} tools with the chat
#' client.
#' @param path_btw A path to a `btw.md`, `AGENTS.md`, or `CLAUDE.md` project
#' context file. If `NULL`, btw will find a project-specific context file by
#' walking up the parents of the current working directory, preferring
#' `btw.md`, then `AGENTS.md`, then `CLAUDE.md`, with fallback to a
#' user-level `btw.md` file if no project file is found. See `?btw-config`
#' for the complete list of locations. Set `path_btw = FALSE` to create a
#' chat client without using a `btw.md` file.
#' @param path_btw Path to a `btw.md`, `AGENTS.md`, or `CLAUDE.md` project
#' context file, or a named list with `project` and/or `user` fields to
#' control project-level and user-level context independently. Each field
#' can be `TRUE` (search the default locations), `FALSE` (skip that scope),
#' or a path to a specific file; a missing (or `NULL`) field keeps the
#' default for that scope, set by the `btw.client.path_btw_project` and
#' `btw.client.path_btw_user` options described below.
#' If `NULL`, btw will find a
#' project-specific context file by walking up the parents of the current
#' working directory, preferring `btw.md`, then `AGENTS.md`, then
#' `CLAUDE.md`, and always combines it with the user-level context file:
#' the highest-priority user-level `btw.md`, or if none exists, a
#' user-level `AGENTS.md`. The defaults for each scope can be changed globally
#' with the `btw.client.path_btw_project` and `btw.client.path_btw_user`
#' options, e.g. `options(btw.client.path_btw_user = FALSE)`. A scalar path
#' uses only that file, skipping user-level context
#' (equivalent to `list(project = path, user = FALSE)`), and a scalar `FALSE`
#' skips all context files. See `?btw-config` for the complete list of
#' locations.
#' @param path_llms_txt A path to an `llms.txt` file containing context about
#' the current project. By default, btw will look for an `llms.txt` file in
#' the your current working directory or its parents. Set `path_llms_txt =
Expand Down Expand Up @@ -558,6 +582,85 @@ maybe_find_in_project <- function(path, file_name, arg = "path") {
path
}

# Default for a scope of `path_btw` when it isn't specified: an R option so
# users can opt out of (or pin) a scope globally, e.g.
# `options(btw.client.path_btw_user = FALSE)` in ~/.Rprofile.
btw_path_scope_default <- function(scope) {
option <- paste0("btw.client.path_btw_", scope)
value <- getOption(option, TRUE)
if (isTRUE(value) || isFALSE(value)) {
return(value)
}
check_string(value, arg = option)
value
}

# Normalize the `path_btw` argument of btw_client()/btw_app() into a list with
# `project` and `user` fields, each TRUE (search the default locations), FALSE
# (skip that scope), or a path to a specific file.
#
# Scalar values: NULL/TRUE search project and user locations (each scope's
# default comes from its `btw.client.path_btw_*` option), FALSE skips context
# entirely, and a string uses only that file as the project context, skipping
# user-level context -- a shortcut for list(project = path, user = FALSE).
#
# A named list controls each scope independently: a missing (or NULL) field
# keeps the default for that scope, from the corresponding
# `btw.client.path_btw_*` option.
normalize_path_btw <- function(path_btw, arg = "path_btw") {
if (is.list(path_btw)) {
nms <- names(path_btw)
if (
length(path_btw) == 0 ||
is.null(nms) ||
any(!nzchar(nms)) ||
!all(nms %in% c("project", "user"))
) {
cli::cli_abort(c(
"{.arg {arg}} must be a named list with {.code project} and/or {.code user} fields.",
"i" = "Each field can be {.code TRUE} (search the default locations), {.code FALSE} (skip), or a path to a file."
))
}
return(list(
project = normalize_path_btw_field(
path_btw$project,
btw_path_scope_default("project"),
paste0(arg, "$project")
),
user = normalize_path_btw_field(
path_btw$user,
btw_path_scope_default("user"),
paste0(arg, "$user")
)
))
}

if (is.null(path_btw) || isTRUE(path_btw)) {
return(list(
project = btw_path_scope_default("project"),
user = btw_path_scope_default("user")
))
}

if (isFALSE(path_btw)) {
return(list(project = FALSE, user = FALSE))
}

check_string(path_btw, arg = arg)
list(project = path_btw, user = FALSE)
}

normalize_path_btw_field <- function(value, default, arg) {
if (is.null(value)) {
default
} else if (isTRUE(value) || isFALSE(value)) {
value
} else {
check_string(value, arg = arg)
value
}
}

find_btw_context_file <- function(path = NULL, search_user = TRUE) {
# 1. Local closest btw.md file
path <- maybe_find_in_project(path, "btw.md", "path_btw")
Expand All @@ -572,9 +675,9 @@ find_btw_context_file <- function(path = NULL, search_user = TRUE) {
path <- maybe_find_in_project(NULL, "CLAUDE.md", "path_btw")
}

# 4. User btw.md file
# 4. User-level btw.md file, falling back to a user-level AGENTS.md
if (search_user && is.null(path)) {
path <- path_find_user("btw.md")
path <- path_find_user_context()
}

path
Expand Down Expand Up @@ -602,12 +705,31 @@ read_single_btw_file <- function(path) {
}

read_btw_file <- function(path = NULL) {
if (isFALSE(path)) {
return(list())
paths <- normalize_path_btw(path)

# Locate the project-level context file: search, skip, or an explicit path
project_path <- if (isTRUE(paths$project)) {
find_btw_context_file(NULL, search_user = FALSE)
} else if (isFALSE(paths$project)) {
NULL
} else {
maybe_find_in_project(paths$project, "btw.md", "path_btw")
}

project_path <- find_btw_context_file(path, search_user = FALSE)
user_path <- path_find_user("btw.md")
user_path <- if (isTRUE(paths$user)) {
path_find_user_context()
} else if (isFALSE(paths$user)) {
NULL
} else {
user_path <- fs::path_norm(fs::path_expand(paths$user))
if (!fs::file_exists(user_path)) {
cli::cli_abort(c(
"x" = "Invalid {.arg path_btw$user}: {.path {user_path}} does not exist.",
"i" = "Point {.code path_btw$user} at an existing file, or set {.code user = FALSE} to skip user-level context."
))
}
user_path
}

# If the project path was set explicitly to the same file as the user-level
# config, don't read it twice.
Expand Down
Loading
Loading