Neovim plugin for Toolpath — records your editing history as structured provenance documents.
Every edit you make, every undo branch you abandon, captured as a DAG of steps with diffs, actors, and timestamps. When you close the buffer, the session exports a Toolpath Path document.
Neovim's undo tree is already a DAG. When you undo and make a different edit, it creates a branch — the old branch becomes a dead end. toolpath.nvim maps this directly to Toolpath's step DAG:
+-- step-3a -- step-4a (dead end: you undid this)
step-1 -- step-2 --+
+-- step-3b -- step-4b (head: where you ended up)
The plugin hooks TextChanged events, tracks undo sequence numbers, and shells
out to the path track CLI to compute diffs and build the document
incrementally. Normal edits and undo/redo navigations run asynchronously so the
UI never blocks. The plugin is thin (~350 lines of Lua) — all the heavy lifting
happens in the CLI.
- Neovim >= 0.10 (for
vim.system) - The
pathCLI binary on$PATH(or configured viabin)
Add a file like lua/plugins/toolpath.lua:
return {
"empathic/toolpath-nvim",
cmd = { "ToolpathStart", "ToolpathStatus", "ToolpathExport" },
event = "BufReadPost",
config = function()
require("toolpath").setup({
actor = "human:alex",
source = "neovim",
record = {
dir = "~/.local/share/toolpath",
},
})
end,
}event loads the plugin when a file is opened; cmd allows lazy-loading
via any :Toolpath* command before that.
Clone into your Neovim runtime path:
git clone https://github.com/empathic/toolpath-nvim \
~/.local/share/nvim/site/pack/toolpath/start/toolpath-nvimThen call setup() in your init.lua:
require("toolpath").setup({
actor = "human:alex",
source = "neovim",
record = {
dir = "~/.local/share/toolpath",
},
})The plugin/toolpath.lua file registers all :Toolpath* commands
automatically when Neovim loads the plugin from the runtime path.
require("toolpath").setup({
-- Path to the `path` binary (default: "path", found on $PATH)
bin = "/usr/local/bin/path",
-- Default actor for all sessions (default: "human:$USER")
actor = "human:alex",
-- Actor definitions — included in the Path document's meta.actors block
actors = {
["human:alex"] = {
name = "Alex",
identities = {
{ system = "github", id = "akesling" },
{ system = "email", id = "alex@example.com" },
},
},
},
-- Path-level metadata
title = "Editing session", -- optional, omitted if nil
source = "neovim", -- optional, omitted if nil
-- Session recording
record = {
dir = "~/.local/share/toolpath", -- required: where session files go
auto = true, -- default true; false = manual :ToolpathStart only
pattern = "*", -- default "*"; glob to filter files
},
})| Key | Default | Description |
|---|---|---|
dir |
(required) | Base directory for session data. live/ and archive/ subdirs are created automatically. Supports ~. |
auto |
true |
Auto-start recording when a file is opened. Set false for manual control. |
pattern |
"*" |
Glob pattern to filter which files are auto-tracked. |
Examples:
-- Record everything
record = { dir = "~/.local/share/toolpath" }
-- Only record Rust files
record = { dir = "~/.local/share/toolpath", pattern = "*.rs" }
-- Configure the output directory but start sessions manually
record = { dir = "~/.local/share/toolpath", auto = false }The directory layout created by the plugin:
~/.local/share/toolpath/
├── README.md ← explains what's here
├── live/ ← active session state files (temporary)
└── archive/ ← exported Path documents (persistent)
├── init.lua.20260217T143022.path.json
└── main.rs.20260217T150108.path.json
When a buffer is closed, the session is exported to archive/ as
<filename>.<timestamp>.path.json and the live/ state file is cleaned up.
| Command | Description |
|---|---|
:ToolpathStart [actor] |
Start tracking the current buffer. Optional actor override. |
:ToolpathStop |
Stop tracking and close the session. |
:ToolpathNote <text> |
Annotate the current head step with intent. |
:ToolpathAnnotate [text] |
Annotate the head step. Use Lua API for full control. |
:ToolpathExport [file] |
Export the Path document. Opens in a scratch buffer if no file given. |
:ToolpathStatus |
Print session status: path, actor, step count, VCS commit, version. |
require("toolpath").statusline() returns "[toolpath: N steps]" when
tracking is active, and "" otherwise.
require("lualine").setup({
sections = {
lualine_x = {
{ require("toolpath").statusline },
},
},
})vim.o.statusline = "%f %m%=%{v:lua.require('toolpath').statusline()} %l:%c"When tracking is active, the following buffer-local variables are set:
| Variable | Type | Description |
|---|---|---|
vim.b.toolpath_tracking |
boolean | true while tracking |
vim.b.toolpath_session |
string | Session state file path |
vim.b.toolpath_actor |
string | Actor for this session |
vim.b.toolpath_steps |
number | Total steps recorded |
All variables are cleared when tracking stops.
Run :checkhealth toolpath to verify your setup. It checks:
- Neovim version
pathbinary is found and is the Toolpath CLI- Session directory is writable
setup()has been called
All functions are available on the require("toolpath") module:
local toolpath = require("toolpath")
toolpath.setup(opts) -- configure the plugin
toolpath.start(actor, quiet) -- start tracking current buffer
toolpath.stop(bufnr) -- stop tracking (default: current buffer)
toolpath.note(intent) -- annotate the current head step
toolpath.annotate(opts) -- annotate any step (intent, source, refs)
toolpath.export(output_file) -- export Path document
toolpath.statusline() -- "[toolpath: N steps]" or ""
toolpath.status() -- print session info via vim.notify
toolpath.version -- "0.1.0"When a tracked file is inside a git repository, the plugin automatically anchors the session to VCS state:
- Session start:
--base-uri(from git remote) and--base-ref(HEAD commit) are passed topath track init, settingpath.basein the output. - Each edit:
--source '{"type":"git","revision":"...","branch":"..."}'is passed topath track step, landing instep.meta.source. - Cache refresh: git HEAD is cached per repo and refreshed on
FocusGainedandShellCmdPost— no git calls on every keystroke.
Multiple files in the same repo share base.ref and source.revision values,
so consumers can correlate their Path documents without session UUIDs.
Files outside a git repo are tracked normally without VCS metadata.
The plugin correctly handles undo, redo, and undo tree browsers like vim-mundo. The Toolpath DAG mirrors the Neovim undo tree — same nodes, same branches, same dead ends.
It distinguishes new edits from navigation by checking whether
undotree().seq_last increased:
- New edit (
seq_lastincreased): computes a diff and creates a Toolpath step. If multiple undo entries were created between TextChanged firings (e.g., by a plugin or macro), intermediate states are replayed to capture each entry individually. - Navigation (
seq_lastunchanged): caches the buffer content viapath track visitand inherits the step mapping from the nearest ancestor step (by walkingundotree().entries). This means branching from any point in the undo tree — even intermediate states that weren't explicit TextChanged boundaries — wires the parent correctly.
You can freely jump around the undo tree with mundo, undo/redo, g-/g+, or
:earlier/:later. Only genuinely new edits produce steps. Branches in the
undo tree map to branches in the Toolpath DAG.
Each Toolpath step contains:
- A unified diff of the change (computed by the
similarcrate) - The actor who made the change (e.g.
human:alex) - A timestamp (ISO 8601)
- Parent references wiring the step into the DAG
The full Path document includes all steps — including dead ends from abandoned undo branches — plus optional metadata (title, source, actor definitions).
Neovim CLI
+------------------+ +-------------------+
| toolpath.nvim | | path track init | -- creates session
| | --> | path track step | -- records each edit (async)
| hooks: | | path track visit | -- caches undo/redo nav (async)
| TextChanged | | path track note | -- annotates intent
| BufUnload | | path track close | -- exports + cleans up
| | +-------------------+
+------------------+
| |
v v
undo tree seq session state file
(parent tracking) (steps, diffs, DAG)
The CLI is editor-agnostic. VS Code, Emacs, or any other editor can use the same
path track subcommands.
Apache-2.0