A collection of lightweight CLI tools for AI content generation and chat operations. Zero dependencies beyond Python 3.10+ stdlib.
| CLI | Description | Auth |
|---|---|---|
gemini-image |
Generate images via Imagen 4.0 / Gemini native models | GEMINI_API_KEY |
gemini-tts |
Text-to-speech via Gemini native audio | GEMINI_API_KEY |
gemini-transcribe |
Transcribe audio files via Gemini | GEMINI_API_KEY |
gemini-video |
Generate video via Google Veo 2/3/3.1 | GEMINI_API_KEY |
gemini-vision |
Analyze images/videos via Gemini (supports YouTube, Instagram, TikTok) | GEMINI_API_KEY |
slackcli |
Lightweight Slack client (channels, messages, search, reactions) | SLACK_USER_TOKEN |
llm-usage |
Monitor LLM token usage, costs, and quotas across providers | ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY |
agent-run |
Background wrapper for coding agents (Claude Code, Codex…) with PTY steering + live log streaming | — |
# One-liner (pip)
pip install git+https://github.com/marcioapm/toolbox.git
# Or clone and install in editable mode
git clone https://github.com/marcioapm/toolbox.git
cd toolbox
pip install -e .
# Or use the install script
curl -sSL https://raw.githubusercontent.com/marcioapm/toolbox/main/install.sh | bashSet your API keys as environment variables:
# Gemini API key (get one at https://aistudio.google.com/apikey)
export GEMINI_API_KEY="your-key-here"
# Slack user token (get one at https://api.slack.com/apps → OAuth & Permissions)
export SLACK_USER_TOKEN="xoxp-your-token-here"Add them to your shell profile (~/.bashrc, ~/.zshrc, etc.) for persistence.
Generate images using Google's Imagen 4.0 or Gemini native image models.
| Model | Speed | Quality | Notes |
|---|---|---|---|
imagen-4.0-generate-001 |
Medium | Best | Default, production-ready |
imagen-4.0-ultra-generate-001 |
Slow | Highest | Maximum quality |
imagen-4.0-fast-generate-001 |
Fast | Good | Quick iterations |
nano-banana-pro-preview |
Medium | Good | Gemini native |
gemini-3-pro-image-preview |
Medium | Good | Gemini 3 Pro |
gemini-3.1-flash-image-preview |
Fast | OK | Fastest native |
# Basic generation
gemini-image "a cat riding a skateboard"
# Custom output and model
gemini-image "corporate logo, minimal" -o logo.png -m imagen-4.0-fast-generate-001
# Multiple images
gemini-image "abstract art" -n 4 -o art.png
# Saves: art.png, art-2.png, art-3.png, art-4.png
# Custom aspect ratio
gemini-image "landscape photo" --aspect 16:9 -o wide.png
# Using Gemini native model
gemini-image "watercolor painting of a forest" -m gemini-3-pro-image-previewpositional:
prompt Image generation prompt
options:
-o, --output FILE Output file (default: output.png)
-m, --model MODEL Model to use (default: imagen-4.0-generate-001)
-n, --count N Number of images, 1-4 (default: 1)
--aspect RATIO Aspect ratio (default: 1:1)
Text-to-speech using Gemini's native audio generation.
Models:
| Model | Speed | Quality |
|---|---|---|
gemini-2.5-flash-preview-tts |
Fast | Good (default) |
gemini-2.5-pro-preview-tts |
Slower | More expressive |
Voices:
| Voice | Character |
|---|---|
| Kore | Default, neutral |
| Aoede | Deep, expressive |
| Charon | Deep, authoritative |
| Fenrir | Strong, bold |
| Puck | Light, playful |
| Orbit | Calm, measured |
| Vale | Warm, gentle |
# Basic TTS
gemini-tts "Hello, world!" -o hello.wav
# Choose voice and model
gemini-tts "Breaking news from the tech world" -v Charon -m gemini-2.5-pro-preview-tts
# Expressive voice for storytelling
gemini-tts "Once upon a time in a land far away..." -v Aoede -m gemini-2.5-pro-preview-tts -o story.wav
# Quick announcement
gemini-tts "Your build has completed successfully" -v Puckpositional:
text Text to speak
options:
-o, --output FILE Output WAV file (default: output.wav)
-m, --model MODEL TTS model (default: gemini-2.5-flash-preview-tts)
-v, --voice VOICE Voice name (default: Kore)
Transcribe audio files via the Gemini API.
| Model | Speed | Notes |
|---|---|---|
gemini-2.5-flash |
Fast | Default, cheap |
gemini-2.5-pro |
Slower | More accurate, pricier |
Model names are accepted as free-form strings, so any new Gemini model can be passed via -m.
# Basic transcription (prints transcript to stdout)
gemini-transcribe meeting.mp3
# Save transcript to a file
gemini-transcribe call.ogg -o transcript.txt
# Use the more accurate model with a language hint
gemini-transcribe lecture.wav -m gemini-2.5-pro --language Portuguese
# Custom prompt (e.g. add speaker labels)
gemini-transcribe interview.m4a --prompt "Transcribe with speaker labels (Speaker A, Speaker B)."
# Get full JSON response instead of just text
gemini-transcribe note.opus --jsonpositional:
audio_path Path to audio file (.ogg/.opus, .mp3, .wav, .m4a, .flac, .aac, .webm)
options:
-o, --output FILE Write transcript to file (default: stdout)
-m, --model MODEL Gemini model (default: gemini-2.5-flash)
--prompt TEXT Custom transcription prompt
--language TEXT Optional language hint, e.g. "Portuguese"
--json Output the full JSON response
--api-key TEXT Gemini API key [env: GEMINI_API_KEY]
Note: Files larger than ~19 MB are rejected (Gemini's inline_data limit is 20 MB). Use the Files API for larger audio.
Generate videos using Google's Veo models. Submits an async job and polls until completion.
| Model | Speed | Quality | Notes |
|---|---|---|---|
veo-3.0-fast-generate-001 |
Fast | Good | Default |
veo-3.0-generate-001 |
Slow | High | Best Veo 3 |
veo-3.1-fast-generate-preview |
Fast | Good | Latest fast |
veo-3.1-generate-preview |
Slow | Highest | Latest quality |
veo-2.0-generate-001 |
Medium | OK | Older model |
# Basic video generation
gemini-video "a drone flying over mountains at sunset"
# High quality with specific model
gemini-video "time-lapse of a flower blooming" -m veo-3.0-generate-001 -o flower.mp4
# Vertical video (e.g., for mobile/social)
gemini-video "person walking through a neon-lit city" --aspect 9:16 -o vertical.mp4
# Quick draft
gemini-video "ocean waves crashing on rocks" -m veo-3.0-fast-generate-001positional:
prompt Video generation prompt
options:
-o, --output FILE Output file (default: output.mp4)
-m, --model MODEL Model (default: veo-3.0-fast-generate-001)
--aspect RATIO Aspect ratio (default: 16:9)
Note: Video generation is async. The CLI submits the job and polls every 5 seconds. Typical generation takes 1-5 minutes depending on the model.
Lightweight Slack CLI that uses a user token to act as you (not a bot).
# List channels
slackcli channels
slackcli ch -n 50
# Read history
slackcli history C02DLS4PFH7
slackcli h C02DLS4PFH7 -n 30
# Send a message
slackcli send C02DLS4PFH7 "Hello from the CLI!"
slackcli s C02DLS4PFH7 "Quick update: deploy complete"
# Reply in a thread
slackcli reply C02DLS4PFH7 1710430020.123456 "Thread reply here"
# Search messages
slackcli search "deployment failed" -n 5
slackcli search "from:@alice bug report"
# List users
slackcli users
slackcli userinfo U01234ABCDE
# Get DM channel ID
slackcli dm U01234ABCDE
# Check unread messages
slackcli unread
# React to a message
slackcli react C02DLS4PFH7 1710430020.123456 thumbsup
slackcli unreact C02DLS4PFH7 1710430020.123456 thumbsup| Command | Alias | Description |
|---|---|---|
channels |
ch |
List channels (public, private, DMs) |
history |
h |
Read channel message history |
send |
s |
Send a message to a channel |
reply |
r |
Reply in a thread |
search |
— | Search messages across workspace |
users |
u |
List workspace members |
userinfo |
ui |
Show user details (name, email, timezone) |
dm |
— | Get or create a DM channel ID |
unread |
— | Show channels with unread messages |
react |
— | Add an emoji reaction |
unreact |
— | Remove an emoji reaction |
Your SLACK_USER_TOKEN needs these scopes:
channels:read,channels:history— Read public channelsgroups:read,groups:history— Read private channelsim:read,im:history— Read DMschat:write— Send messagessearch:read— Search messagesusers:read— List/view usersreactions:write— Add/remove reactions
Analyze images and videos using Gemini's multimodal capabilities. Auto-downloads videos from YouTube, Instagram, TikTok, X/Twitter, Vimeo, and more via yt-dlp.
# Analyze a local image
gemini-vision photo.jpg
# Describe with custom prompt
gemini-vision screenshot.png -p "What's the error in this screenshot?"
# Transcribe speech from a video
gemini-vision video.mp4 -p "Transcribe all speech in this video"
# Analyze YouTube video
gemini-vision "https://youtube.com/watch?v=dQw4w9WgXcQ" -p "Summarize this video"
# Instagram reel
gemini-vision "https://instagram.com/reel/ABC123/" -p "Describe what happens"
# TikTok / X post
gemini-vision "https://tiktok.com/@user/video/123" -p "What's in this video?"
gemini-vision "https://x.com/user/status/123" -p "Describe the video"
# Use a different model
gemini-vision photo.jpg -m gemini-2.5-pro -p "Detailed art analysis"
# Keep the downloaded video file
gemini-vision "https://youtube.com/watch?v=..." --keepYouTube, Instagram, TikTok, X/Twitter, Vimeo, Facebook, Reddit — anything yt-dlp supports.
positional:
file Image/video path, URL, or social media link
options:
-p, --prompt TEXT Analysis prompt [default: Describe what you see in detail.]
-m, --model [...] Gemini model [default: gemini-2.5-flash]
--keep Keep downloaded video (don't delete temp file)
--api-key TEXT Gemini API key [env: GEMINI_API_KEY]
yt-dlpfor social media downloads:brew install yt-dlp
Monitor LLM token usage and quotas across Anthropic, OpenAI, and Google Gemini.
| Provider | Rate limits | Token usage | Cost |
|---|---|---|---|
| Anthropic | ✅ via response headers | ✅ via OpenClaw logs | ✅ |
| OpenAI | ✅ via response headers | ✅ org API (needs admin key) + OpenClaw logs | ✅ |
| Gemini | — (no API) | ✅ via OpenClaw logs | ✅ |
# Check all providers
llm-usage
# Check specific provider
llm-usage -p anthropic
llm-usage -p openai
# JSON output (for scripts/agents)
llm-usage --json
# Skip OpenClaw local stats
llm-usage --no-openclaw🔍 LLM Usage Monitor
2026-03-15 17:39
✅ ANTHROPIC
Status: ok
Rate limits:
requests-limit: 4000
requests-remaining: 3999
tokens-limit: 400000
tokens-remaining: 399990
✅ OPENAI
Status: ok
✅ GEMINI
Status: ok
Available models: 45
📊 OPENCLAW LOCAL USAGE
Today:
anthropic: 121.2M tokens (925 in / 192.9K out) · cache: 112.9M read / 8.2M write · $112.39 · 728 reqs
openai-codex: 7.8M tokens (3.4M in / 5.7K out) · cache: 4.3M read / 0 write · $6.80 · 42 reqs
Week:
anthropic: 1298.7M tokens (12.7K in / 2.4M out) · cache: 1190.8M read / 105.5M write · $1314.18 · 11751 reqs
google: 33.6M tokens (33.5M in / 63.0K out) · $73.59 · 451 reqs
openai-codex: 83.2M tokens (17.2M in / 146.4K out) · $43.62 · 794 reqs
options:
-p, --provider [anthropic|openai|gemini|all] Provider(s) to check [default: all]
--openclaw / --no-openclaw Include OpenClaw local session usage [default: openclaw]
--json Output as JSON
--anthropic-api-key TEXT Anthropic API key [env: ANTHROPIC_API_KEY]
--openai-api-key TEXT OpenAI API key [env: OPENAI_API_KEY]
--openai-admin-key TEXT OpenAI admin key for usage API [env: OPENAI_ADMIN_KEY]
--gemini-api-key TEXT Gemini API key [env: GEMINI_API_KEY]
threadctl is not shipped by this package. It now lives in its own
repo (marcioapm/threadctl) — the toolbox copy was a stale fork that was
missing live subcommands and got installed over the real binary on a
production host, so it was deleted here rather than re-synced.
All tools follow the same patterns:
- Auth via environment variables — set
GEMINI_API_KEYand/orSLACK_USER_TOKEN - Positional argument for main input — prompt text, search query, etc.
- Flags for options —
-ooutput,-mmodel,-ncount,-vvoice - Exit codes — 0 = success, 1 = error (with stderr message)
- Human-readable stdout — file paths, message timestamps, channel IDs
- No interactive prompts — everything is flags/args, suitable for scripting
# Image: generate → save to file
gemini-image "prompt" -o /tmp/out.png -m imagen-4.0-fast-generate-001
# TTS: text → WAV file
gemini-tts "text to speak" -o /tmp/speech.wav -v Aoede
# Transcribe: audio file → text
gemini-transcribe meeting.mp3 -o /tmp/transcript.txt
# Video: prompt → MP4 (takes minutes, async polling)
gemini-video "prompt" -o /tmp/video.mp4
# Vision: analyze images/videos (YouTube, Instagram, etc.)
gemini-vision photo.jpg -p "What's in this image?"
gemini-vision "https://youtube.com/watch?v=..." -p "Summarize this video"
gemini-vision video.mp4 -p "Transcribe the speech"
# Slack: read unread → send reply
slackcli unread
slackcli history CHANNEL_ID -n 10
slackcli send CHANNEL_ID "message"
slackcli react CHANNEL_ID TIMESTAMP emoji_name
# Usage: check token spending across providers
llm-usage
llm-usage --json
llm-usage -p anthropic
# Agent-run: background coding agents with steering + live logs
agent-run build claude --permission-mode bypassPermissions --print 'Refactor X'
agent-run tail build # follow logs in real time
agent-run status build # running | done | failed | died | killed
agent-run -i chat claude --permission-mode bypassPermissions
agent-run steer chat 'Also add tests for edge cases.'
agent-run kill chat # TERM the run; runner does its own cleanup
agent-run reap --dry-run # preview idle-kills + terminal-state cleanupBackground wrapper for long-running coding agents (Claude Code, Codex, Pi, OpenCode). Creates a run directory with structured state files you can poll safely — no brittle process-poll loops — and adds a stdin FIFO when you need to steer an interactive agent mid-flight.
Storage is split across two roots so a hard crash or reboot never loses a log even though the ephemeral process state is gone:
/tmp/agent-runs/<name>/— ephemeral process state (pid, status, exit_code, FIFO). tmpfs on Linux, wiped on reboot — a missing entry here unambiguously means "not running". Override withAGENT_RUN_STATE_DIR./var/tmp/agent-runs/<name>/— persistent log, cleaned transcript, a copy of the prompt file, and a per-run scratch dir (tmp/, mode 0700) exported asTMPDIRinto the launched command's environment. Survives reboot/crash; the log fd is opened here from the start, so there's no copy-on-exit step a crash could lose. Override withAGENT_RUN_LOG_DIR.log/log.clean/promptare pruned automatically after 21 days; thetmp/scratch dir is instead cleaned up byagent-run reap(see below).
Every launch gets its own disk-backed scratch dir at
$AGENT_RUN_LOG_DIR/<name>/tmp/, exported as TMPDIR into the launched
command's environment (and therefore every descendant it forks/execs). The
agent's argv is never modified — only the environment carries this. This
exists to contain tools that dump large, un-cleaned scratch data into
$TMPDIR: OpenCode's bundled JDTLS, for example, creates
mkdtemp()-based Eclipse workspaces (routinely hundreds of MB for a large
Java repo) and never removes them. Left to land in the system /tmp —
which is tmpfs (RAM) on most Linux hosts — enough leaked runs can consume
tens of GB of RAM. Routing each run's TMPDIR into its own directory means
reaping the run also reaps whatever it leaked.
The scratch dir is not deleted when the run ends — postmortem
artifacts matter for debugging a crashed or misbehaving agent. agent-run reap removes it when terminal state is old enough (and independently removes
aged orphaned scratch after a reboot loses state). Relaunching the same run
name intentionally replaces the prior log directory, including its scratch.
# Non-interactive (one-shot, e.g. claude --print, codex exec):
agent-run build claude --permission-mode bypassPermissions --print 'Build the thing'
# Interactive (steerable via stdin FIFO):
agent-run -i chat claude --permission-mode bypassPermissionsagent-run list # non-terminal runs only (default)
AGENT_RUN_LIST_DEFAULT=all agent-run list # restore the pre-filter default for a caller
agent-run list --all # every recognized run, including done/failed/died/killed
agent-run list --status died,killed # only runs whose status is in this set
agent-run status <name> # one-line status
agent-run logs <name> [N] # last N lines (default 50)
agent-run tail <name> # follow log (exits when agent dies)
agent-run steer <name> '<message>' # write to agent stdin (needs -i)
agent-run kill <name> [SIGNAL] # default TERM; KILL force-terminates
agent-run reap [--dry-run] [--idle-hours N] [--min-age-hours N] [--force-unknown] [--name NAME]kill sends TERM/INT/HUP straight to the identity-verified runner, which
catches it and runs its own teardown (kill/reap the workload, publish
terminal state). agent-run kill <name> KILL does not send a raw SIGKILL
to the runner — an uncatchable signal would skip that teardown and orphan
the running agent while state still said "running". Instead it TERMs the
runner first and waits a bounded window for normal teardown; only if the
runner is still alive after that does it re-verify identity and
parentage, KILL the runner and its recorded children directly, and
publish terminal state itself. Only TERM, INT, HUP, and KILL are accepted;
other signals are rejected rather than forwarded.
status reports not running (log preserved) when the process state is
gone (e.g. after a reboot) but the log survived in /var/tmp. list
defaults to showing only non-terminal runs (starting/running/stalled)
from /tmp — pass --all or --status <list> to include conclusively
terminal ones (done/failed/died/killed), or set
AGENT_RUN_LIST_DEFAULT=all to restore the prior default without changing
call sites. Unrecognized/legacy/corrupt statuses are always shown under an
explicit Unrecognized / needs attention heading rather than as live runs;
use reap --force-unknown only after operator review. Preserved-log-only runs
(state dir already gone) appear separately. logs/tail/clean always read
the persistent log, falling back to the old single-directory layout for runs
launched before the state/log split.
agent-run reap reconciles stale state and cleans up old runs in one pass:
- Stale-running reconciliation (unchanged from before): a
runningrun whose pid is missing, malformed, or gone is markeddied; arunningrun whose pid is alive but whose log has been idle longer than--idle-hours(orAGENT_RUN_IDLE_KILL_HOURS, default 24h) is idle-killed through the same identity-verified escalationagent-run kill <name> KILLuses, and markedkilled. - Terminal-state and orphan-scratch garbage collection: runs whose
status is conclusively terminal (
done,failed,died,killed) and whoseended_atis older than--min-age-hours(or preferredAGENT_RUN_MIN_AGE_HOURS; compatible aliasAGENT_RUN_REAP_MIN_AGE_HOURS, default 168h/7 days) have their ephemeral state dir and scratch dir (tmp/, theTMPDIR) removed. Persistentlog/log.clean/promptsurvive reap. A state-lesstmp/left after a reboot is independently collected once its contents have aged past the same threshold. A run reconciled todied/killedin this invocation is never collected in the same invocation. Unknown/legacy/corrupt statuses are left intact by default and require--force-unknownto collect after review.
Both steps only ever act after re-verifying a live pid belongs to the
recorded runner (_pid_alive/_process_identity) or an inode hasn't been
swapped out from under the scan (_safe_rmtree's root-contained,
inode-reverified deletion, plus the same per-name launch lock used to
serialize relaunches). State GC first atomically renames a directory to a
reserved sentinel so an interrupted deletion is resumed by the next reap;
--dry-run runs the same read-only eligibility checks and prints only actions
a real reap would take, without mutating or deleting anything.
Ephemeral, under $AGENT_RUN_STATE_DIR/<name>/ (default /tmp/agent-runs):
| File | Contents |
|---|---|
status |
starting / running / done / failed / died / killed |
exit_code |
numeric exit code (after completion) |
pid, pgid |
agent session/group leader pid (== pgid under setsid); pgid is informational only, not a kill target |
process_identity |
platform-specific runner birth token, verified before kill signals the runner |
command |
pretty-printed launch command |
argv |
JSON-encoded argv (authoritative form for replay) |
started_at, ended_at |
ISO-8601 UTC timestamps |
stdin |
FIFO for steer (only when launched with -i) |
pty_pid |
PID of the PTY child (interactive only) |
keeper_pid |
PID of the FIFO keeper (interactive only) |
interactive |
1 if launched with -i, else 0 |
reap_reason |
set by agent-run reap when it changes status (died/killed) |
tmp_dir |
absolute path to this run's scratch dir |
Persistent, under $AGENT_RUN_LOG_DIR/<name>/ (default /var/tmp/agent-runs):
| File | Contents |
|---|---|
log |
combined stdout+stderr (PTY-captured in interactive mode) |
log.clean |
rendered transcript (only when launched with --echo) |
prompt |
copy of the -f/--prompt-file input, if one was given |
tmp/ |
per-run scratch dir exported as TMPDIR; removed only by agent-run reap, never on normal run exit |
- Written in Python (
src/toolbox/agent_run.py), installed as a[project.scripts]entry point alongside the rest of the toolbox. - Double-forks and
os.setsid()on launch so the run becomes its own session + process-group leader.agent-run killsignals the identity-verified runner directly (never the whole process group); on Linux the signal is bound to a pidfd opened before the final identity check, closing the ordinary PID-recycling window. Darwin has no pidfd equivalent, so a narrow TOCTOU gap remains between the last identity re-check and the actual signal call — accepted as a residual risk, not fully closed. - Each run name is serialized by a permanent per-name lock file under
$AGENT_RUN_STATE_DIR/.locks/<name>.lock; these are never pruned, so concurrent launches/prunes of the same name always contend on the same lock inode. The detached runner holds its own inherited copy of that lock fd until it has published its identity and resolved readiness, so a launcher dying mid-setup cannot release the lock early. - On interactive runs, a dedicated "keeper" child process holds the FIFO
open for writing (
O_RDWR) so readers never see EOF between steers. - The PTY is allocated via
pty.fork(); the parent runs aselect()loop that shuttles FIFO → PTY master (keystrokes) and PTY master → log file (agent output). Works identically on Linux and macOS without depending on the (different)script(1)flavors. - SIGTERM/INT/HUP handlers always finalize
status+exit_code+ended_at, even when the launcher is killed mid-run.statusstarts atstarting(published before the detached runner exists) and only moves torunningonce the runner is actually controllable — child spawned/exec'd for one-shot, or PTY/FIFO/keeper ready for interactive. Any setup failure before that point still resolves synchronously tofailed, never leavingstartingstranded with no process behind it. TMPDIRis exported intoos.environinside the detached runner before any child is forked/exec'd, so the launched agent — and anything it in turn forks or execs — inherits it automatically; the launch argv itself is never touched. This is deliberately environment-only rather than argv-injectedenv TMPDIR=..., since some agents' own argument parsers would otherwise need to understand and pass through an unrecognized wrapper prefix.agent-run list's default view andagent-run reap's garbage-collection eligibility share one definition of "terminal" (done/failed/died/killed) so they never disagree about which runs are "done with". A script that previously scraped every line underlist's "Live runs" heading and relied on terminal runs appearing there needs--allnow — this is the one deliberate behavior break in this feature; the heading text also changes to describe the filter actually in effect.
MIT