A personal toolkit of small, single-file command-line tools that let an AI agent
(or you) drive everyday services — messaging apps, Google, Blender, kanban,
wikis — from the terminal. Each tool is one self-contained script with a
--help. No framework, minimal dependencies.
Built as the "hands" for an autonomous Claude Code setup: the agent reaches for a CLI instead of asking a human to click. They work equally well run by hand.
Several of these tools drive consumer messaging apps via unofficial protocols or browser automation, acting as your own account. That is powerful and also carries real risk:
- Terms of Service. WhatsApp (Baileys), Messenger (ws3-fca), Instagram, and Telegram-as-a-user all technically violate the platform's ToS. Enforcement against personal-volume use is rare but not zero — your account could be banned. Keep volume human-paced. Discord uses the official bot API and is the safe exception.
- Credentials never live in this repo. Every tool reads secrets from
environment variables or stores a session under a local, git-ignored
.tokens/directory. Nothing in this repo contains a token, password, or cookie —.gitignoreblocks all of it, and so should yours. - Respect other people. Tools that read or auto-reply to messages touch
other people's words. Only automate conversations with people who know and
consent. The
imessage-responderships without its system prompt, contact, or message history — you must supply your own. - No financial actions.
commbankis read-only by design — it scrapes, it never moves money.
You are responsible for how you use these. Start read-only, keep volume low.
These CLIs parse --flags permissively — an unrecognised --foo becomes
args.foo = true. A command that reads only the flags it knows will therefore
silently ignore anything else, so a typo'd safety flag (--dryrun,
--no-send) falls straight through and fires the real, irreversible action.
This is not hypothetical: signal send --dry-run once actually sent a message.
So every mutating verb (send, delete, rename, submit, …) validates its whole argv against an allowlist and aborts on anything unrecognised:
import { assertKnownFlags } from "../lib/flags.mjs";
assertKnownFlags(args, ["to", "text", "file"], { verb: "send" });Two rules that matter more than the call itself:
- Derive the allowlist from the flags the command actually reads in code, not from its help text. An allowlist that accepts flags the command ignores is the same silent-ignore bug wearing a seatbelt.
- A
--dry-runmust be real — resolve the target, print, and return before mutating. A safety flag that is a no-op is the original footgun.
Read-only verbs (list/read/export/whoami) don't need this. Tools that
parse per-subcommand with node's strict parseArgs (outline, kan) already
fail closed at the parser and don't import the helper — but note that strictness
scoped to the whole CLI rather than the verb is not enough: a flag belonging
to a sibling verb parses cleanly and gets dropped. That bug was live in
radicale and is what the helper's per-verb allowlist exists to prevent.
Most tools are Node ESM (.mjs); a couple are Python. Per-tool dependencies
live in that tool's folder.
git clone https://github.com/nickmeinhold/cli-tools.git
cd cli-tools
# Node tools: install deps in the folders you want
cd whatsapp && npm install && cd ..
cd google && npm install && cd ..
# ...etc
# Run any tool's help
node whatsapp/whatsapp.mjs --helpRequirements: Node 18+ for the .mjs tools, Python 3.10+ for the Python ones,
and a claude CLI on PATH for the AI-backed tools (imessage-responder).
| Tool | What it does | Mechanism | ToS risk |
|---|---|---|---|
whatsapp |
Send/read WhatsApp DMs & groups as you | Baileys (WhatsApp Web) | |
telegram |
Read/send Telegram as your user account | GramJS / MTProto | |
discord |
Message via an official bot | Discord REST v10 | ✅ official |
signal |
Read (local DB) + send Signal | Signal Desktop DB + signal-cli | personal |
messenger |
Read/send Facebook Messenger DMs | ws3-fca + Playwright | |
instagram |
Read/send Instagram DMs | Playwright (web) | |
linkedin |
Read/send LinkedIn DMs | Playwright (web) | |
google |
Gmail, Drive, Calendar | Google APIs (OAuth) | ✅ official |
blender |
Drive Blender programmatically | BlenderMCP socket / headless | ✅ |
outline |
Outline wiki CRUD + search | Outline API | ✅ official |
kan |
Kan.bn kanban boards/cards | Kan API | ✅ official |
forge |
Generator-evaluator agent loop | local | ✅ |
playwright |
Headed-browser automation + auth bridge | Playwright | varies |
commbank |
Read-only NetBank balance/transaction scrape | Playwright | personal, read-only |
social |
Build a social graph from FB/LinkedIn | Playwright + GitHub API | |
marketplace-watch |
Watch FB Marketplace for new listings | Playwright | |
imessage-responder |
Autonomous AI replies in an iMessage thread | chat.db + headless Claude | macOS only |
humanitix |
Humanitix event ticketing — events, orders, tickets | Humanitix public API | ✅ official |
slack |
Read/search/post Slack channels & DMs | Slack Web API | ✅ official |
radicale |
CalDAV calendars & contacts CRUD | Radicale CalDAV | self-hosted |
parallax |
Nightly cross-repo "surprise" watchman | git scan + Bayesian belief | local |
lib |
Shared Playwright plumbing | — | — |
Baileys-backed WhatsApp client. Post/read in your personal groups & DMs — the case the Meta Cloud API can't do (it's business→customer 1:1 only).
node whatsapp/whatsapp.mjs auth # one-time QR scan (phone → Linked Devices)
node whatsapp/whatsapp.mjs list-groups
node whatsapp/whatsapp.mjs send --to <jid> --text "hi"
node whatsapp/whatsapp.mjs watch # daemon: logs incoming DMs + mediaSession is stored under .tokens/whatsapp/ (git-ignored).
Read/send Telegram as your user account (MTProto), so it sees your personal DMs and history — a bot cannot. Interactive login on first run.
node telegram/telegram.mjs --helpThe safe one: sends as an official bot (no self-bot ban risk). Can DM users who share a server with the bot, read replies, fetch attachments.
node discord/discord.mjs helpAsymmetric by design: reads by decrypting Signal Desktop's local DB
(instant, full history); sends by shelling out to signal-cli. Pair via QR.
node signal/signal.mjs --helpRead/send Facebook Messenger DMs as your own account. Note: the unofficial
protocol client is blind to E2EE threads — for those, drive messenger.com via
the playwright profile. whoami before trusting an empty inbox.
node messenger/messenger.mjs --helpInstagram DMs as you. IG's private API is blocked, so this drives a headless browser (slower/heavier, but the only path IG leaves open). Interactive login.
node instagram/instagram.mjs --helpLinkedIn DMs as you. LinkedIn has no messaging API, and its internal Voyager
REST endpoint now 500s (DMs moved to a GraphQL endpoint with a rotating query
hash), so this drives the messaging SPA headlessly and reads the DOM. Reuses a
saved Playwright session. send fails closed on unknown flags; --dry-run
opens the thread but sends nothing.
node linkedin/linkedin.mjs --helpGmail, Drive, and Calendar from one OAuth credential.
node google/gmail.mjs --help # search/draft/send mail, attachments
node google/gdrive.mjs --help # upload/list/share, markdown → Google Doc
node google/gcal.mjs --help # list/create events, invite attendeesProvide your own OAuth client; the token lives in .tokens/ (git-ignored).
Drive Blender programmatically (a CLI over the BlenderMCP addon). Two modes: socket (drives a live Blender) and headless (reproducible batch jobs, e.g. GLB / blendshape work).
node blender/blender.mjs --helpFull CRUD + search against an Outline wiki, incl. markdown export. Multi-instance aware.
node outline/outline.mjs --helpKan.bn kanban: workspaces, boards, cards, lists, labels, members, invites.
node kan/kan.mjs --helpA self-contained generator-evaluator loop (planner / builder / evaluator), with trajectory analysis (plateau / oscillation / regression detection).
node forge/forge.mjs --helpHeaded-browser automation for sites without a clean API, and the auth-bridge
front door: auth --site URL --name LABEL does an interactive login (your hands:
password + 2FA) and saves a session that the messaging CLIs reuse. The _*.mjs
scripts are small task-specific recipes.
node playwright/playwright.mjs --helpRead-only NetBank scraper (balances, transactions). There is no code path
that transfers money or mutates anything. Sessions are short-lived, so each
sitting starts with an interactive auth (client number + password + NetCode).
node commbank/commbank.mjs --helpBuilds a social graph: harvests FB friends / LinkedIn connections and cross-references against GitHub (location=builder signal). Public APIs + browser automation; checkpointed and paced. Use responsibly and respect others' privacy.
node social/social.mjs --helpPolls Facebook Marketplace for new listings matching a search and notifies you.
cat marketplace-watch/watch.sh # configure the search + notify targetmacOS only. An autonomous 🤖 responder for a single iMessage thread,
backed by headless Claude. It reads the thread from the local Messages chat.db,
generates a reply, and either sends it (auto-prefixed 🤖) or escalates sensitive
topics to you. Safety properties baked in: arm-only first run (never answers
the existing backlog), every send gated, errors escalate rather than guess.
Ships intentionally incomplete. You must supply:
system-prompt.txt— copy fromsystem-prompt.txt.exampleand tailor it.CONTACT_HANDLE/OWNER_HANDLEenv vars — the contact's handle and yours.
Only use this with someone who knows and consents to autonomous AI replies.
export CONTACT_HANDLE="+10000000000" OWNER_HANDLE="you@example.com"
cp imessage-responder/system-prompt.txt.example imessage-responder/system-prompt.txt
python3 imessage-responder/responder.py # first run arms only; sends nothingCLI for the Humanitix public API — list events, orders, tickets, attendees. x-api-key auth, parseArgs subcommands, JSON out. Get a key from the Humanitix Console → Account → Advanced → Public API key, put it in ~/.claude/.env as HUMANITIX_API_KEY.
node humanitix/humanitix.mjs --helpCLI for the Slack Web API (no deps, plain fetch). Read/search/post channels & DMs. A user token (xoxp-…) acts as you and can search; a bot token (xoxb-…) only sees channels it's invited to. Put whichever in ~/.claude/.env as SLACK_TOKEN, or use the logged-in Slack desktop app session (zero-setup).
node slack/slack.mjs helpCalDAV CLI for Radicale — calendars and address books as first-class ops (list/add/delete events & contacts) instead of hand-curled PROPFIND/REPORT. Zero-dependency, Node 18+. Auth via RADICALE_USERNAME/RADICALE_PASSWORD (+ RADICALE_BASE_URL or a --site you add to the SITES map).
node radicale/radicale.mjs --helpA nightly cross-repo surprise engine — scans a fleet of git repos and measures Bayesian belief-shift (KL(posterior‖prior) in nats), alarming only when something crosses an attention threshold (silent on a quiet morning). Edit lib/registry.mjs (the human seam) with your own heartbeats/expiries; wire parallax-nightly.sh to launchd/cron and set PARALLAX_TG_TO to Telegram yourself a digest.
node parallax/parallax.mjs scan --jsonApp-agnostic distribution CLIs for Apple App Store Connect (asc.py — iOS TestFlight + the full Mac App Store package→upload→attach→submit pipeline) and Google Play (gplay.py — AAB upload, release notes, store listing). No app identifiers are hardcoded: each command's target comes from a per-app config (~/.config/appstore/apps.json, see appstore/apps.example.json) selected via --app/$APPSTORE_APP/default. Keeps distribution-safety discipline — Apple submit is fail-closed behind --confirm, Play upload commits a Console draft. Python 3.10+; pip install -r appstore/requirements.txt.
asc --help # after: ln -sf "$PWD/appstore/asc.py" ~/.local/bin/asc
gplay --help # after: ln -sf "$PWD/appstore/gplay.py" ~/.local/bin/gplayShared Playwright plumbing (browser-context.mjs) used by the browser-driven
messaging tools. Not a standalone tool.
- One file per tool. Each script is readable top-to-bottom and carries its
own
--help. No build step, no shared framework to learn. - Secrets out-of-band. Code is committable because credentials live in env
vars or a git-ignored
.tokens/dir — never inline. - Read before write. The riskier tools default to read/observe; sending is an explicit, separate action.
MIT © 2026 Nick Meinhold