πŸ“ Tutorials
Β· 7 min read

Build a CLI That Generates README Files From Your Code


Every project needs a README. Few projects have a good one. You start with the best intentions, then the codebase evolves and the README stays frozen β€” still describing the setup process from six months ago, missing half the current features.

What if you could point a CLI at any project directory and get a complete, accurate README back? That’s what we’re building: a Python script that scans your codebase, reads the files that matter β€” dependency manifests, entry points, config files β€” sends them to a local LLM via Ollama, and writes a full README.md with installation steps, usage examples, API docs, and a contributing section. Everything runs locally, nothing leaves your machine.

What We’re Building

A single CLI tool β€” readme_gen.py β€” that:

  1. Scans a project directory for key files (package.json, requirements.txt, main entry points, config files)
  2. Reads and concatenates the relevant content
  3. Sends it to a local Ollama model with a structured prompt
  4. Outputs a complete Markdown README with standard sections

Prerequisites

ollama pull qwen2.5-coder:14b

No external Python dependencies β€” we’re using only the standard library.

The Complete Script

Create readme_gen.py:

#!/usr/bin/env python3
"""Generate a README.md from your codebase using Ollama."""

import argparse
import json
import os
import urllib.request

OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "qwen2.5-coder:14b"

# Files that reveal project structure and purpose
KEY_FILES = [
    "package.json", "requirements.txt", "pyproject.toml", "setup.py",
    "Cargo.toml", "go.mod", "Makefile", "Dockerfile", "docker-compose.yml",
    "tsconfig.json", ".env.example",
]

# Common entry point patterns
ENTRY_POINTS = [
    "main.py", "app.py", "index.py", "server.py", "cli.py",
    "index.ts", "index.js", "app.ts", "app.js", "main.ts", "main.js",
    "src/main.py", "src/app.py", "src/index.ts", "src/index.js",
]

PROMPT = """You are a technical writer. Based on the following project files, generate a complete README.md.

Include these sections:
# Project Name (infer from the project files)
## Description β€” what the project does, in 2-3 sentences
## Features β€” bullet list of key features
## Prerequisites β€” what's needed before installation
## Installation β€” step-by-step setup commands
## Usage β€” how to run it, with code examples
## API Reference β€” if applicable, document key endpoints or functions
## Configuration β€” environment variables or config options if any
## Contributing β€” standard contributing guidelines
## License β€” infer from files or default to MIT

Rules:
- Output ONLY valid Markdown, no wrapping code fences
- Be specific β€” use actual package names, commands, and file paths from the project
- If you can't determine something, make a reasonable assumption and note it
- Keep it practical and concise

Project files:

{context}"""


def scan_directory(path):
    """Collect a summary of the directory structure."""
    tree = []
    for root, dirs, files in os.walk(path):
        # Skip noise
        dirs[:] = [d for d in dirs if d not in {
            "node_modules", ".git", "__pycache__", "venv", ".venv",
            "dist", "build", ".next", ".cache", "target",
        }]
        depth = root.replace(path, "").count(os.sep)
        if depth > 3:
            continue
        indent = "  " * depth
        tree.append(f"{indent}{os.path.basename(root)}/")
        for f in files[:20]:  # cap files per directory
            tree.append(f"{indent}  {f}")
    return "\n".join(tree[:150])  # cap total lines


def read_key_files(path):
    """Read content of key project files."""
    contents = {}
    for filename in KEY_FILES + ENTRY_POINTS:
        filepath = os.path.join(path, filename)
        if os.path.isfile(filepath):
            try:
                with open(filepath, "r", errors="ignore") as f:
                    text = f.read(5000)  # cap per file
                contents[filename] = text
            except (OSError, UnicodeDecodeError):
                continue
    return contents


def build_context(path):
    """Assemble the context string sent to the LLM."""
    parts = []

    # Directory tree
    tree = scan_directory(path)
    parts.append(f"=== Directory Structure ===\n{tree}")

    # Key file contents
    files = read_key_files(path)
    for name, content in files.items():
        parts.append(f"=== {name} ===\n{content}")

    # Existing README (so the model can improve on it)
    readme_path = os.path.join(path, "README.md")
    if os.path.isfile(readme_path):
        with open(readme_path, "r", errors="ignore") as f:
            existing = f.read(3000)
        parts.append(f"=== Existing README.md ===\n{existing}")

    context = "\n\n".join(parts)
    # Truncate if too long for context window
    if len(context) > 12000:
        context = context[:12000] + "\n... (truncated)"
    return context


def generate_readme(context):
    """Send context to Ollama and return the generated README."""
    payload = json.dumps({
        "model": MODEL,
        "prompt": PROMPT.format(context=context),
        "stream": False,
        "options": {"temperature": 0.4, "num_predict": 3000}
    }).encode()

    req = urllib.request.Request(
        OLLAMA_URL,
        data=payload,
        headers={"Content-Type": "application/json"}
    )

    with urllib.request.urlopen(req, timeout=120) as resp:
        return json.loads(resp.read())["response"].strip()


def main():
    parser = argparse.ArgumentParser(description="Generate README.md from your codebase using Ollama")
    parser.add_argument("path", nargs="?", default=".", help="Project directory to scan")
    parser.add_argument("-o", "--output", default=None, help="Output file (default: README.md in project dir)")
    parser.add_argument("-m", "--model", default=None, help="Ollama model to use")
    parser.add_argument("--stdout", action="store_true", help="Print to stdout instead of writing a file")
    args = parser.parse_args()

    global MODEL
    if args.model:
        MODEL = args.model

    project_path = os.path.abspath(args.path)
    if not os.path.isdir(project_path):
        print(f"Error: {project_path} is not a directory", file=__import__("sys").stderr)
        raise SystemExit(1)

    print(f"Scanning {project_path}...")
    context = build_context(project_path)

    print(f"Generating README with {MODEL}...")
    readme = generate_readme(context)

    if args.stdout:
        print(readme)
    else:
        out = args.output or os.path.join(project_path, "README.md")
        with open(out, "w") as f:
            f.write(readme + "\n")
        print(f"Written to {out}")


if __name__ == "__main__":
    main()

That’s the whole thing β€” about 130 lines, zero dependencies.

How It Works

The script does three things in sequence:

Scan. scan_directory() walks the project tree, skipping common junk directories (node_modules, .git, pycache), and builds a text representation of the structure. It caps depth at 3 levels and 150 lines β€” enough for the LLM to understand the layout without blowing up the context window.

Read. read_key_files() looks for files that reveal what a project is: dependency manifests like package.json or requirements.txt, entry points like main.py or index.ts, and config files like Dockerfile or .env.example. Each file is capped at 5,000 characters. If an existing README is present, it’s included too β€” so the model can improve on it rather than starting from scratch.

Generate. Everything gets assembled into a structured prompt and sent to Ollama. The prompt explicitly requests standard README sections and tells the model to use actual names and paths from the project files. Temperature is set to 0.4 β€” low enough for consistent output, high enough to avoid robotic phrasing.

Usage

Basic β€” generate a README for the current directory:

python3 readme_gen.py

Point it at a specific project:

python3 readme_gen.py /path/to/your/project

Preview without writing a file:

python3 readme_gen.py --stdout

Write to a custom location:

python3 readme_gen.py -o docs/README.md

Use a different model:

python3 readme_gen.py -m codellama:13b

Choosing a Model

The model matters a lot here. README generation requires understanding project structure and writing clear prose β€” that’s a harder task than pure code generation. Here’s what works:

ModelSizeSpeedQuality
qwen2.5-coder:7b4.5 GB~10sDecent for small projects
qwen2.5-coder:14b9 GB~20sBest balance (default)
codellama:13b7 GB~15sGood structure, weaker prose
llama3:8b4.7 GB~12sBetter prose, less code-aware

For a deeper comparison, see our guide on the best AI models for coding locally.

Extending It

A few ideas to take this further:

Add source file sampling. The current script only reads known entry points. You could sample the first 50 lines of every .py or .ts file to give the model richer context about what the code does.

Section-by-section generation. Instead of one big prompt, generate each README section separately. This gives you more control and lets you use different temperatures β€” lower for installation steps, higher for the project description.

Watch mode. Pair it with a file watcher to regenerate the README whenever key files change. Useful for libraries where the README is the documentation.

Tips and Gotchas

  • Large monorepos: The context cap (12,000 chars) keeps things manageable, but for monorepos you’ll get better results pointing the tool at a specific package directory rather than the root.
  • Existing READMEs: The script feeds any existing README to the model. This means running it twice tends to improve the output β€” the model refines rather than starting over.
  • Hallucinated commands: LLMs sometimes invent CLI flags or installation steps. Always review the output before committing. This is a first draft tool, not a replacement for human review.
  • Timeout: The default is 120 seconds. Large models on CPU can be slow on first generation while the model loads. Subsequent runs are faster.

Pair It With Other Tools

This handles the README. For API-specific documentation, check out our AI API docs generator β€” it produces OpenAPI specs from your route definitions. The two tools complement each other well: one for the human-facing README, the other for machine-readable API docs.

What You Built

A zero-dependency Python CLI that scans a project directory, reads the files that matter, and generates a complete README using a local LLM. No API keys, no cloud services, no data leaving your machine. Point it at any repo, get a README back in under 30 seconds.

The output isn’t perfect β€” it’s a strong first draft that captures your project’s actual structure, dependencies, and setup process. Edit it, commit it, and move on to the code that matters.

πŸ“˜