A personal knowledge management repository for organizing notes, references, and insights. Includes an automated pipeline for converting PDFs and web pages to markdown, organized by subject area, with sync to Obsidian.
knowledge-management/
├── notes/ # Atomic notes, fleeting thoughts, and evergreen content
├── references/
│ └── papers/ # Converted markdown output, organized by subject
│ ├── transformers/
│ │ ├── 1706.03762.md
│ │ └── 1706.03762_images/
│ └── cuda/
├── resources/
│ └── sources/ # Input sources, organized by subject
│ ├── transformers/
│ │ └── urls.txt
│ └── cuda/
│ └── urls.txt
├── scripts/
│ ├── convert_pdfs.py # Conversion script (run locally in .venv)
│ └── sync_to_vault.py # Local Obsidian vault sync script
├── projects/ # Project-specific knowledge and documentation
├── templates/ # Reusable document and note templates
├── sync_config.json # Subject → Obsidian vault path mapping
├── requirements.txt # Python dependencies (pymupdf4llm, markitdown)
└── README.md
The pipeline converts PDFs and web pages to markdown, extracts images from PDFs, and syncs the output to your Obsidian vault with proper frontmatter for the wiki schema.
- Add source URLs to
resources/sources/<subject>/urls.txt - Convert locally:
.venv/bin/python scripts/convert_pdfs.pydownloads PDFs and converts every source to markdown (with images). It is incremental — a source whose markdown already exists is skipped, so re-running is safe and cheap. - Review the diff and commit
- Sync to Obsidian with
.venv/bin/python scripts/sync_to_vault.py
The sync script renames files to the wiki schema (<source_type>-<slug>.md), prepends YAML frontmatter, and copies extracted images alongside the markdown.
Each line follows the format: url | title | source_type [| author]
The author field is optional but recommended for sites whose URLs have generic path stems (e.g. YouTube, where every video shares /watch). When provided, it is appended to the title slug to form the output filename, e.g. llm-inference-lecture-roofline-analysis-for-gpu-faradawn-yang.md.
https://arxiv.org/pdf/1706.03762.pdf | Attention Is All You Need | paper
https://docs.nvidia.com/cuda/cuda-programming-guide/index.html | CUDA Programming Guide | doc
https://www.youtube.com/watch?v=7EJjdDLK4cg | LLM Inference Lecture: Roofline Analysis for GPU | video | Faradawn Yang
Valid source types: paper, blog, video, course, code, thread, pdf, doc
Blank lines and lines starting with # are ignored.
- Add the URL to
resources/sources/<subject>/urls.txt - Commit and push
- Convert:
For a reviewable diff on a larger batch, do it on a branch first (
.venv/bin/python scripts/convert_pdfs.py
git checkout -b corpus/<name>) and merge once the output looks right. - Check the new files — a suspiciously small
.mdusually means the fetch hit a login wall or a JS-only page. Delete those rather than syncing a blank Raw file; theurls.txtline stays, so a later run retries. - Commit, then run
.venv/bin/python scripts/sync_to_vault.py
Note on YouTube URLs: YouTube and similar sites use a generic path stem (
/watch) for every video, which would cause filename collisions. The script resolves this by building a human-readable slug from the title and author fields:<title-7-words>-<author-3-words>.md. Theauthorfield inurls.txtis what enables the author part of the slug — add it whenever you submit a YouTube or similarly generic URL. If no title or author is available, the script falls back to the URL's unique query parameter (e.g.?v=VIDEO_ID).
-
Create the subject folder with a
urls.txt:mkdir -p resources/sources/<new-subject>
Add URLs to
resources/sources/<new-subject>/urls.txt -
Add the Obsidian vault mapping in
sync_config.json:{ "subjects": { "<new-subject>": "/path/to/obsidian/vault/Raw" } } -
(Optional) Register a NotebookLM notebook for the subject so new markdown is synced to NotebookLM via the
nlmCLI. Add the notebook ID undernotebooklminsync_config.json:{ "notebooklm": { "<new-subject>": "<notebook-id>" } }If omitted, only the NotebookLM push is skipped for this subject — the Obsidian vault sync (
sync_to_vault.py) still runs normally. -
Create the Obsidian vault for the subject area with the required layer folders:
mkdir -p "/path/to/obsidian/vault/<new-subject>"/{Raw,Wiki,"Learning Path"} cp "/path/to/obsidian/vault/<existing-subject>/CLAUDE.md" "/path/to/obsidian/vault/<new-subject>/CLAUDE.md"
Then adapt the cloned
CLAUDE.mdto the new domain — strip every term, example, and Learning Path stage scope that belongs to the sibling subject, and substitute domain-native terms. A schema clone is a starting point, not a drop-in. -
Run the vault bootstrap workflow (defined in the vault's own
CLAUDE.md, section "Bootstrap workflow"). This is a mandatory one-time step for every new subject-area vault. The bootstrap createsWiki/index.md,Wiki/log.md,Wiki/overview.md, plus stub entity/concept pages for the domain's core terms, plus emptyLearning Path/stage files. Without bootstrap, the first ingest has nothing to link into — summaries end up as orphans and the wiki graph-connectivity invariant is broken. -
Commit, push, and follow steps 3-5 from the "Adding a source to an existing subject" section above.
When starting a new session — on any subject-area vault in this knowledge base — the LLM has no memory of prior work. Before giving any new instructions, ask it to orient itself by reading the load-bearing state files so it can pick up where the last session left off without guessing.
The three checks, in order:
- Read
Wiki/log.mdfor the vault you're working in — the append-only log tells you what's been ingested, what stubs were created, what's deferred, and what was touched last. This is the single most load-bearing file for session continuity. - Check
Raw/for unprocessed files — anything newer than the latest log entry is pending ingest. - Glance at
git statusand recent commits — catches anything changed outside the log (config updates,CLAUDE.mdedits, new subject areas, URL additions).
Suggested resume prompt (vault-agnostic — just substitute the vault path):
Review
Wiki/log.mdin the<vault-name>vault, checkRaw/for unprocessed files, glance atgit status/ recent commits, and tell me what state we're in before we start. Don't edit anything yet.
If you're working across multiple vaults in one session (e.g., bouncing between the Transformer and CUDA vaults), ask for the orientation check on each vault explicitly — the LLM will only check the vault you name.
The vault's own CLAUDE.md contains a longer-form version of this resume prompt tailored to that vault. Use the short form for daily interactive sessions; reach for the long form only when the vault is unfamiliar or it's been weeks since the last session.
- PDFs:
pymupdf4llm— lightweight, extracts images inline at their original position - Web pages:
markitdown(Microsoft) — converts HTML URLs directly to markdown
Conversion runs locally in a virtualenv (.venv/, git-ignored):
uv venv --python 3.12
uv pip install -r requirements.txtThen always invoke the scripts through it: .venv/bin/python scripts/convert_pdfs.py.
The
youtube-transcriptionextra is load-bearing.requirements.txtpinsmarkitdown[youtube-transcription], not baremarkitdown. Without the extra, markitdown returns only a video's title and description — a 13,000-word lecture becomes a 200-word shell, and the failure is silent. If video conversions come back at a few hundred words, that extra is missing.
(This replaced a Warp cloud agent that ran the same script and opened a PR. The only thing lost is the PR review gate; use a branch for large batches instead.)
Adopted 2026-08-29 after a corpus-expansion session where checking beat assuming, every time. Any agent working in this repo or its vaults — Claude, Codex, or otherwise — should follow these. They are cheap; the mistakes they prevent are not.
-
Never classify or route a source by its title or by keywords. A first-match regex classifier sent Unsloth to inference, nanoGPT to fine-tuning, and backprop to transformers, because it cannot tell "quantization for serving" from "quantization during fine-tuning". Read the captured content. When a page serves a useless
<title>(Medium,Google Colab), recover the real title from the URL slug rather than discarding the record — one such record was Karpathy's "Yes you should understand backprop". -
Before moving or deleting a source, grep the vault for inbound references.
nanochat/gpt.pyreads as a "build a ChatGPT" repo and was routed out of the inference vault on that basis; it is in fact cited byWiki/entities/KV Cache.mdas the production reference building the cache on the FlashAttentionflash_attn_with_kvcachekernel. Titles mislead; citations don't. -
Ask what would undo the change. Deleting a
Raw/file is not durable by itself:scripts/sync_to_vault.pyrecreates any destination that no longer exists, so the daily launchd sync restores it unless theurls.txtline and the convertedreferences/papers/files move too. Trace the pipeline to its end before calling a cleanup done, and dry-run the sync to prove it. -
Re-verify state established earlier in the session. Other sessions edit this repo and these vaults concurrently; during that session a commit and three vault files changed underneath the work in progress. Re-read
git log/git status/ mtimes instead of trusting an earlier reading. -
Distrust your own aggregate numbers. A URL-overlap count was reported wrong because the normalizer stripped query strings, collapsing every YouTube link to
youtube.com/watch. When a number is surprising, re-derive it a second way before reporting it — and correct it plainly when it was wrong. -
Back up before irreversible work; verify after. The vaults are not git-tracked.
cp -Rthe vault first, then confirm withpython3 scripts/test_okf_bundle.py "<vault>"that the bundle is still conformant and no orphan links remain.
See projects/llm-corpus-expansion/routing.md for these applied to a concrete decision,
and each vault's CLAUDE.md for the step-by-step source relocation/removal workflow.
Clone the repository and install dependencies:
git clone https://github.com/aadehamid/knowledge-management.git
pip install -r requirements.txt