Nothing
# Agent context assembly
# Assemble context from memory, instructions, and identity files
# for AI coding agents.
#' Load context files for an AI coding agent
#'
#' Returns assembled context (memory, project/global instructions, agent
#' identity files) tailored to a specific consumer. Files autoloaded by the
#' agent natively are skipped to avoid duplication.
#'
#' Defaults per agent:
#' \itemize{
#' \item \code{"claude"} - skips Claude Code project memory and CLAUDE.md
#' files (autoloaded by 'Claude Code'). Loads Codex memories, AGENTS.md,
#' USER.md, and SOUL.md when present.
#' \item \code{"codex"} - skips AGENTS.md and Codex memories (autoloaded by
#' Codex). Loads Claude Code project memory, CLAUDE.md, USER.md, and
#' SOUL.md when present.
#' \item \code{"corteza"} or \code{NULL} - loads everything available.
#' }
#'
#' Project instructions merge \code{CLAUDE.md} and \code{AGENTS.md}. The
#' file the consumer autoloads is left out, and so is any heading-delimited
#' section whose text the consumer already holds. Two names for one file, or
#' two identical copies, add nothing; a consumer with no autoloaded file
#' receives \code{CLAUDE.md} followed by the sections only \code{AGENTS.md}
#' has. Sections are compared verbatim, so the same rule in different words
#' is loaded from both files.
#'
#' Global instructions are resolved by trying both naming conventions and
#' picking the file relevant to the consumer: \code{~/.claude/CLAUDE.md} or
#' \code{<workspace_dir>/USER.md}.
#'
#' Override the defaults with the \code{include_*} parameters.
#'
#' @param agent Consumer identifier: \code{"claude"}, \code{"codex"},
#' \code{"corteza"}, or \code{NULL} (interactive / unknown). The legacy
#' \code{"llamar"} identifier is accepted as an alias for \code{"corteza"}.
#' @param project_dir Project directory to scan for CLAUDE.md / AGENTS.md.
#' @param workspace_dir Optional directory containing SOUL.md and USER.md
#' (e.g. \code{~/.corteza/workspace}). If \code{NULL}, those files are
#' skipped.
#' @param memory_base Base directory for 'Claude Code' project memory files.
#' @param claude_global_path Path to the global 'Claude Code' instructions
#' file. Defaults to \code{~/.claude/CLAUDE.md}.
#' @param include_memory Override default for memory inclusion. Use
#' \code{TRUE}/\code{FALSE} to force, or \code{NULL} for the agent default.
#' @param include_project Override default for project instructions
#' (CLAUDE.md / AGENTS.md).
#' @param include_global Override default for global instructions
#' (~/.claude/CLAUDE.md / USER.md).
#' @param include_soul Override default for SOUL.md inclusion.
#' @param max_memory_lines Maximum lines to include from each memory source.
#' @return Character string of assembled context, or empty string if no
#' context applies.
#' @examples
#' \donttest{
#' # Codex agent in current project
#' saber::agent_context(agent = "codex")
#'
#' # Corteza with workspace files
#' saber::agent_context(agent = "corteza",
#' workspace_dir = "~/.corteza/workspace")
#'
#' # Force-include memory regardless of agent default
#' saber::agent_context(agent = "claude", include_memory = TRUE)
#' }
#' @export
agent_context <- function(agent = NULL, project_dir = getwd(),
workspace_dir = NULL,
memory_base = file.path(path.expand("~"), ".claude", "projects"),
claude_global_path = file.path(path.expand("~"), ".claude", "CLAUDE.md"),
include_memory = NULL, include_project = NULL,
include_global = NULL, include_soul = NULL,
max_memory_lines = 100L) {
if (is.null(agent)) {
agent_key <- NA_character_
} else {
agent_key <- as.character(agent)[1L]
}
defaults <- agent_context_defaults(agent_key)
incl_mem <- include_memory %||% defaults$memory
incl_codex_mem <- include_memory %||% defaults$codex_memory
incl_proj <- include_project %||% defaults$project
incl_glob <- include_global %||% defaults$global
incl_soul <- include_soul %||% defaults$soul
parts <- character(0L)
if (isTRUE(incl_mem)) {
mem <- agent_context_memory(project_dir, memory_base, max_memory_lines)
if (length(mem) > 0L) {
parts <- c(parts, mem, "")
}
}
if (isTRUE(incl_codex_mem)) {
codex_mem <- agent_context_codex_memory(max_memory_lines)
if (length(codex_mem) > 0L) {
parts <- c(parts, codex_mem, "")
}
}
if (isTRUE(incl_proj)) {
proj <- agent_context_project(project_dir, agent_key,
forced = !is.null(include_project))
if (length(proj) > 0L) {
parts <- c(parts, proj, "")
}
}
if (isTRUE(incl_glob)) {
glob <- agent_context_global(workspace_dir, agent_key,
claude_global_path,
forced = !is.null(include_global))
if (length(glob) > 0L) {
parts <- c(parts, glob, "")
}
}
if (isTRUE(incl_soul) && !is.null(workspace_dir)) {
soul <- agent_context_soul(workspace_dir)
if (length(soul) > 0L) {
parts <- c(parts, soul, "")
}
}
paste(parts, collapse = "\n")
}
#' Per-agent default inclusion flags
#' @noRd
agent_context_defaults <- function(agent) {
if (is.na(agent)) {
return(list(memory = TRUE, codex_memory = TRUE, project = TRUE,
global = TRUE, soul = TRUE))
}
switch(agent,
claude = list(memory = FALSE, codex_memory = TRUE, project = TRUE,
global = TRUE, soul = TRUE),
codex = list(memory = TRUE, codex_memory = FALSE, project = TRUE,
global = TRUE, soul = TRUE),
corteza = list(memory = TRUE, codex_memory = TRUE, project = TRUE,
global = TRUE, soul = TRUE),
llamar = list(memory = TRUE, codex_memory = TRUE, project = TRUE,
global = TRUE, soul = TRUE),
list(memory = TRUE, codex_memory = TRUE, project = TRUE,
global = TRUE, soul = TRUE)
)
}
#' Load project memory section ('Claude Code' per-project memory)
#' @noRd
agent_context_memory <- function(project_dir, memory_base, max_lines) {
if (is.null(memory_base) || !dir.exists(memory_base)) {
return(character(0L))
}
project <- basename(normalizePath(project_dir, mustWork = FALSE))
mem_file <- NULL
mem_dirs <- list.dirs(memory_base, recursive = FALSE, full.names = TRUE)
for (md in mem_dirs) {
proj_encoded <- basename(md)
proj_name <- sub("^.*-home-[^-]+-", "", proj_encoded)
if (proj_name == project) {
candidate <- file.path(md, "memory", "MEMORY.md")
if (file.exists(candidate)) {
mem_file <- candidate
break
}
}
}
if (is.null(mem_file)) {
return(character(0L))
}
mem_lines <- readLines(mem_file, warn = FALSE)
lines <- "## Memory"
if (length(mem_lines) > max_lines) {
lines <- c(lines, mem_lines[seq_len(max_lines)],
sprintf("_... truncated (%d more lines)_",
length(mem_lines) - max_lines))
} else {
lines <- c(lines, mem_lines)
}
lines
}
#' Load Codex memories for non-Codex agents
#' @noRd
agent_context_codex_memory <- function(max_lines) {
mem_dir <- agent_context_codex_memory_dir()
if (!dir.exists(mem_dir)) {
return(character(0L))
}
files <- list.files(mem_dir, recursive = TRUE, full.names = TRUE)
if (length(files) == 0L) {
return(character(0L))
}
info <- file.info(files)
keep <- !is.na(info$isdir) & !info$isdir & !is.na(info$size) &
info$size > 0L
files <- sort(files[keep])
if (length(files) == 0L) {
return(character(0L))
}
max_lines <- max(1L, as.integer(max_lines)[1L])
remaining <- max_lines
out <- "## Codex Memories"
truncated <- FALSE
for (i in seq_along(files)) {
if (remaining <= 0L) {
truncated <- TRUE
break
}
content <- tryCatch(readLines(files[[i]], warn = FALSE),
error = function(e) character(0L))
if (length(content) == 0L) {
next
}
take <- min(length(content), remaining)
label <- substring(files[[i]], nchar(mem_dir) + 2L)
label <- gsub("\\\\", "/", label)
out <- c(out, "", sprintf("### %s", label), "", content[seq_len(take)])
remaining <- remaining - take
if (take < length(content)) {
truncated <- TRUE
break
}
}
if (length(out) == 1L) {
return(character(0L))
}
if (truncated) {
out <- c(out, sprintf("_... truncated after %d lines_", max_lines))
}
out
}
#' Resolve the Codex memory directory
#' @noRd
agent_context_codex_memory_dir <- function() {
codex_home <- Sys.getenv("CODEX_HOME", unset = "")
if (nchar(codex_home) == 0L) {
codex_home <- file.path(path.expand("~"), ".codex")
}
file.path(path.expand(codex_home), "memories")
}
#' Merge and load project instructions (CLAUDE.md and AGENTS.md)
#'
#' Loads both files, CLAUDE.md first, without the one the consumer
#' autoloads. A section whose text the consumer already holds, from the
#' autoloaded file or the file loaded before it, is dropped. Aliases and
#' identical copies therefore add nothing.
#' @noRd
agent_context_project <- function(project_dir, agent, forced = FALSE) {
paths <- file.path(project_dir, c("CLAUDE.md", "AGENTS.md"))
native <- agent_context_native_project(paths, agent, forced)
seen <- md_section_keys(agent_context_read(native))
out <- character(0L)
for (path in setdiff(paths, native)) {
new <- md_new_sections(agent_context_read(path), seen)
if (length(new$lines) == 0L) {
next
}
seen <- c(seen, new$keys)
label <- basename(path)
if (new$dropped > 0L) {
label <- paste(label, "(sections not already loaded)")
}
out <- c(out, if (length(out) > 0L) "", sprintf("## %s", label), "",
new$lines)
}
out
}
#' Project file the consumer autoloads, or NULL
#'
#' A forced include or an unknown consumer has no autoloaded file.
#' @noRd
agent_context_native_project <- function(paths, agent, forced) {
if (forced || is.na(agent)) {
return(NULL)
}
switch(agent, claude = paths[[1L]], codex = paths[[2L]], NULL)
}
#' Read an instruction file, or nothing when it is absent or unreadable
#' @noRd
agent_context_read <- function(path) {
if (is.null(path) || !file.exists(path) || dir.exists(path)) {
return(character(0L))
}
tryCatch(readLines(path, warn = FALSE), error = function(e) character(0L))
}
#' Resolve and load global instructions
#'
#' Picks claude_global_path or workspace_dir/USER.md based on consumer.
#' @noRd
agent_context_global <- function(workspace_dir, agent, claude_global,
forced = FALSE) {
user_path <- if (!is.null(workspace_dir)) {
file.path(workspace_dir, "USER.md")
} else {
NULL
}
claude_exists <- file.exists(claude_global)
user_exists <- !is.null(user_path) && file.exists(user_path)
if (!claude_exists && !user_exists) {
return(character(0L))
}
file_to_load <- NULL
if (forced || is.na(agent)) {
if (claude_exists) {
file_to_load <- claude_global
} else {
file_to_load <- user_path
}
} else if (identical(agent, "claude")) {
# claude autoloads ~/.claude/CLAUDE.md; only load USER.md
if (user_exists && !same_file(claude_global, user_path)) {
file_to_load <- user_path
}
} else {
# codex / corteza / legacy aliases / unknown: prefer claude global,
# fall back to USER.md
if (claude_exists) {
file_to_load <- claude_global
} else {
file_to_load <- user_path
}
}
if (is.null(file_to_load)) {
return(character(0L))
}
content <- tryCatch(readLines(file_to_load, warn = FALSE),
error = function(e) character(0L))
if (length(content) == 0L) {
return(character(0L))
}
label <- if (identical(file_to_load, claude_global)) {
"Global Instructions (~/.claude/CLAUDE.md)"
} else {
"User Preferences (USER.md)"
}
c(sprintf("## %s", label), "", content)
}
#' Load SOUL.md from workspace
#' @noRd
agent_context_soul <- function(workspace_dir) {
soul_path <- file.path(workspace_dir, "SOUL.md")
if (!file.exists(soul_path)) {
return(character(0L))
}
content <- tryCatch(readLines(soul_path, warn = FALSE),
error = function(e) character(0L))
if (length(content) == 0L) {
return(character(0L))
}
c("## Agent Identity (SOUL.md)", "", content)
}
#' Check if two paths resolve to the same file
#'
#' Returns FALSE if either path doesn't exist.
#' @noRd
same_file <- function(a, b) {
if (is.null(a) || is.null(b)) {
return(FALSE)
}
if (!file.exists(a) || !file.exists(b)) {
return(FALSE)
}
norm_a <- tryCatch(normalizePath(a, mustWork = FALSE),
error = function(e) a)
norm_b <- tryCatch(normalizePath(b, mustWork = FALSE),
error = function(e) b)
identical(norm_a, norm_b)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.