| Title: | LLM Wiki Engine |
|---|---|
| Description: | Personal wiki engine with a large language model (LLM) as research assistant. Supports guided sessions through a 'Claude Code' <https://github.com/anthropics/claude-code> skill bundle and autonomous research runs from R via autoresearch(). Results land in a structured vault of markdown pages with 'YAML' frontmatter and wikilinks, ready for hand-editing in your favourite editor alongside the LLM. Vaults are seeded with 'CLAUDE.md' and 'AGENTS.md' so 'Claude Code', 'Codex' <https://github.com/openai/codex>, and other agents share the same operating instructions. Can adopt an existing 'Obsidian' <https://obsidian.md/> vault in place via init_vault(adopt = TRUE). |
| Authors: | Troy Hernandez [aut, cre] (ORCID: <https://orcid.org/0009-0005-4248-604X>), cornball.ai [cph] |
| Maintainer: | Troy Hernandez <[email protected]> |
| License: | Apache License (>= 2) |
| Version: | 0.6.4.5 |
| Built: | 2026-07-07 17:27:04 UTC |
| Source: | https://github.com/cornball-ai/pensar |
Runs a bounded, package-owned research workflow. R controls the loop, source ingestion, wiki writes, index refresh, and logging; model calls are limited to structured decisions such as query planning, source selection, evidence extraction, and page drafting.
The default search backend uses Tavily via TAVILY_API_KEY. The
default model backend uses llm.api when credentials are
available and otherwise falls back to deterministic heuristics. Tests
and integrations can pass fake search_backend,
fetch_backend, and model_backend functions.
autoresearch(topic, vault = default_vault(), search_backend = NULL, fetch_backend = NULL, model_backend = NULL, program = NULL, force = FALSE, overwrite = TRUE, update = TRUE, slug = NULL, provider = "auto", model = NULL, verbose = TRUE)autoresearch(topic, vault = default_vault(), search_backend = NULL, fetch_backend = NULL, model_backend = NULL, program = NULL, force = FALSE, overwrite = TRUE, update = TRUE, slug = NULL, provider = "auto", model = NULL, verbose = TRUE)
topic |
Character. Research topic, free-form string. |
vault |
Character. Vault path. |
search_backend |
Function with signature |
fetch_backend |
Function with signature |
model_backend |
Function with signature
|
program |
Optional list or YAML path overriding the default
autoresearch program. Vault-level |
force |
Logical. Allow writes into adopted vaults. |
overwrite |
Logical. Allow planned wiki pages to overwrite
existing wiki files. Set |
update |
Logical. When |
slug |
Optional character. When supplied, force the synthesis
row's slug to this value, bypassing the title-overlap collision
guard. Useful for explicitly amending an existing wiki page
( |
provider |
Provider for the default |
model |
Optional model name for the default |
verbose |
Logical. Print phase progress. |
A list of class pensar_research with topic, program,
queries, search results, filed sources, extracted claims, written
pages, synthesis metadata, and model usage.
## Not run: vault <- file.path(tempdir(), "ar-example") init_vault(vault, rproj = FALSE, agent_instructions = FALSE) use_vault(vault) Sys.setenv(TAVILY_API_KEY = "tvly-...") res <- autoresearch("transformer scaling laws") print(res) show_page(res$synthesis$slug) ## End(Not run)## Not run: vault <- file.path(tempdir(), "ar-example") init_vault(vault, rproj = FALSE, agent_instructions = FALSE) use_vault(vault) Sys.setenv(TAVILY_API_KEY = "tvly-...") res <- autoresearch("transformer scaling laws") print(res) show_page(res$synthesis$slug) ## End(Not run)
Scans all markdown files in the vault for [[wikilinks]] that
reference the target page.
backlinks(page, vault = default_vault())backlinks(page, vault = default_vault())
page |
Page name (without |
vault |
Path to the vault directory. |
A data.frame with columns source (page name) and
file (path relative to the vault).
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("See [[seed]] for context.", type = "articles", source = "demo", vault = v) backlinks("seed", vault = v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("See [[seed]] for context.", type = "articles", source = "demo", vault = v) backlinks("seed", vault = v) unlink(v, recursive = TRUE)
Compares every non-system page pair by Jaro-Winkler title similarity
and tag-set Jaccard overlap. Pairs whose combined score exceeds
threshold are written to _proposals/dedup.md for human
review.
dedup(vault = default_vault(), threshold = 0.7)dedup(vault = default_vault(), threshold = 0.7)
vault |
Vault path. |
threshold |
Minimum combined score, in [0, 1]. Default 0.7. |
The combined score weights title similarity at 0.6 and tag overlap
at 0.4 (0.6 * jw_sim + 0.4 * tag_jaccard). Title similarity
is computed on lowercased trimmed titles; pages with no title fall
back to node_id.
Pensar never auto-merges. The proposals file is for human review.
A data.frame of proposed pairs (invisibly):
page_a, page_b, title_similarity,
tag_overlap, combined_score. Sorted by combined
score descending.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("A.", type = "articles", source = "alpha-foo", vault = v) ingest("B.", type = "articles", source = "alpha-foos", vault = v) dedup(v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("A.", type = "articles", source = "alpha-foo", vault = v) ingest("B.", type = "articles", source = "alpha-foos", vault = v) dedup(v) unlink(v, recursive = TRUE)
Returns the path of the vault pensar will act on, resolved from the
same opt-in sources as every other entry point: the
PENSAR_VAULT environment variable, a walk-up for
schema.md, then options("pensar.vault") (set by
use_vault). This is the public getter paired with the
use_vault() setter; call it to confirm which vault a bare
ingest() or status() will use before
running them.
default_vault()default_vault()
Per CRAN policy there is no implicit home-filespace fallback. If no
source is configured this errors with a setup hint rather than
guessing a path. For the resolution source as well as the path
(useful when debugging "why is it filing to the wrong vault?"), call
status(), which reports both.
Character string: the normalized vault path. Errors if no vault is configured.
use_vault to set the vault for a session.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) use_vault(v) default_vault() options(pensar.vault = NULL) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) use_vault(v) default_vault() options(pensar.vault = NULL) unlink(v, recursive = TRUE)
Writes content to raw/{type}/, generates a filename from source
and date, adds YAML frontmatter, updates index.md, and appends
to log.md.
ingest(content, type = c("articles", "chats", "briefings", "matrix"), source, title = NULL, tags = NULL, vault = default_vault(), force = FALSE)ingest(content, type = c("articles", "chats", "briefings", "matrix"), source, title = NULL, tags = NULL, vault = default_vault(), force = FALSE)
content |
Character string or character vector (lines) of content. |
type |
Content type: |
source |
Short identifier for the content source (e.g., URL, session ID, project name). |
title |
Optional title. If |
tags |
Optional character vector of tags. |
vault |
Path to the vault directory. |
force |
In adopted vaults ( |
The path to the written file, invisibly.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Hello, world.", type = "articles", source = "demo", vault = v) status(v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Hello, world.", type = "articles", source = "demo", vault = v) status(v) unlink(v, recursive = TRUE)
Calls saber::agent_context() to assemble the current
memory / project-instructions / global-instructions / identity
string for an agent, then writes it into the vault through the
existing ingest() pipeline (so the manifest, index, log,
and auto-commit all kick in).
ingest_agent_context(agent = c("claude", "codex", "corteza"), vault = default_vault(), project_dir = getwd(), workspace_dir = NULL, ...)ingest_agent_context(agent = c("claude", "codex", "corteza"), vault = default_vault(), project_dir = getwd(), workspace_dir = NULL, ...)
agent |
One of |
vault |
Vault path. |
project_dir |
Project directory passed to
|
workspace_dir |
Optional workspace dir
(e.g., |
... |
Forwarded to |
Saber stays in pensar's Suggests: the wrapper guards with
requireNamespace("saber") and errors with an install hint
if the package isn't available. It also gates on the
agent_context() export specifically so the wrapper degrades
cleanly against older saber versions (pre-0.4) that don't ship the
function, instead of failing R CMD check's static analysis or
crashing at runtime. Users who don't want this wrapper pay no
mandatory dependency cost.
Returns silently with a message (and no write) when saber returns an empty context, so the vault doesn't accumulate empty snapshots.
The relative path of the written page, invisibly. Returns
NULL invisibly when saber returns an empty context.
## Not run: v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest_agent_context("claude", vault = v) unlink(v, recursive = TRUE) ## End(Not run)## Not run: v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest_agent_context("claude", vault = v) unlink(v, recursive = TRUE) ## End(Not run)
Deprecated. Use ingest_repo() with the default
enrich = "auto" (which writes a briefing.md for R packages
under raw/repos/<name>/) instead. This function now delegates to
ingest_repo() after warning.
ingest_briefing(project = NULL, scan_dir = path.expand("~"), vault = default_vault())ingest_briefing(project = NULL, scan_dir = path.expand("~"), vault = default_vault())
project |
Project name. If |
scan_dir |
Directory to search for the project. Defaults to
|
vault |
Path to the vault directory. |
Invisibly, the path(s) written by ingest_repo().
## Not run: v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest_briefing(project = "pensar", vault = v) ## End(Not run)## Not run: v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest_briefing(project = "pensar", vault = v) ## End(Not run)
Captures repo state pinned to a commit SHA so wiki claims can cite a
specific point in time. Writes one or more artifacts under
raw/repos/<name>/<artifact>.md:
type: repo-briefing – saber digest of HEAD
(regenerable). Requires the saber package.
type: repo-ast – exported and internal symbols
from saber::symbols() (regenerable). Requires saber.
type: repo-snapshot – commit-pinned
metadata: SHA, origin URL, branch, tracked file listing, recent
commits.
ingest_repo(path, name = NULL, ref = "HEAD", enrich = c("auto", "package", "none"), artifacts = c("briefing", "ast", "snapshot"), files = NULL, tags = NULL, vault = default_vault())ingest_repo(path, name = NULL, ref = "HEAD", enrich = c("auto", "package", "none"), artifacts = c("briefing", "ast", "snapshot"), files = NULL, tags = NULL, vault = default_vault())
path |
Path to a local git repo. Tilde-expanded. |
name |
Repo identifier used as the directory name under
|
ref |
Git ref to snapshot. Default |
enrich |
One of |
artifacts |
Subset of |
files |
Optional file globs (relative to the repo root) whose
tracked paths are listed in |
tags |
Optional character vector of tags applied to every written artifact. |
vault |
Path to the vault directory. |
Re-running overwrites the artifact files in place; git tracks history.
This supersedes ingest_briefing(), which is now deprecated.
Character vector of paths written, invisibly.
## Not run: v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest_repo("~/corteza", vault = v) ## End(Not run)## Not run: v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest_repo("~/corteza", vault = v) ## End(Not run)
Fetches url (10s timeout), refuses non-2xx responses or
content types outside text/JSON/XML, and writes the body into the
vault through ingest(). If the manifest already records this
URL as a source, returns the existing page path without re-fetching.
ingest_url(url, vault = default_vault(), type = "articles", title = NULL, tags = NULL)ingest_url(url, vault = default_vault(), type = "articles", title = NULL, tags = NULL)
url |
URL to fetch. |
vault |
Vault path. |
type |
Ingest type. Default |
title |
Optional page title. If |
tags |
Optional character vector of tags. |
For HTML responses the page's <title> is extracted and used
as the page title when title is not supplied.
The relative path of the written (or existing) page, invisibly.
## Not run: v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest_url("https://example.com", vault = v) unlink(v, recursive = TRUE) ## End(Not run)## Not run: v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest_url("https://example.com", vault = v) unlink(v, recursive = TRUE) ## End(Not run)
Creates the vault directory structure and seeds the control files:
schema.md, index.md, log.md, a
.gitattributes marking log.md and the merge-conflict
digest as merge=union (concurrent appends from two vault
clones merge cleanly instead of conflicting), and (by default)
agent instruction files for Claude Code and Codex.
init_vault(path = default_vault(), rproj = TRUE, agent_instructions = TRUE, adopt = FALSE, commit = NULL, force = FALSE)init_vault(path = default_vault(), rproj = TRUE, agent_instructions = TRUE, adopt = FALSE, commit = NULL, force = FALSE)
path |
Path to the vault directory. No implicit default: pass
an explicit path, or configure one via |
rproj |
If |
agent_instructions |
If |
adopt |
Opt-in read-only adopt mode. When |
commit |
Auto-commit gate. |
force |
Write gate. |
The vault path, invisibly. Returns NULL invisibly when
the safety gate refused to scaffold.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) list.files(v, recursive = TRUE) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) list.files(v, recursive = TRUE) unlink(v, recursive = TRUE)
Scans the vault for orphan pages (no incoming wikilinks), broken wikilinks (pointing to nonexistent pages), and tag clusters with no wiki synthesis.
lint(vault = default_vault(), min_cluster_size = 3L)lint(vault = default_vault(), min_cluster_size = 3L)
vault |
Path to the vault directory. |
min_cluster_size |
Minimum number of raw pages sharing a tag to suggest a wiki page. Default 3. |
A list with class pensar_lint.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Refers to [[absent]].", type = "articles", source = "demo", vault = v) lint(v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Refers to [[absent]].", type = "articles", source = "demo", vault = v) lint(v) unlink(v, recursive = TRUE)
Appends a structured entry to log.md with timestamp, operation
type, and message.
log_entry(message, operation = "note", vault = default_vault())log_entry(message, operation = "note", vault = default_vault())
message |
Description of what happened. |
operation |
Operation type (e.g., |
vault |
Path to the vault directory. |
Invisible NULL.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) log_entry("Reviewed wiki/seed.md", operation = "review", vault = v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) log_entry("Reviewed wiki/seed.md", operation = "review", vault = v) unlink(v, recursive = TRUE)
Returns <vault>/.pensar/manifest.yml. Pensar uses this file for
per-source ingest provenance and an opt-in path -> page_uid
address map.
manifest_path(vault)manifest_path(vault)
vault |
Vault path. |
The manifest is written by ingest() and ingest_repo()
after a successful page write. Read-only operations
(vault_registry(), update_index(), and status())
never touch it. The directory is created lazily by
update_manifest(), so simply asking for the path does not
materialize .pensar/.
Absolute path to the manifest file.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) manifest_path(v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) manifest_path(v) unlink(v, recursive = TRUE)
Walks raw/briefings/, classifies each file as a briefing or AST
artifact, maps the slug to a repo name, keeps the newest file per
(repo, artifact) pair, and moves it to
raw/repos/<repo>/<artifact>.md. Older duplicates are dropped if
drop_old = TRUE (git history retains them).
migrate_briefings_to_repos(vault = default_vault(), dry_run = TRUE, drop_old = TRUE, rename_map = c(llamaR = "corteza", llamar = "corteza"))migrate_briefings_to_repos(vault = default_vault(), dry_run = TRUE, drop_old = TRUE, rename_map = c(llamaR = "corteza", llamar = "corteza"))
vault |
Path to the vault. |
dry_run |
If |
drop_old |
If |
rename_map |
Named character vector mapping legacy slug stems to
current repo names. Defaults handle the |
Wikilinks are rewritten in wiki/*.md only – raw/ is left
untouched, per the immutability rule. Aliases (e.g.
[[2026-04-30-corteza|corteza]]) are preserved.
Run with dry_run = TRUE (the default) first and review the
planned operations before committing.
Invisibly, a data.frame with columns file,
repo, artifact, action, destination.
Scans a single page for [[wikilinks]] and returns the targets.
Mirror of backlinks() in the forward direction.
outlinks(page, vault = default_vault())outlinks(page, vault = default_vault())
page |
Page name (without |
vault |
Path to the vault directory. |
A data.frame with columns target (page name) and
exists (logical: whether the target page exists in the vault).
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) fp <- ingest("Cites [[seed]] and [[missing]].", type = "articles", source = "demo", vault = v) outlinks(tools::file_path_sans_ext(basename(fp)), vault = v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) fp <- ingest("Cites [[seed]] and [[missing]].", type = "articles", source = "demo", vault = v) outlinks(tools::file_path_sans_ext(basename(fp)), vault = v) unlink(v, recursive = TRUE)
Returns the frontmatter, a short body head, the page's outlinks, and
its backlinks in one struct so callers don't have to call four
functions. Resolves name through the registry, so a query
like "Notes/Foo" works alongside the basename style.
page_context(name, vault = default_vault(), body_chars = 300L)page_context(name, vault = default_vault(), body_chars = 300L)
name |
Page name, path, alias, or page_uid. |
vault |
Vault path. |
body_chars |
Maximum chars of body to return. Default 300. |
A list with components path (relative path),
node_id, frontmatter (named list), body_head
(string), outlinks (data.frame), backlinks
(data.frame), class = "pensar_page_context".
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) fp <- ingest("Body text here.", type = "articles", source = "demo", vault = v) ctx <- page_context(tools::file_path_sans_ext(basename(fp)), vault = v) names(ctx) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) fp <- ingest("Body text here.", type = "articles", source = "demo", vault = v) ctx <- page_context(tools::file_path_sans_ext(basename(fp)), vault = v) names(ctx) unlink(v, recursive = TRUE)
Pensar ships markdown skill bundles under inst/skills/pensar/.
Returns the absolute path to the bundle root, or to a specific
skill when skill is given. Useful for symlinking pensar
skills into an agent's skill directory, e.g.
ln -s $(Rscript -e 'cat(pensar::pensar_skill_path())') \
~/.claude/skills/pensar.
pensar_skill_path(skill = NULL)pensar_skill_path(skill = NULL)
skill |
Optional skill name (e.g., |
Absolute path. Returns an empty string when the skill is
not installed (matching system.file() behavior).
pensar_skill_path() pensar_skill_path("autoresearch")pensar_skill_path() pensar_skill_path("autoresearch")
Print a pensar research session result
## S3 method for class 'pensar_research' print(x, ...)## S3 method for class 'pensar_research' print(x, ...)
x |
A |
... |
Unused. |
x, invisibly.
Returns a normalized list with version, created,
sources, and address_map fields. When the manifest
file is missing, returns a fresh empty struct with the current date
as created; the manifest file is not written.
read_manifest(vault)read_manifest(vault)
vault |
Vault path. |
Malformed YAML logs a warning and returns the empty struct so callers can keep going.
A list with components version (integer),
created (YYYY-MM-DD string), sources
(named list of per-source records), and address_map
(named list mapping relative path to page_uid).
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) read_manifest(v)$sources unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) read_manifest(v)$sources unlink(v, recursive = TRUE)
Parses entries from log.md (the format written by
log_entry()). Returns entries from the last days days,
newest first.
recent_activity(vault = default_vault(), days = 7L)recent_activity(vault = default_vault(), days = 7L)
vault |
Vault path. |
days |
Window in days. Default 7. |
A data.frame with columns timestamp (POSIXct),
operation, message, sorted newest first.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) recent_activity(v, days = 30) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) recent_activity(v, days = 30) unlink(v, recursive = TRUE)
Substring match (case-insensitive). Default scope is registry-only
fields: title, tags, and frontmatter aliases.
With in_body = TRUE the body of each page is scanned too;
that reads every file and is slower.
search_pages(query, vault = default_vault(), type = NULL, in_body = FALSE)search_pages(query, vault = default_vault(), type = NULL, in_body = FALSE)
query |
Substring to search for. |
vault |
Vault path. |
type |
Optional type filter. When supplied, only pages whose
registry |
in_body |
If |
A data.frame with columns path, node_id,
title, type, and matched_in (character
identifying where the substring was found: "title",
"tag:<tag>", "alias:<alias>", or "body").
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Body cites [[other]].", type = "articles", source = "demo-article", vault = v) search_pages("demo", vault = v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Body cites [[other]].", type = "articles", source = "demo-article", vault = v) search_pages("demo", vault = v) unlink(v, recursive = TRUE)
Returns the page content alongside its outgoing and incoming wikilinks. Use this when you need to review or edit a page: the outlinks show what raw sources the page cites; the backlinks show what depends on it.
show_page(page, vault = default_vault())show_page(page, vault = default_vault())
page |
Page name (without |
vault |
Path to the vault directory. |
A list with class pensar_page.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) fp <- ingest("Body text.", type = "articles", source = "demo", vault = v) show_page(tools::file_path_sans_ext(basename(fp)), vault = v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) fp <- ingest("Body text.", type = "articles", source = "demo", vault = v) show_page(tools::file_path_sans_ext(basename(fp)), vault = v) unlink(v, recursive = TRUE)
Returns page counts by category, total pages, and the number of
unresolved entries in the merge-conflict digest (printed as a loud
banner when nonzero, since synthesizing the digest is the first
priority for any agent in the vault).
When vault is NULL (default), the vault is resolved
via PENSAR_VAULT, walk-up from getwd(), or
options("pensar.vault"), and the source of the match is
recorded for display.
status(vault = NULL)status(vault = NULL)
vault |
Path to the vault directory. |
A list with class pensar_status, including the
resolved vault path and a source label
("env", "walkup", "walkup-subdir",
"option", or "explicit").
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) status(v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) status(v) unlink(v, recursive = TRUE)
Reads every tag from the registry, optionally compares against a
taxonomy file (_meta/taxonomy.md, a markdown bullet list of
allowed tags), and writes a proposals report to
_proposals/tags.md. Unknown tags get near-miss suggestions
via Jaro-Winkler distance against the taxonomy.
tags(vault = default_vault(), taxonomy = NULL, near_miss_threshold = 0.15)tags(vault = default_vault(), taxonomy = NULL, near_miss_threshold = 0.15)
vault |
Vault path. |
taxonomy |
Optional path to a taxonomy file. Defaults to
|
near_miss_threshold |
Maximum Jaro-Winkler distance for an unknown tag to be suggested as a typo of a taxonomy entry. Default 0.15. |
Pensar never auto-renames. The proposals file is for human review.
A list with components used (data.frame of tag /
count), unknown (data.frame of unknown tags with optional
suggestions), and unused_taxonomy (character vector of
taxonomy entries with zero usage). Invisible.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("hi", type = "articles", source = "demo", tags = c("foo", "bar"), vault = v) tags(v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("hi", type = "articles", source = "demo", tags = c("foo", "bar"), vault = v) tags(v) unlink(v, recursive = TRUE)
Scans all markdown files in the vault and regenerates index.md
as a categorized catalog with wikilinks and titles.
update_index(vault = default_vault())update_index(vault = default_vault())
vault |
Path to the vault directory. |
The path to index.md, invisibly.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Body.", type = "articles", source = "demo", vault = v) update_index(v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Body.", type = "articles", source = "demo", vault = v) update_index(v) unlink(v, recursive = TRUE)
Patches a manifest record for one page. Writes only
.pensar/manifest.yml; never edits other tools' manifest
formats (.manifest.json, .raw/.manifest.json).
update_manifest(vault, source = NULL, path = NULL, page_uid = NULL, address = NULL, hash = NULL, ingested_at = NULL)update_manifest(vault, source = NULL, path = NULL, page_uid = NULL, address = NULL, hash = NULL, ingested_at = NULL)
vault |
Vault path. |
source |
Source identifier (URL, session id, etc.). Optional. |
path |
Relative path inside the vault that this update is about. Required if any of the other fields are set. |
page_uid |
Stable page identity from frontmatter
|
address |
Alias for |
hash |
Content hash (typically |
ingested_at |
Timestamp string. Defaults to current time when any source-shaped field is set. |
source, hash, or ingested_at together write or
refresh a sources[path] record. page_uid (or
address, which is treated as an alias) writes an
address_map[path] record. A call with only path and
page_uid updates the address map without touching the
sources record.
The manifest path, invisibly.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) update_manifest(v, source = "demo", path = "raw/articles/demo.md", hash = "sha1:abc") read_manifest(v)$sources[["raw/articles/demo.md"]] unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) update_manifest(v, source = "demo", path = "raw/articles/demo.md", hash = "sha1:abc") read_manifest(v)$sources[["raw/articles/demo.md"]] unlink(v, recursive = TRUE)
Sets options("pensar.vault") so subsequent pensar calls
resolve to path without repeating the argument. Persist by
adding pensar::use_vault("~/wiki") to ~/.Rprofile as
a global default. Both PENSAR_VAULT and a project-local
schema.md found via walk-up (in the current directory or a
vault/ subdir) will override this option (see
default_vault resolution order).
use_vault(path)use_vault(path)
path |
Path to your pensar vault directory. |
The resolved path, invisibly.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) use_vault(v) status() options(pensar.vault = NULL) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) use_vault(v) status() options(pensar.vault = NULL) unlink(v, recursive = TRUE)
No-op if the vault is not inside a git repo or if there are no
changes. Stages all changes under the vault (respecting
.gitignore), commits with the given message, and optionally
pushes to remotes.
vault_commit(message, vault = default_vault(), push = NULL)vault_commit(message, vault = default_vault(), push = NULL)
message |
Commit message. |
vault |
Path to the vault directory. |
push |
If |
The vault may be its own repo, or nested inside a larger
repo (e.g. a project repo with the vault at vault/). Nested
vaults are handled conservatively: staging and the commit are
scoped to the vault subtree, so unrelated changes elsewhere in the
enclosing repo – dirty or even already staged – are never swept
into a pensar commit. Nested vaults also never auto-push:
PENSAR_AUTO_PUSH is ignored, since pushing the enclosing
repo could carry unrelated unpushed commits. An explicit
push = TRUE pushes the enclosing repo's branch with a single
plain push (no rebase-retry automation).
For a vault that is its own repo, the push default is
configurable at two levels. Per vault: set auto_push: false
in schema.md frontmatter to turn auto-push off for that
vault alone (auto_push: true likewise forces it on,
overriding the environment). Per machine: the
PENSAR_AUTO_PUSH environment variable, where "0" or
"false" (case-insensitive) skips the push step. The
explicit push argument beats both. When nothing is
configured, pushes go to every configured remote. A rejected push
(another author pushed first) is retried once after
git pull --rebase; concurrent appends to log.md
rebase cleanly because init_vault() marks it
merge=union, and a stopped rebase is resolved by the
vault_merge() engine. A rebase that still fails is aborted,
leaving the local commit unpushed for manual reconciliation.
TRUE if a commit was made, FALSE otherwise
(invisibly).
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) # Returns FALSE invisibly: no .git in this temp vault. vault_commit("noop", vault = v, push = FALSE) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) # Returns FALSE invisibly: no .git in this temp vault. vault_commit("noop", vault = v, push = FALSE) unlink(v, recursive = TRUE)
Renders every markdown page in the vault to HTML, resolving
[[wikilinks]] to relative anchor tags. Output is a standalone
site that can be served from any static file server or opened via
file://.
vault_export(vault = default_vault(), out_dir = default_site_dir())vault_export(vault = default_vault(), out_dir = default_site_dir())
vault |
Path to the vault directory. |
out_dir |
Destination directory. No default: pass an explicit
path or set |
No default out_dir: pass an explicit path or set the
PENSAR_SITE_DIR environment variable. Per CRAN policy
pensar will not silently render into a home-filespace location
(e.g., tools::R_user_dir()). PENSAR_SITE_DIR is the
recommended escape hatch – point it at a Syncthing folder so edits
propagate to other devices on export.
Requires the pandoc command-line tool to be available.
Export is incremental. The first export to a given out_dir is a
full build; subsequent exports re-render only pages whose source
changed, plus any page whose wikilinks point at a name that was
added, removed, or moved (so broken/resolved links stay correct).
State is kept in .pensar-export-cache.yml inside out_dir;
delete it to force a full rebuild. The stylesheet and site index are
always regenerated (they don't invoke pandoc).
The output directory path, invisibly.
if (nzchar(Sys.which("pandoc"))) { v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Body.", type = "articles", source = "demo", vault = v) vault_export(v, out_dir = tempfile("site-")) unlink(v, recursive = TRUE) }if (nzchar(Sys.which("pandoc"))) { v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Body.", type = "articles", source = "demo", vault = v) vault_export(v, out_dir = tempfile("site-")) unlink(v, recursive = TRUE) }
Scans every markdown page in the vault (excluding control files),
extracts [[wikilinks]] as edges, and renders the result via
saber::graph_svg(). Node tooltips carry the page type, tags,
and date from YAML frontmatter; broken wikilinks (targets with no
matching page) appear as external nodes with a distinct tooltip.
vault_graph(vault = default_vault(), width = 1600L, height = 1200L, ...)vault_graph(vault = default_vault(), width = 1600L, height = 1200L, ...)
vault |
Path to the vault directory. |
... |
Passed through to |
width, height
|
Viewport in pixels. Defaults (1600 x 1200) are
larger than |
Character vector of SVG lines. Write with writeLines().
## Not run: # Requires a version of 'saber' that exports graph_svg(). v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Cites [[other]].", type = "articles", source = "demo", vault = v) svg <- vault_graph(v) writeLines(svg, tempfile(fileext = ".svg")) ## End(Not run)## Not run: # Requires a version of 'saber' that exports graph_svg(). v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Cites [[other]].", type = "articles", source = "demo", vault = v) svg <- vault_graph(v) writeLines(svg, tempfile(fileext = ".svg")) ## End(Not run)
Resolves every conflicted path mechanically where the correct
result is computable, and files genuine divergence into a committed
digest at .pensar/merge-conflicts.md for later synthesis.
The vault is never left mid-merge: after resolution the rebase is
continued (or the merge committed).
vault_merge(vault = default_vault())vault_merge(vault = default_vault())
vault |
Path to the vault directory. |
Resolution by class:
index.md: regenerated from the merged tree.
.pensar/manifest.yml: unioned by path and pruned to
the merged tree (see merge_manifest_structs()).
log.md and the digest itself: line union (both
sides' appended lines survive), covering vaults whose
.gitattributes predates the union-merge scaffold.
Raw add/add collisions: both files kept; the incoming side is renamed with a numeric suffix and its manifest record is re-keyed. Raw is ground truth; nothing is discarded.
Wiki (and other markdown) pages: a strict superset side wins outright; otherwise the side with valid frontmatter and strictly fewer broken wikilinks wins and the discarded version goes to the digest; otherwise the current branch's side is kept and both full versions go to the digest.
Digest entries carry a content-derived id (path + both blob shas + class), so re-running the resolution is idempotent. Entries embed both full versions inside code fences and use plain paths, never wikilinks, so the digest stays out of the link graph.
Nested vaults (a vault directory inside a larger repo) are resolved conservatively: only conflicts under the vault subtree are touched. If the stopped merge or rebase also has conflicts outside the vault, the vault resolutions are staged and the merge/rebase is left in place for the human to finish; pensar never completes a merge that still has non-vault conflicts, and never aborts one it did not start.
Invisibly, a data frame with one row per resolved path
(columns path, class, kept, digested),
or NULL when no merge or rebase was in progress.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) # No git repo, nothing stopped: returns NULL with a message. vault_merge(v) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) # No git repo, nothing stopped: returns NULL with a message. vault_merge(v) unlink(v, recursive = TRUE)
Scans every .md file in vault and returns a data.frame
with one row per page. Used internally to resolve wikilinks and
query frontmatter without re-scanning the filesystem on every call.
vault_registry(vault = default_vault(), cache = c("session", "user", "none"), refresh = FALSE)vault_registry(vault = default_vault(), cache = c("session", "user", "none"), refresh = FALSE)
vault |
Vault path. |
cache |
Cache policy: |
refresh |
If |
Identity columns:
node_id is the current link-resolution identity
(path-aware basename via name_from_path()). This is what
[[page]] matches against.
page_uid is a stable identity sourced from frontmatter
id: or address:. NA if the page declares
neither. Stable identity is opt-in via frontmatter; pensar never
fabricates one from a path hash, because path hashes change on
rename.
Cache levels:
"session" (default): memoized in a package-level
environment, keyed by vault path. No disk write. Invalidates when
any .md file's mtime changes.
"user": persisted to
tools::R_user_dir("pensar", "cache"). CRAN-safe location;
never writes inside the vault itself (.pensar/ is reserved
for vault-owned state).
"none": rebuild on every call.
A data.frame with columns: path, node_id,
page_uid, title, aliases, type,
category, tags, sources, links_out,
system_file. Aliases / tags / links_out are list-columns.
The type and category fields come from frontmatter
verbatim; callers compute an effective type as type
when present, falling back to category otherwise.
v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Body cites [[other]].", type = "articles", source = "demo", vault = v) reg <- vault_registry(v) nrow(reg) unlink(v, recursive = TRUE)v <- tempfile("vault-") init_vault(v, rproj = FALSE, agent_instructions = FALSE) ingest("Body cites [[other]].", type = "articles", source = "demo", vault = v) reg <- vault_registry(v) nrow(reg) unlink(v, recursive = TRUE)