Skip to content

ops voice

auroracapital edited this page May 21, 2026 · 1 revision

/ops:voice — Voice, Phone & Video

Voice / phone / video interface for claude-ops. All API calls via curl — no SDK dependencies. Native handlers (Phone, FaceTime, Zoom desktop) require zero credentials; programmatic channels (Twilio, Bland AI, ElevenLabs, Groq, Zoom REST) resolve credentials via the standard claude-ops chain.

Skill: ~/.claude/plugins/data/ops-ops-marketplace/skills/ops-voice/SKILL.md Wrapper: ~/.claude/plugins/data/ops-ops-marketplace/bin/ops-voice Routed from: Skills Reference#opscomms for natural-language send flows.


TL;DR

# Native — no credentials, opens the macOS app
/ops:voice phone +1234567890
/ops:voice facetime user@example.com
/ops:voice zoom start

# Smart calendar joiner (v2.9.1)
/ops:voice join                  # joins the meeting that's happening now
/ops:voice join --at next        # skip current, join the next future meeting
/ops:voice join --dry-run        # show what would happen, don't launch

# Programmatic — credentials required
/ops:voice twilio-call  +1234567890 +15551234567 --twiml https://demo.twilio.com/docs/voice.xml
/ops:voice twilio-sms   +1234567890 +15551234567 "your message"
/ops:voice bland-call   +1234567890 "task prompt for the AI agent"
/ops:voice tts          "text to speak"  --voice <voice_id> --out /tmp/out.mp3
/ops:voice transcribe   /path/to/audio.mp3
/ops:voice zoom schedule "<topic>" --start "2026-05-22T15:00:00Z" --duration 30

Sub-commands

Sub-command Channel Needs creds? Cost Notes
phone <num> Phone.app via Continuity No iPhone plan macOS + iPhone, same iCloud, Calls on Other Devices ON
facetime <handle> [--audio] FaceTime No Free (Apple↔Apple) Defaults to video; --audio for audio only
zoom start Zoom desktop No Zoom plan Opens zoommtg:// instant meeting
zoom join <id> [--pwd …] Zoom desktop No Zoom plan Opens existing meeting in desktop app
zoom schedule "<topic>" … Zoom REST Yes (ZOOM_API_TOKEN) Zoom plan Server-to-Server OAuth access token
join Auto-detected from calendar No (uses gog calendar) v2.9.1 smart joiner — see below
twilio-call <to> <from> --twiml <url> Twilio Voice REST Yes Per-minute Outbound call following a TwiML URL
twilio-sms <to> <from> "<body>" Twilio SMS REST Yes Per-message Plain SMS via Twilio Messaging
bland-call <num> "<prompt>" Bland AI Yes Per-minute AI voice agent calls the number
tts "<text>" [--voice …] [--out …] ElevenLabs Yes Per-character Saves MP3, auto-plays on macOS
transcribe <file> Groq Whisper Yes Per-second Returns .text field
setup Configuration Scans env / Doppler / keychain, validates each key

join — Smart calendar-driven meeting joiner (v2.9.1)

/ops:voice join reads your Google Calendar via gog calendar events --all --today -j --sort start, picks the event that's happening now (within ±window minutes, default 10) or the next future meeting, extracts a conference URL, applies the AV policy below, then hands off to the native opener (which honours Plugin-Rules#rule-7--mobile--ssh-sessions-compact-text-no-tables — SSH/mobile sessions get a copy-able URL block instead of open).

Flags

--at now|next|HH:MM   default: now
--window MIN          default: 10 (±minutes around target time)
--dry-run             print plan, don't launch
--json                machine-readable output

Conference URL extraction

Walks the event in this order until something resolves:

  1. hangoutLink (Google Meet)
  2. conferenceData.entryPoints[].uri (any provider — Meet / Zoom / Teams / Webex)
  3. location field
  4. description regex scan: zoom.us/j/<ID>, meet.google.com/<code>, teams.microsoft.com/l/meetup-join/…, webex.com/meet/…

AV policy (smart heuristic)

Attendees Camera Microphone
1–2 ON ON
3–9 ON MUTED
10+ OFF MUTED

Per-event overrides

Tag the event title or description (case-insensitive) to force a setting:

[cam:on]   [cam:off]                  → force camera state
[mic:on]   [mic:off]   [mic:muted]    → force mic state

Example calendar event description:

Quarterly review with the board.
[cam:on] [mic:muted]

Overrides always beat the heuristic. Combine freely.

Mic source — lid state detection

The script picks the physical mic source based on whether your laptop lid is open or closed:

OS Detection Result
macOS ioreg -r -k AppleClamshellState"AppleClamshellState" = Yes Closed → external mic; Open → MacBook mic
Linux /proc/acpi/button/lid/*/stateclosed / open Same mapping
Windows / WSL / unknown n/a Reports default — meeting app keeps its remembered device

The script reports the chosen source and launches Camera Hub when present, but does not programmatically flip the in-app device picker — Zoom / FaceTime / Meet all remember the last-selected device, so flipping it once per app is permanent.

Elgato Virtual Camera auto-launch

If Elgato Camera Hub is installed, it's launched before the meeting opens so the virtual cam is registered as a usable device. Detection paths:

OS Path
macOS /Applications/Elgato Camera Hub.app, ~/Applications/Elgato Camera Hub.app, /Applications/Camera Hub.app
Linux elgato-camera-hub on $PATH, or AppImage at ~/Applications/Elgato*CameraHub*.AppImage
Windows / WSL ${PROGRAMFILES}/Elgato/CameraHub/CameraHub.exe

Zoom URL rewriting

When the picked event has a https://zoom.us/j/<ID>?pwd=<PWD> link, it's rewritten to:

zoommtg://zoom.us/join?confno=<ID>&pwd=<PWD>

so the desktop app opens directly with no browser middleman. When the AV policy resolves to cam=off, &zc=0 is appended (Zoom's "camera off on join" flag).

Dry-run output

$ /ops:voice join --dry-run
dry-run: would join "Weekly Sync" (meet, 3 attendees)
  url=https://meet.google.com/abc-defg-hij
  cam=on mic=muted
  lid=closed mic_source=external
  elgato_hub=/Applications/Elgato Camera Hub.app

JSON mode:

/ops:voice join --dry-run --json
# {"ok":true,"channel":"join","detail":"...","picked":{"summary":"...","provider":"meet","attendees":3,"url":"..."},"policy":{"cam":"on","mic":"muted","lid":"closed","mic_source":"external"}}

Credential resolution order

Programmatic channels (Twilio, Bland, ElevenLabs, Groq, Zoom REST) resolve each required key in this order — first hit wins:

  1. Environment variable (e.g. $TWILIO_AUTH_TOKEN)
  2. ops_cred_getlib/credential-store.sh reads OS-native keychain (macOS Keychain / secret-tool on Linux / Credential Manager on Windows)
  3. preferences.json — plugin data dir ($PREFS_PATH)
  4. Dopplerdoppler secrets get <KEY> --plain (scans all projects / configs)
  5. Password manager — Bitwarden / 1Password / Dashlane, if configured

Required keys by channel:

Channel Required env / key names
Twilio voice/SMS TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER
Bland AI BLAND_AI_API_KEY
ElevenLabs ELEVENLABS_API_KEY (optional ELEVENLABS_VOICE_ID, defaults to Rachel)
Groq Whisper GROQ_API_KEY
Zoom REST ZOOM_API_TOKEN (Server-to-Server OAuth access token — generate from Zoom Marketplace)

Cross-OS support

OS Native phone FaceTime Zoom start/join (desktop) Zoom schedule (REST) Twilio Bland ElevenLabs / Groq join smart joiner
macOS yes (Continuity) yes yes yes yes yes yes full (lid + Elgato + ioreg)
Linux no (Apple-only) no (Apple-only) yes (Zoom Linux client) yes yes yes yes full (/proc/acpi lid + Elgato detection)
Windows / WSL no (Apple-only) no (Apple-only) yes (Zoom Windows client) yes yes yes yes partial — no lid detection, mic source reports default; Elgato detected via $PROGRAMFILES

See OS Compatibility for the broader feature matrix.


SSH / mobile mode (Rule 7)

When $SSH_CONNECTION, $SSH_CLIENT, $SSH_TTY is set, or $OPS_MOBILE=1 is exported:

  • API channels (Twilio, Bland, ElevenLabs, Groq, Zoom REST) work unchanged — pure HTTP, no local UI.
  • Native channels (phone, facetime, zoom start|join, join) detect the SSH context, source lib/opener.sh, and call ops_open_url, which prints a copy-able URL block instead of spawning the host's open / xdg-open (which would launch a browser on the SSH target the user can't see).

This matches the broader Plugin-Rules#rule-7--mobile--ssh-sessions-compact-text-no-tables mobile/SSH convention used across every ops skill.


Outbound comms guardrail (Rule 6)

twilio-call, twilio-sms, and bland-call are 1:1 outbound channels — they ring or text a specific human. They are bound by Plugin-Rules#rule-6--outbound-comms-require-per-message-approval-always:

  1. Stage one draft. Show the full to, from, body, TwiML URL, or prompt.
  2. AskAskUserQuestion with [Send] / [Edit] / [Skip], or wait for a plain-chat approval word (ok, send, go, approved, ship it).
  3. Send that one. Then stage the next.

Batch sending is never allowed. This applies equally to direct skill use, /ops:comms routing, subagents, and the daemon. The user's defense-in-depth hook (block-outbound-comms.py with /tmp/.claude-send-ok one-shot, 120s TTL token) backs this up — but the skill itself is the primary gate.

Public-broadcast channels (Reddit / X / LinkedIn / Discord public / blog posts) are not covered — see Rule 6 in Plugin Rules for the full scope.


Routing from /ops:comms

The natural-language router in /ops:comms maps intent to ops-voice sub-commands:

User says Resolves to
call <name> ops-voice phone <number>
facetime <name> ops-voice facetime <handle>
start a zoom ops-voice zoom start
text <name> "..." ops-voice twilio-sms <to> <from> "..."
have an AI call <name> and tell ... ops-voice bland-call <number> "..."
join my meeting ops-voice join

Contact-number lookup uses the same resolver as WhatsApp (mcp__whatsapp__search_contacts) plus an optional contacts.json map in preferences.json.


Setup

/ops:voice setup

Scans all sources in parallel (Rule 4 — run_in_background: true):

  • Env vars: TWILIO_*, BLAND_AI_API_KEY, ELEVENLABS_API_KEY, GROQ_API_KEY, ZOOM_*
  • Shell profiles: ~/.zshrc, ~/.bashrc, ~/.zprofile, ~/.envrc
  • Doppler — every project × dev|stg|prd config
  • OS-native keychain (macOS / Linux secret-tool / Windows Credential Manager)
  • Native prerequisites: /Applications/zoom.us.app, FaceTime.app, Phone.app

Validates each found key with a live probe:

Channel Probe
Twilio GET https://api.twilio.com/2010-04-01/Accounts/$SID.json
Bland GET https://api.bland.ai/v1/me
ElevenLabs GET https://api.elevenlabs.io/v1/voices?page_size=1
Groq GET https://api.groq.com/openai/v1/models
Zoom GET https://api.zoom.us/v2/users/me

For each unset service, presents AskUserQuestion with [Paste manually] / [Deep hunt — spawn agent] / [Skip] — Rule 3 forbids silent skips.


Related

Clone this wiki locally