Releases: Sachchaa/better-md
Release list
v0.10.0 — a terminal that reads like a document
better-md updateOr, first time:
curl -fsSL https://better-md.dev/install.sh | shPlans that read like documents in the terminal
better-md --plan -t looked crude, and the cause was structural rather than a matter of taste: the renderer had no colour vocabulary at all — only bold, dim, italic, underline and reverse video. With no colour available, "this is code" had nowhere to go except reverse video, so a plan mentioning a dozen identifiers rendered as a page of filled blocks. It read like a redacted document. Measured on a sample plan: 16 uses of reverse video, zero colour.
| Before | Now | |
|---|---|---|
| Inline code | reverse video | a soft blue |
# and ## |
plain, with a ═══ rule sized to the text |
the brand green, bold, no rule |
### |
no treatment at all — rendered as prose | bold |
| Table and fence borders | full strength, louder than the content | receded |
| Table width | content width, ragged right edge | fills the pane |
How much colour depends on what your terminal admits to. COLORTERM gets exact 24-bit colour — the same green as the logo. A 256color terminal gets the nearest palette index. A plain xterm gets the basic sixteen rather than nothing, because an ssh default should not be monochrome.
Inline code is deliberately not the brand colour. Code appears constantly in a plan, and in green it would compete with every heading on the page.
Piping is unchanged and still plain. better-md plan.md -t | less and > plan.txt emit no escape codes at all. With no colour to lean on there, headings keep a rule underneath — it is a fallback for exactly that case, not decoration, which is why a colour terminal no longer shows one. NO_COLOR is honoured.
Reverse video now means one thing only: selection. The status bar, the outline's current entry, the current search match. Never content.
The brand, in the product
The logo now appears where the product names itself. The website and README have carried it since it landed, but the editor the binary serves is embedded, so it only arrives here: it titles itself better-md rather than Markdown Dashboard — a name the product has never had — its header names the product instead of saying "Markdown", and it finally has a favicon.
v0.9.0 — better-md stop
better-md updateOr, first time:
curl -fsSL https://better-md.dev/install.sh | shStop a detached server
better-md stop # all of them
better-md stop ./docs # just that workspaceStopped 1 session.
/Users/me/project/docs
--detach leaves servers running and v0.8.0's --sessions showed you which. Ending one still meant reading a pid out of that listing and running kill.
That is worth a command rather than a documented pipeline, for one reason: a pid from a listing can be stale by the time you act on it. The server may have exited and the operating system handed that number to something else, and then kill hits a bystander. stop re-confirms each server is ours — an authenticated request it answers — immediately before signalling it. A shell pipeline cannot close that window.
It will not escalate on your behalf. SIGTERM only, so the server runs its own shutdown: closes the port, removes its record. If one ignores that, you get the pid and the command to force it rather than a silent kill -9, because a forced exit skips the shutdown handler and can leave a half-written file behind. That decision is yours.
The workspace argument is resolved the way the workspace itself was — absolute, then through symlinks, with a file resolving to its directory — so stop ./docs, stop ./docs/plan.md and a symlink to the same directory all match.
It is a subcommand rather than a flag because it acts, the same line that made --sessions a flag: subcommands act and may take arguments, flags ask and take none. stop is a verb, so unlike a plural noun it shadows no plausible directory — ./stop still opens one.
Foreground servers are untouched, because a plain better-md notes.md records nothing to find. Ctrl-C ends those.
v0.8.0 — better-md --sessions
better-md updateOr, first time:
curl -fsSL https://better-md.dev/install.sh | shSee what is running
better-md --sessions2 sessions running.
/Users/me/project/docs
http://127.0.0.1:8080 pid 47745
/Users/me/other/plans
http://127.0.0.1:60838 pid 47747
--detach leaves a server behind on purpose — the init claude hook relies on it, and so does b in terminal mode — which made "what do I have running?" a question the CLI could not answer. Finding out meant reading ~/.better-md/sessions/*.json by hand, and those files hold each server's bearer token, so the obvious cat puts a credential in a terminal you might paste from. This prints the workspace, the address and the pid, and never the token.
With nothing running it says so, and says why there might be nothing: only detached runs record themselves, and an ordinary run writes nothing outside the workspace.
Stale records are pruned as it lists. A record outlives its server whenever the process could not run its shutdown handler — kill -9, a crash, a reboot. Liveness is proven by asking the server rather than by checking the pid, because ports and pids are both recycled, so a pid that happens to exist proves nothing about whose it is.
It is a flag, not a bare sessions word, so a directory called sessions still opens as one. The line this follows: subcommands act and may take arguments (update, uninstall, init claude), flags ask and take none (--check-updates, --version, --sessions).
Fixed
A hung server could wedge the CLI indefinitely. The liveness check had no timeout, and Node's fetch has no default one, so a process still holding its port but no longer replying would hang both this listing and --detach's reuse check — the one the Claude Code hook runs on every finished plan. It is now bounded, and aborts the request rather than abandoning it.
v0.7.0 — terminal mode
better-md updateOr, first time:
curl -fsSL https://better-md.dev/install.sh | shRead a plan in the terminal
better-md --plan -t
better-md notes.md --terminalRenders the document where you already are, with no server and no browser. Same file resolution as before, so --plan and --agent work exactly as they do in browser mode.
Live reload is the point of it. The pane updates as your agent rewrites the plan, and it holds the spot you were reading inside a section rather than snapping back to that section's heading — with an agent writing continuously, anchoring to the heading alone would pull you back every few hundred milliseconds.
| Key | |
|---|---|
j k ↓ ↑ |
scroll a line |
space f / u PgUp |
page forward / back |
g G |
start / end |
/ then n N |
search, cycle matches |
o |
heading outline |
e |
open in $EDITOR |
b |
hand off to the browser editor |
? |
every key |
q |
quit |
Search matches literally, never as a pattern: plan.md will not match planXmd, and typing a [ finds a bracket instead of crashing the viewer.
e uses $VISUAL then $EDITOR and guesses nothing — with neither set it tells you what to set rather than dropping you into whatever happens to be installed. b reuses a server already serving that workspace instead of starting a second one, the same path --detach and init claude use.
Piping prints plain text and exits, so better-md plan.md -t | less and > plan.txt both work.
What it does not do: there is no editing in the terminal — e and b are how you get to an editor. Code blocks are not syntax highlighted, and long lines inside them are truncated rather than wrapped, because reflowing code changes what it says.
Fixed
better-md plan.md -t | head printed a Node stack trace. A reader closing the pipe mid-write surfaced as an unhandled error on stdout, and the trace landed on top of what you were reading. Quitting a pager early does the same thing.
Under it
No new runtime dependencies — there are still none. The TUI is hand-written ANSI over Node's raw mode, which is worth a few hundred lines to keep the binary something you can verify against a published checksum.
The Markdown block parser is now shared between the browser renderer and the terminal one, so the two cannot drift on what a document means. The 44 existing renderer tests pass unmodified, XSS cases included.
Terminal restoration got the most attention, because a shell left in raw mode with no cursor needs reset and most people do not know that. Verified in a real pty: stty -g byte-identical before and after, for q, Ctrl-C and an external SIGTERM. q exits 0 and Ctrl-C exits 130 — in raw mode Ctrl-C arrives as a byte rather than a signal, so nothing else could tell the two apart.
v0.6.0
better-md updateOr, first time:
curl -fsSL https://better-md.dev/install.sh | shLet the agent open the plan
better-md init claude --writeAdds a Claude Code hook that opens each finished plan the moment it is written, so reviewing one stops being another command to remember. Run /hooks once afterwards so Claude Code reloads its config.
The hook runs better-md --plan --detach, and both halves matter. --plan holds a server open until you stop it, so a blocking hook would hang the very turn that produced the plan; the hook is async and --detach returns in about 0.3s. It also reuses a server already serving those plans instead of starting another — firing it three times leaves one process, not three, which is the difference between a workflow and a memory leak.
init reads, merges and writes; it never replaces. Tested against a settings file holding a model, permissions and an existing hook — all preserved. It refuses outright if settings.json is not valid JSON, because a malformed one disables every setting in it. Preview is the default; --write applies it. Running it twice is a no-op.
--detach works on its own too, for any workspace.
Agent adapters
--plan now opens whichever supported agent wrote most recently, so it means "the plan I was just looking at" without naming a tool. --agent claude or --agent cursor picks one.
| Agent | Plans |
|---|---|
claude |
~/.claude/plans |
cursor |
~/.cursor/plans |
Codex is deliberately absent: it keeps sessions in a sqlite log and writes no plan files, so an adapter for it could never resolve. A test pins that so it stays a decision rather than an oversight.
One agent at a time rather than all of them merged into one listing — a workspace has a single root, and the path confinement the file API depends on is derived from that.
Fixed: Claude Code's plansDirectory setting was ignored entirely, so anyone who had set it was silently pointed at ~/.claude/plans instead. It is now honoured, resolved against the project root as its schema specifies, with project settings winning over user settings.
One new file on disk
Reuse needs somewhere to record where a detached server is listening, so ~/.better-md/sessions now exists — one record per workspace. Only detached runs create it; an ordinary better-md notes.md still writes nothing outside the workspace, and better-md uninstall removes it along with the binaries.
v0.5.0
better-md updateOr, first time:
curl -fsSL https://better-md.dev/install.sh | shNested lists
Indentation used to be discarded outright, so every list rendered flat — a plan with sub-tasks lost its structure the moment you opened it. That was the last big gap for the documents this tool exists to read.
- [x] Tokenizer
- [x] Unicode word boundaries
- [ ] CJK segmentation
- [ ] Inverted index
1. Delta-encode postings
2. Intersect as a linear merge
- benchmark at 100k docsAny depth, bullets and numbers mixing freely, task lists nesting like anything else, tabs counted as four columns.
Nesting is parsed from relative indentation rather than a fixed step, because - and 1. are different widths and real documents indent children accordingly. Serialising back indents each level by the parent's marker width — two columns under - , three under 1. . That is not cosmetic: it is what makes the structure survive a reparse, both here and in every other Markdown tool that reads the file.
Because the preview is editable, this had to work in both directions. Typing into the live preview forces a full re-serialise, and the source comes back byte-identical.
The renderer now covers
Headings, emphasis, inline and fenced code, links, images, blockquotes, horizontal rules, tables with column alignment, task lists, and nested lists.
Still not handled: reference-style links, footnotes, setext headings, and paragraphs continued under a list item. Raw HTML is escaped rather than passed through, deliberately.
Clearer about what this is for
The pitch led with "Your Markdown files, in a real editor", which reads as a general Markdown app and buried the thing that actually makes it different. It now says what it is for — reviewing what your coding agent writes — on the website, in the README, in the installer's output, and in --help, whose command list also puts --plan first instead of third.
v0.4.0
better-md updateOr, first time:
curl -fsSL https://better-md.dev/install.sh | shbetter-md uninstall
removed /Users/you/.local/bin/btr-md
removed /Users/you/.local/bin/better-md
better-md is gone. Nothing else was left on disk.
The docs used to say "delete the two files it reports", and the obvious way to do that is wrong: rm $(command -v better-md) removes one of them and leaves btr-md on your PATH — still runnable, still 110 MB. That is why this is a command and not a documentation fix.
It is named uninstall rather than remove on purpose. This is a document editor, and better-md remove notes.md reads like "delete this document" — not a word worth leaving ambiguous. remove is accepted as an alias anyway, so typing it is not a dead end.
A btr-md that is not a byte-copy of the running binary belongs to someone else. It is left alone, and the fact is printed rather than passed over in silence. Siblings are removed before the running binary, so a failure part-way still leaves a working better-md to retry with, and a permission error suggests the sudo form instead of an errno.
Nothing else is left behind, verified rather than asserted: better-md writes no config, cache or state directory, and the editor stores nothing in the browser — no localStorage, no cookies. The only files it ever writes are the documents you save and, during update, a staging file it renames over itself.
Where to find it
The uninstall instruction previously lived in a comment inside install.sh — invisible to anyone piping it to sh — and one clause in the middle of a README paragraph. It is now in --help, in the message the installer prints, in the command table on the website, and in a section of its own.
v0.3.1
better-md updateOr, first time:
curl -fsSL https://better-md.dev/install.sh | shFinding your way in
Two small things, both about the first minute with the tool.
better-md on its own now shows the help and exits 0. It used to answer missing a file or directory argument and exit 1 — a dead end for the most likely thing anyone types first. Flags without a target (better-md --no-open) still error, since that is a mistake rather than a question.
The installer now says what to do next. It used to end with a single Try it: better-md --plan, which gives one example without hinting that anything else exists:
Open Markdown files from your disk in a real editor.
better-md --plan your newest Claude Code plan
better-md notes.md a single file
better-md ./docs every Markdown file in a directory
better-md --help all commands and options
better-md update upgrade to the latest release
The PATH note now appears only when your install directory actually is not on PATH.
Also
Test-suite flakiness introduced by the 8080 default in v0.2.0: the server and end-to-end suites spawned without a port, so they competed for 8080 with anything else on the machine and failed when it was taken. They pin an explicit port now — no change to the shipped binary's behaviour.
v0.3.0
On v0.2.0? This is the first release you can pull down with the command itself:
better-md updateOtherwise:
curl -fsSL https://better-md.dev/install.sh | shTables and task lists now render
The two constructs coding agents put in plans most often were the two the renderer did not handle — a table collapsed into a paragraph of pipes, and - [x] showed literal brackets.
Both work now, in both directions, which matters because the preview is editable: a construct that renders but does not serialise back is data loss, not a cosmetic gap. Editing the live preview and watching the source pane re-serialise keeps the table, its column alignment, and both checkbox states intact.
- Tables — GFM style. A header row plus a delimiter row, which is also what keeps prose containing pipes from turning into a table by accident.
| :-- | :-: | --: |sets per-column alignment.\|inside a cell is content, not a separator; without that a cell mentioning a pipe silently gains a column and shifts every value after it. Short rows are padded rather than dropped. - Task lists —
- [x]and- [ ]render as checkboxes. They aredisabledon purpose: the preview is contenteditable, and a live checkbox would flip on click and then snap back on the next render. Editing happens in the source pane.
Cell content is escaped like everything else — the preview is built with innerHTML, so a cell containing <img src=x onerror=…> becomes text, never an element.
What the renderer covers
Headings, emphasis, inline and fenced code, links, images, blockquotes, horizontal rules, ordered and unordered lists, tables, task lists.
Still not handled: nested lists, reference-style links, footnotes, setext headings. Raw HTML is escaped rather than passed through, deliberately.
Prebuilt for macOS and Linux, arm64 and x64. The installer verifies every download against SHA256SUMS and refuses on mismatch; so does better-md update.
v0.2.0
curl -fsSL https://better-md.dev/install.sh | shAlready on v0.1.0? That build has no update command, so re-run the line above once. From v0.2.0 onward, better-md update does it.
Added
--version— v0.1.0 had no version baked in at all, so there was no way to tell what you were running.better-md update— downloads the latest release, verifies it against the publishedSHA256SUMS, and only then replaces the binary. A mismatch aborts with your working binary untouched, and the swap is an atomic rename, so an interrupted update never leaves half a binary.--check-updates— asks and changes nothing.
Neither runs on startup. better-md makes no network request unless you type one of these two commands.
- Port 8080 by default, so the URL is the same every run instead of a fresh random port. If 8080 is busy it says so and takes a free port — a taken port never stops it from starting. An explicit
--portis honoured exactly and fails rather than moving, since if you named a port something is probably pointed at it.
Fixed
- List markers never rendered. Tailwind's preflight resets
list-styletonone, and the inline styles never set it back — so every bullet and number was invisible, including for the two buttons in the toolbar. --helpwrote to stderr and exited 1, sobetter-md --help | lessshowed nothing andbetter-md --help && …never continued.
Also
A website at better-md.dev explaining how the bridge works, and the editor is now at playground.better-md.dev to try without installing.
Not yet supported
The renderer still does not handle tables or task lists (- [x]) — both show as literal text.
Prebuilt for macOS and Linux, arm64 and x64. Verify a download against SHA256SUMS; the installer does it for you and refuses on mismatch.