md2idx
Split a large Markdown into heading-level JSON with a numbered index (table of contents) and a sections array. Your LLM reads the index and retrieves only the sections it needs — instead of loading the whole file into context.
Quick start
npx md2idx README.md | jq -r '.index' # read the index first
npx md2idx README.md | jq -r '.sections[2]' # grab one section by number
npx md2idx README.md | jq -r '.sections[0:3][]' # grab a contiguous range
npx md2idx README.md | jq -r '.sections | join("\n\n")' # join all sections back into the full document
npx md2idx data.md | jq -r '.sections[4]' | grep Tokyo # extract lines containing "Tokyo" from a section
npm install -g md2idx # global install
md2idx README.md | jq -r '.index' # now available as md2idx
Why md2idx
Before / After
Without md2idx, reading a single section of a large document means loading the entire file:
Read spec.md → 5,000 lines into context (all of them)
"What's the retry policy?" → agent searches through everything
With md2idx, the agent reads a 20-line index first and fetches only what it needs:
npx md2idx spec.md | jq -r '.index' # ~20 lines (table of contents)
npx md2idx spec.md | jq -r '.sections[5]' # ~80 lines (just that section)
# Total: ~100 lines in context instead of 5,000
Why not just grep for headings?
grep -nE '#{1,6} ' spec.md gives you a list of headings. For simple cases, that works — but md2idx solves several problems grep doesn't:
| grep | md2idx | |
|---|---|---|
| Get section content (not just heading lines) | Requires calculating line ranges + Read with offset/limit | jq '.sections[N]' |
Setext headings (=== / ---) |
Not detected | Detected |
# inside code fences |
False positives | Skipped |
Inline markup in headings ([link](https://github.com/oubakiou/md2idx/blob/main/url)) |
Included as-is | Stripped in index, preserved in section |
Usage
When a human invokes the CLI directly
md2idx [file] [--pretty]
| Argument / flag | Description |
|---|---|
file |
Input Markdown file. Reads from stdin when omitted |
--pretty |
Pretty-print the JSON output (default is single-line for piping into jq) |
--help |
Print usage and exit |
Pipe from stdin:
cat spec.md | md2idx | jq -r '.index'
When an LLM invokes the CLI via a skill
# Skill installation example with gh skill install
gh skill install oubakiou/md2idx md2idx-read --agent claude-code --scope project
# Skill installation example with npx skills add
npx skills add oubakiou/md2idx --skill md2idx-read --agent claude-code --yes
An LLM agent (e.g. Claude Code) uses the md2idx-read skill to read large Markdown files efficiently. Instead of loading an entire document into context, the agent fetches the index first, then retrieves only the sections it needs — saving tokens and keeping context focused.
Output format
{
"index": "# 0. Project README\n## 1. Install\n## 2. Usage\n### 3. Options\n## 4. License",
"sections": [
"# Project README",
"## Install\n\nnpm install -g ...",
"## Usage\n\n...",
"### Options\n\n...",
"## License\n\nMIT",
],
}
index— A single string containing a numbered table of contents for the LLM to decide which sections to retrieve. Each line is<# markers for level> <serial>. <heading text>. The serial matches thesectionsarray index.sections— A flat array of raw Markdown strings (heading line + body), in document order. A child heading always occupies a contiguous index range after its parent, so slice-based retrieval works.
Preamble
Text before the first heading is stored as sections[0] with an index entry of 0. (no heading marker).
How it works
md2idx uses a zero-dependency line scanner (no external Markdown parser). It detects ATX headings (# ) and setext headings (=== / ---), skips fenced code blocks, and strips inline markup from heading text for the table of contents.
License
MIT
No comments yet
Be the first to share your take.