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:
- Scans a project directory for key files (package.json, requirements.txt, main entry points, config files)
- Reads and concatenates the relevant content
- Sends it to a local Ollama model with a structured prompt
- Outputs a complete Markdown README with standard sections
Prerequisites
- Python 3.10+
- Ollama installed and running
- A code-capable model β
qwen2.5-coder:14borcodellama:13b(see best models for coding locally)
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:
| Model | Size | Speed | Quality |
|---|---|---|---|
qwen2.5-coder:7b | 4.5 GB | ~10s | Decent for small projects |
qwen2.5-coder:14b | 9 GB | ~20s | Best balance (default) |
codellama:13b | 7 GB | ~15s | Good structure, weaker prose |
llama3:8b | 4.7 GB | ~12s | Better 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.