Local Markdown document tooling for counting, transcription, and conversion
Project description
mkd
Contents:
What is mkd?
mkd is a local-first CLI oriented around Markdown documents that:
- gathers an essential family of Markdown chores under a single executable, run with a path-explicit command
- holds no opinion about file layout in your project
- keeps heavyweight tools (Pandoc, Poppler, and RapidOCR) outside the Python package
What can mkd do?
-
Transcribe images and PDFs into page-sectioned Markdown, with a content-addressed local cache for large jobs
-
Convert between DOCX/HTML/Markdown via Pandoc, with a tweakable reference DOCX letting you easily specify your desired house style
-
Split/join Markdown files, backed by manifests to survive edit cycles without losing order
-
Reflow (e.g. to one sentence per line) for diff-clean prose review
-
Word counts (including tags allowing multiple custom section-based counts at once)
-
Proofing via deterministic OCR cleanup & zero external LLM calls
Only safe fixes auto-apply; everything uncertain is surfaced for review.
Why can I trust mkd?
[!TIP]
mkd's local-first trust model is intentionally boring about trust boundaries.In particular,
mkdwill never:
- fetch remote URLs
- check remote ilnks
- log into services
- talk to a server
- crawl repos
- watch directories
-
The only automatic side effect is the transcription cache, which is placed at
${XDG_CACHE_HOME:-~/.cache}/mkdby default. -
Commands read the input paths you pass and write to either:
- stdout
- explicit output paths
- documented paths derived from those inputs
-
Conversion and OCR commands may create and use temporary files under the system temp directory (i.e.
/tmp), then clean them up after processing completes.
Who is mkd for?
- Who it is for: an engineer or writer who wants these operations boring, scriptable, and reproducible.
- What it is not: a document server, a network client, or a policy engine.
Getting started
Prerequisites
Global/mandatory
Ensure the following are available (including in the $PATH, if applicable) before proceeding:
- Python
>= 3.11 - uv
Subcommand-specific
The following dependencies are only required by certain mkd subcommands:
| Dependency | Subcommands requiring it |
|---|---|
| Pandoc | count, from-docx, from-html, to-docx, reference |
| Poppler | transcribe pdf |
[!TIP] On macOS, both can be installed easily via Homebrew:
brew install pandoc poppler
Installation
Use the checkout directly while developing:
uv sync
uv run mkd --help
To install mkd machine-wide (based on a local checkout of the repo), run this in your checkout's root dir:
uv tool install -e . --force
mkd --help
[!TIP]
- Examples below use the bare
mkd.- From an unmanaged checkout, prefix them with
uv run.- For project-pinned invocation (via wrapper scripts), see docs/operations.md.
Core commands
[!IMPORTANT] Run
mkd doctorfirst to confirm the environment before working on live documents (especially after installing or upgradingmkdor an external tool).
-
Check the environment, then preflight and count a draft:
mkd doctor --smoke # probe pandoc, Poppler, and RapidOCR mkd check paper.md --fail-on warning # fail the command on warnings, not just errors mkd count paper.md
-
Format prose for line-oriented review:
mkd sentences draft.md formatted.md # write to a new file mkd sentences draft.md --in-place # rewrite the input (explicit opt-in)
-
Transcribe source material into page-sectioned Markdown:
mkd transcribe pdf reading.pdf --output reading.md mkd transcribe pdf reading.pdf --page-range 12 18 --mode auto --output reading-p12-p18.md mkd transcribe images pages/ --output pages.md
-
Proof OCR output, then split it by page and join it back after editing:
mkd proof transcript.md proofed.md --report proof-report.json mkd split proofed.md parts --by page --manifest mkd join --manifest parts/_mkd-split.json --output edited.md
-
Convert across document formats:
mkd from-docx authoritative.docx draft.md --keep-comments mkd from-html export.html export.md --selector '.WordSection1' mkd to-docx draft.md final.docx --reference-doc reference.docx
Testing
-
Run the full test suite:
uv run pytest
- Tests target module boundaries first, then CLI wiring.
tests/test_cli.pydrives the app through Typer'sCliRunner.- The test suite runs fully offline, matching the runtime contract.
See docs/testing.md for structure, mocks, and smoke checks.
-
Lint:
# checks: # - line length 100 # - rule sets E, F, I, N, W uv run ruff check .
Documentation map
| Document | Contains |
|---|---|
| docs/commands.md | Full CLI reference covering options, outputs, & failure semantics |
| docs/workflows.md | End-to-end recipes for transcription, proofing, checks, conversion, & split/join |
| docs/operations.md | Install modes, environment checks, & stdout/file behavior |
| docs/architecture.md | Module boundaries, external-tool edges, cache model, & invariants |
| docs/cache.md | Cache location, invalidation, refresh, & cleanup |
| docs/troubleshooting.md | Common failures & how to narrow them |
| docs/extension-guide.md & docs/development.md | Adding commands & following repo conventions |
Other features
Transcription cache
To make large jobs cheap to rerun, transcribe pdf and transcribe images cache successful pages by default.
CLI flags
- Default runs reuse valid successful entries and retry misses or prior failures.
--refreshignores cached successes and rewrites them.--no-cachedisables reads and writes for a single run.--cache-dir PATHusesPATHas the app cache root for a single run.
Cache location
By default, the cache lives at ${XDG_CACHE_HOME:-~/.cache}/mkd.
mkd cache path
mkd cache prune --older-than 30d --dry-run
mkd cache clean --force
Lifecycle
-
Cache entries key on:
- the source bytes
- the
mkdversion - the processing options
- the signatures of the OCR and Poppler tools involved
-
This is so that changes in the user's choice of tools or options invalidate stale entries automatically.
[!TIP]
- Cache data can contain extracted document text and diagnostic source paths.
- Use
--no-cachefor a sensitive one-off run.- Full layout and invalidation rules can be found at docs/cache.md.
Word-count markers
-
By default,
mkd countcounts the whole rendered document. -
To count only selected regions, wrap them:
<!-- word-count-begin --> This counts. <!-- word-count-end -->
- Short forms (
<!-- wcb -->and<!-- wce -->) are also accepted.
- Short forms (
[!TIP] Run
mkd checkbefore trusting marked counts in automation.
Design and safety
The core is deliberately small and boring: every capability is a thin CLI shell over a Typer-free module that can be tested in isolation.
- Local-first and path-based: commands take explicit input/output paths, write local files or stdout, and perform no network I/O.
- Thin CLI over tested modules:
src/mkd/cli.pyowns only Typer wiring, option validation, and exit codes; domain logic lives in modules that return plain values, dataclasses, or typed exceptions. - Heavyweight tools at narrow edges: Pandoc, Poppler, and RapidOCR are resolved with
shutil.which, invoked throughsubprocess, and translated intomkdobjects or typed errors immediately, so foreign formats never leak inward. - Failures are contract, not surprise: expected problems raise one of twelve
MkdErrorsubclasses, which the CLI renders asmkd <command>: <message>rather than as a stack trace.
Guaranteed invariants
mkd's only persistent side effects are documented and local: explicit output files, explicit in-place rewrites, the app cache at ${XDG_CACHE_HOME:-~/.cache}/mkd unless disabled or overridden, and temporary files under the system temp directory during conversion or OCR.
- No network, ever: no command fetches a remote URL, and
checkvalidates structure and DOCX hazards statically, without invoking Pandoc. - No silent overwrites:
sentencesandproofrequire--in-placeto touch the input;split,join, andreferencerefuse risky overwrites without--force;from-htmlrefuses an identical input/output path. - Clean stdout: content-producing commands keep stdout to content and send diagnostics to stderr or explicit report files, so
mkdcomposes in pipelines. - Stable transcript headings: every transcript section is headed
### Page <label>; this is a cross-module contract, soproofpreserves it, page-basedsplitrecognizes it, and the cache keys on the label that produced it.
mkd doctor returns a stable scheme: 0 (healthy), 1 (a failed check, or a warning under --strict), and 2 (a usage or orchestration error). Other commands exit non-zero on a handled failure.
For the full module map, external-tool contracts, and the complete invariant list, see docs/architecture.md.
External dependencies
-
mkdships thin and defers document-format interpretation to established tools. -
mkd doctorreports which are present, their versions (and, with--smoke, whether they actually run);- Missing tools surface as concise, actionable CLI errors.
| Tool | Powers | Notes |
|---|---|---|
| Pandoc | word counts, DOCX and HTML conversion, reference DOCX | parses Markdown to JSON; owns format interpretation |
Poppler (pdfinfo, pdftotext, pdftoppm) |
PDF page counts, text extraction, rasterization | text-first, with raster fallback for OCR |
| RapidOCR | OCR of images and PDF fallback pages | installed into the environment automatically by uv |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mkd-0.3.0.tar.gz.
File metadata
- Download URL: mkd-0.3.0.tar.gz
- Upload date:
- Size: 91.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fed4a856759246ad36e652b77730f5ac0d67390670bdadbaf9d0c29251da2383
|
|
| MD5 |
2e678c385b46487242da9c2d0d4b6fe2
|
|
| BLAKE2b-256 |
e277576fe61c77a54d8ddfd5ede8048a9cf3651caa044f200b10e7b70a15423b
|
File details
Details for the file mkd-0.3.0-py3-none-any.whl.
File metadata
- Download URL: mkd-0.3.0-py3-none-any.whl
- Upload date:
- Size: 73.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72451c6d16bc47e9e11ee69e1632c4ad6d68be76e22ccfc7670a067bce00cdcd
|
|
| MD5 |
8f8a6e18d44997bf2875e1e64ab46165
|
|
| BLAKE2b-256 |
93337f9648a0a00ed468e1b4c1ed0d464e9517359d1f8829daeb0284fac30203
|