tools
A collection of small, self-contained tools, inspired by Simon Willison's tools (source).
Browse the rendered tools at tools.ibecker.eu; the source lives on GitHub at watercrossing/tools, where issues and pull requests are welcome.
Each tool lives in its own folder and stands alone. See CLAUDE.md for the conventions new tools follow.
Microsoft Teams
- teams-chat-to-markdown — convert a copied Teams meeting-chat HTML export into clean Markdown, preserving authors, timestamps, reply-quotes (as blockquotes), reactions, emoji, and links.
- teams-transcript-to-markdown — a userscript for a Teams recording's Stream page that scrolls the whole virtualized transcript and downloads it as Markdown in one click, with consecutive speaker turns collapsed and a gap/completeness check for missing entries. The header carries the meeting metadata (title, start, duration, recorder, speakers), and the script updates itself from GitHub.
- teams-slidegrab — recover a deck that was shared live via PowerPoint Live but never handed over as a file: from a recording (OBS or Microsoft Stream), read the on-screen
Slide X of Ycounter with local OCR and save one clean screenshot per slide. CPU-only; nothing leaves the machine. - screenshare-slidegrab — the same for a deck shown as a plain screen share, where there is no counter to read: find the stretches where the shared screen holds still, crop out the slide, and keep one image per slide plus a PDF of them all. Slides are compared cropped and size-normalised, so window resizes and Teams layout changes don't make an old slide look new; animation build steps are folded into their final state (a build adds content where the slide was blank, a new slide replaces old text), and revisits are dropped.
- obs-interview-transcript — transcribe an interview you recorded yourself with OBS, rather than depending on Teams for it: Desktop Audio on track 1, your microphone on track 2, out comes one speaker-attributed transcript. The tracks overlap, because your mic also hears the far end through your speakers, but the desktop track cannot contain your voice — so gating the mic against it separates the two, per word rather than per segment. Local Whisper + pyannote; nothing leaves the machine, which is the point when the recording is research data.
Zoom
- zoom-transcript-to-markdown — convert the transcript panel of a Zoom cloud-recording share page, saved as HTML, into Markdown: one block per speaker turn, the chat shown alongside the recording, the recording length, and a check for missing transcript rows (holes in the row ids, or a speaker change whose header row was lost).
Deployment
- github-push-deploy — auto-deploy a GitHub repo on every push, using a repo webhook and a small PHP listener on a plain Apache + PHP-FPM box. The listener verifies the webhook's HMAC-SHA256 signature, then clones the repo and runs your own deploy script — publish files, run a build, restart a service, launch a container, whatever you put in it. No CI runner or third-party service; one
deploy.confdrives it all. - repo-web-view — publish a directory tree as a static, GitHub-style browsable site: every folder becomes an
index.htmlshowing its renderedREADME.mdabove a listing of the folder's contents, and a generated.htaccessmakes Apache download files on click while folders render. Self-contained pages (inlined CSS, embedded README images), and--render-markdowngives every.mdfile a rendered page of its own instead of a download. Every page gets a search box (/orCtrl-K) that searches the rendered pages by heading section and the whole tree by filename, off one shared index loaded on first use. Pairs with github-push-deploy as the publish step.
Analytics
- goatcounter-backfill — replay archived Apache/nginx
combinedaccess logs into GoatCounter without collapsing visitor sessions. GoatCounter expires sessions after 8 hours of wall-clock inactivity, sogoatcounter importreplays an entire archive inside one session window: a visitor returning daily to the same page becomes a single visit stamped on day one, and the later days produce no rows at all. This tool assigns sessions from log time via thesessionfield of/api/v0/count, and mirrors the-excluderules so backfilled data matches what the live importer collects.
Storage
- mergerfs-tier-mover — the missing half of a tiered mergerfs pool: demote the least-recently-modified files from the SSD branch to the HDD branch until the SSD is back above a free-space floor.
category.create=ff+minfreespaceonly ever fills the fast branch, so without a mover the SSD fills once and every later write lands on the HDD however cold the data holding the SSD has become. Safe to run against a live pool — the copy is fsynced and renamed into place before the source is unlinked, files that are recent, held open, or written during their own copy are skipped, and hard links are never unshared.
Hardware
- nut-usb-watchdog — notice that a USB-attached UPS has stopped talking to NUT, reset the USB device, and restart the driver. Some USB HID UPSes stop answering control transfers while staying enumerated:
lsusbstill lists the device, the kernel logs no disconnect, the driver still claims interface 0 — only the descriptor reads fail, andupscgoesData stale. Nothing raises an alarm, because the driver never exits and its unit staysactive, so the UPS can sit unmonitored for months; and restarting the driver does not help, because a fresh one fails in the same place. AUSBDEVFS_RESETon the device node does, so that is what this runs, then restarts the unit. Silent when healthy and when a reset worked — both go to syslog — so as a cron job it mails only when the UPS is still unreachable afterwards.
Networking
- slaac-prefix-watch — log which global IPv6 prefixes a host holds, to find out how often the ISP renumbers the line. Consumer routers take a dynamic prefix by DHCPv6-PD and can't be asked about it, but every host's SLAAC addresses carry the prefix and its announced lifetimes. Silent unless the set of preferred prefixes changes — including one appearing where there was none, so it doubles as a sentinel on a LAN without IPv6 — and
--reportsummarises the history. Stdlib only and Python 3.10+, so cron can run it with the system interpreter.
Secrets
- config-password-sanitisation — swap the real secrets in a config file for stable placeholders so it can be pasted into a ticket, a chat or an LLM prompt, then swap them back so the answer you get returned is a file you can deploy. Redacting is a one-way door; a map of secret → placeholder makes the round trip byte-exact. Substitution is one leftmost-longest pass, so a placeholder cannot be eaten by the next replacement and
topcannot bite the front offtopsecret, and the sanitised file is grepped for every secret before you are told it is safe to send.--checkreports secrets by placeholder, never by value, and exits non-zero — a pre-commit hook.
Documentation
- markdown-docs-lint — lint a Markdown docs tree for the failures that break silently: dead relative links, dead
#anchors, pages nothing links to, and files grown too long to read whole. Anchors are the point: rename a heading and every inbound#fragmentdies with no error, no visual change, and a one-linegit diffthat does not include the files that broke. Handles the traps a hand-rolled checker gets wrong — GitHub's slug leaves two hyphens where an em dash sat between spaces, code fences are not headings, and backticked text still counts toward a slug. Exits non-zero, so it works as a pre-commit hook.
Research
- zotero-index — query a Zotero library offline from a Better BibTeX JSON auto-export synced from another machine: the attachment path for a citekey, the citekey for a DOI, a search by title/author/tag/collection/year, and a data-quality report. No Zotero install and no web API. A
[path_map]rewrites the exporting host's (often Windows) attachment paths for the machine you're on, and a slim index is cached and rebuilt only when the export's mtime or size changes, so an export on a slow cloud mount is read only when it changed. A citekey shared by several items is reported, never silently resolved to one of them.
Travel
- thameslink-engineering-work — turn Thameslink's planned engineering work page into a single
.icscalendar. The page only shows one day at a time with no overview, so this fetches N consecutive days, de-duplicates the multi-day closures by the stableINCxxxxxxxxid already in the server-rendered HTML, and writes one all-day event per item, ready to import into Google Calendar.--filternarrows it to routes you actually care about.
Claude Code
- claude-render-transcripts — render a Claude Code session
.jsonltranscript (including headlessclaude -pruns that never appear in the/resumepicker) into readable plain text: one header per turn andtext/thinking/tool_use/tool_resultblocks flattened, with long tool inputs and results truncated. - markdown-to-rich-text — paste Markdown (such as a reply copied out of a Claude chat) and copy it back out as formatted text that pastes into Outlook or Word with real Heading 1–6 styles, lists, bordered tables and monospaced code, instead of literal
**asterisks**. Paste into the page and the formatted version is already on the clipboard; relative repo links and images, meaningless in an email, are reduced to their text.
Web capture
- copy-page-html — a userscript that copies the HTML of one picked page element to the clipboard, no devtools needed: hover to highlight, click to copy, done. The CSS selector is cached per site, so a repeat visit is a single menu click. Built for pages that resist automated scraping (e.g. Drupal behind Cloudflare), where grabbing one landmark element by hand is otherwise a "inspect element → find the node → copy outer HTML" chore.
Overleaf
- overleaf-comments-export — a Tampermonkey userscript that syncs Overleaf review comments into the LaTeX source as
\olcmacros (author, timestamp, highlighted span, comment), each on its own line just above the line it annotates, so they land in git. Idempotent via a per-line%olcsyncmarker (insert new, update changed, never duplicate); resolved threads skipped by default; one macro per reply. Writing is a real edit that propagates to collaborators, so it confirms first.