A dailies picker (zkd-style): lists the last `journal_days` (default 7) days newest-first, previews existing journals and shows "@create" for new ones, and opens the chosen day (creating if new). Journals resolve by their ISO-date title, so no new RPC is needed. picker.select gains an optional Telescope preview pane. e2e covers the recent-days list (exists/@create across a month boundary) and open-on-pick via a stubbed vim.ui.select. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.3 KiB
| title | modified | tags | ||
|---|---|---|---|---|
| heph.nvim | 2026-06-01 |
|
heph.nvim
The primary user surface (tech-spec §8): a Neovim plugin that replaces
obsidian.nvim and is a thin client of the local hephd over its
unix-socket JSON-RPC. Notes, journals, and tasks are edited as ordinary
buffers; the daemon owns all storage and sync. Built in checkpointed slices on
feature/v1-prototype; this card tracks the stable surface as it lands.
Architecture
heph.nvim/lua/heph/ modules, each small and single-purpose:
| Module | Responsibility |
|---|---|
rpc |
libuv (vim.uv) unix-socket JSON-RPC client. A blocking call() is built over the async pipe by pumping the loop with vim.wait until the matching id returns. Demuxes responses by id; partial lines are buffered; JSON null decodes to Lua nil (luanil). A Session is one connection — the module keeps a default singleton and lets tests open isolated sessions. |
node |
Buffer-backed nodes. A node is a buffer named heph://node/<id> with buftype=acwrite; BufReadCmd loads the body via node.get, BufWriteCmd saves the whole buffer via node.update. |
link |
Parse the [[wiki-link]] under the cursor (mirroring extract.rs grammar) and follow it via node.resolve (exact, never fuzzy search). Unresolved links are allowed. |
journal |
Open/create a dated journal node (idempotent — deterministic id). |
daemon |
Managed-daemon lifecycle: ensure (connect if a daemon already serves the socket, else spawn one we own), stop_spawned (kill only what we spawned, on exit), readiness-poll. Shared with the e2e harness. |
config / init |
setup(opts), socket resolution, default keymaps. |
command |
The :Heph <subcommand> dispatch + completion. |
Surfaces never touch SQLite — every operation is a daemon RPC (tech-spec §3). The plugin is mode-agnostic: Tactical/Strategic/Organizational are plugin-side compositions of daemon primitives, not daemon concepts.
Daemon lifecycle
setup({}) is plug-and-play by default (autostart = true): if nothing is
serving the socket, the plugin spawns a local hephd against the default XDG
paths, kills only the daemon it spawned on VimLeavePre, and self-heals —
rpc.call retries once through a respawn hook if the connection drops. It only
ever spawns when nothing is already serving the socket, so a server/client
daemon you started is respected. With autostart = false the plugin connects
only and warns/errors if unreachable — for when you run your own daemon. The
$HEPH_SOCKET / $HEPH_DB env knobs (and mise run dev) isolate a dev Neovim
onto a separate daemon + DB so real data is never touched.
Daemon RPC dependencies
Beyond the existing methods (tech-spec §6), the plugin relies on
node.resolve {title} → Node | null: an exact, owner-scoped,
non-tombstoned alias-then-title match — the same mapping the store uses to
materialize wiki links, so "follow link under cursor" jumps to the same
node the stored link points at. When the target doesn't resolve, follow
creates a doc with that title (the zettelkasten follow-or-create gesture)
and materializes the source's backlink (saving the source if it has unsaved
edits, else adding the wiki link directly).
Commands (as of slice 11c)
| Command | Action |
|---|---|
:Heph home |
Open the home / index landing page (created on first use; title via opts.home) |
:Heph today / :Heph journal <YYYY-MM-DD> |
Open today's / a dated journal |
:Heph journals |
Pick among recent days (preview existing, @create for new); count via opts.journal_days |
:Heph follow (also <CR> in a node buffer) |
Follow the [[link]] under the cursor — creating the target doc if it doesn't exist yet |
:Heph doc <title> |
Create (and open) a new wiki doc |
:Heph open <id> |
Open a node buffer by id |
:Heph search <query> |
Full-text search; pick a result to open |
:Heph next [scope] |
Tactical "what is next?" view (<CR> opens a task's context) |
:Heph list [attention] |
Organizational survey of the outstanding set |
:Heph capture <title> |
Capture a committed task (pick attention) |
:Heph attention [color] |
Set the current task's attention |
:Heph done / :Heph drop / :Heph skip |
State change on the current task |
:Heph promote [attention] |
Promote the - [ ] line under the cursor to a committed task |
:Heph log <text> |
Append a breadcrumb to the current task's log |
"Current task" is resolved from the buffer: a task node, or a canonical-context
doc whose owning task is followed via its canonical-context backlink. The
next/list views render the titled rows the daemon returns (list enriched to
carry titles + the context id, so no N+1 node.get). Pickers use built-in
vim.ui.select, auto-upgrading to Telescope when installed.
Promotion (:Heph promote) mints a committed task from the - [ ] line
under the cursor (the daemon's task.promote, item_ref = the cursor item's
1-based index among context items, computed code-fence-aware to mirror
extract.rs) and rewrites that line into a [[link]] to the new task. To keep
that link unambiguous, wiki-link resolution excludes canonical-context docs, so
[[Task Title]] resolves to the task, not its identically-titled context doc.
Testing (tech-spec §9)
The headless e2e suite drives the plugin in nvim --headless against a real
hephd over a temp socket, asserting both buffer contents and resulting DB
state (via an isolated RPC session). It uses a self-contained busted-style
runner (tests/e2e/runner.lua) — no external plugins, no network — so it is
deterministic. mise run test-nvim builds the daemon and runs the suite against
system-installed Neovim; a deliberately failing spec exits non-zero (no
false-green; the runner also fails if it discovers zero specs). CI runs the same
suite through the test-nvim Dagger function (.dagger/, invoked by
build.yaml as dagger call test-nvim), which bakes a pinned, arch-detected
Neovim onto a Rust image, builds hephd, and runs the suite — reproducible, and
identical to the native mise run test-nvim path.