ops-voice

GitHub

macOS语音与视频操作技能,支持Phone、FaceTime、Zoom、Meet等原生应用及Twilio、Bland AI等API服务。通过curl调用,无SDK依赖,具备统一凭证解析机制和出站通信审批护栏。

claude-ops/skills/ops-voice/SKILL.md Lifecycle-Innovations-Limited/claude-ops

Trigger Scenarios

需要发起或接听电话/视频会议 需要通过Twilio或AI代理发送语音/SMS消息 需要调度Zoom会议

Install

npx skills add Lifecycle-Innovations-Limited/claude-ops --skill ops-voice -g -y
More Options

Non-standard path

npx skills add https://github.com/Lifecycle-Innovations-Limited/claude-ops/tree/main/claude-ops/skills/ops-voice -g -y

Use without installing

npx skills use Lifecycle-Innovations-Limited/claude-ops@ops-voice

指定 Agent (Claude Code)

npx skills add Lifecycle-Innovations-Limited/claude-ops --skill ops-voice -a claude-code -g -y

安装 repo 全部 skill

npx skills add Lifecycle-Innovations-Limited/claude-ops --all -g -y

预览 repo 内 skill

npx skills add Lifecycle-Innovations-Limited/claude-ops --list

SKILL.md

Frontmatter
{
    "name": "ops-voice",
    "description": "Voice operations — native macOS Phone (Continuity), FaceTime, Zoom, Google Meet, WhatsApp call, Twilio voice + SMS, Bland AI agent calls, ElevenLabs TTS, Whisper transcription. All curl-based, no SDK deps.",
    "allowed-tools": [
        "Bash",
        "Read",
        "Write",
        "AskUserQuestion",
        "WebFetch"
    ],
    "argument-hint": "[phone|facetime|zoom|meet|whatsapp-call|twilio-call|twilio-sms|bland-call|tts|transcribe|setup]"
}

OPS:VOICE — Voice Operations

Voice / phone / video interface. All API calls via curl — no SDK dependencies. Native macOS handlers (Phone.app, FaceTime, Zoom) require no credentials; programmatic channels (Twilio, Bland, ElevenLabs, Groq, Zoom schedule) resolve credentials via:

Credential resolution order: env vars → ops_cred_get (lib/credential-store.sh / keychain) → preferences.json → Doppler CLI (doppler secrets get <KEY> --plain) → password manager.

All sub-commands have a thin bash wrapper at bin/ops-voice — prefer it over inline curl when scripting.

Outbound comms guardrail (Rule 6): twilio-call, twilio-sms, and bland-call are 1:1 outbound channels and MUST follow the per-message approval gate — stage final draft, show full target+body, wait for explicit approval (AskUserQuestion or single-word chat approval), send one, then stage the next. Never batch.


Sub-commands

Parse $ARGUMENTS for the command keyword, then execute. Native handlers exit fast; API calls report status and (where relevant) a poll command.


phone <number> — Native Phone.app via Continuity

Routes through the linked iPhone. Requires macOS + iPhone signed in to the same iCloud account with Calls on Other Devices enabled. No credentials.

bin/ops-voice phone "+1234567890" --json
# {"ok":true,"channel":"phone","detail":"dialing +1234567890 via Phone.app (Continuity)"}

Under the hood: open "tel:<E.164>".


facetime <number-or-email> [--audio] — FaceTime video/audio

Defaults to video. Pass --audio for FaceTime Audio (free, Apple↔Apple). Accepts phone numbers or Apple-ID emails.

bin/ops-voice facetime user@example.com               # video
bin/ops-voice facetime "+1234567890" --audio --json   # audio

Under the hood: open "facetime://<handle>" or open "facetime-audio://<handle>".


zoom start|join|schedule — Zoom meetings

# Open zoom.us app and start a new instant meeting
bin/ops-voice zoom start

# Join an existing meeting
bin/ops-voice zoom join 1234567890 --pwd <password>

# Schedule a meeting via Zoom REST API (requires ZOOM_API_TOKEN — Server-to-Server OAuth access token)
bin/ops-voice zoom schedule "<topic>" --start "2026-05-22T15:00:00Z" --duration 30 --json
# {"ok":true,"channel":"zoom","detail":"scheduled meeting 12345 — https://us05web.zoom.us/j/..."}

Native start/join use zoommtg:// URL scheme. Schedule requires ZOOM_API_TOKEN resolved via the order above. Generate a Server-to-Server OAuth app in your Zoom Marketplace, then exchange for an access token.


meet start|join — Google Meet

# Open a brand-new instant meeting (https://meet.new)
bin/ops-voice meet start

# Join by meeting code (xxx-yyyy-zzz) or full URL
bin/ops-voice meet join abc-defg-hij
bin/ops-voice meet join https://meet.google.com/abc-defg-hij --json

meet start opens https://meet.new in the default browser (creates a fresh meeting and lands you in it). meet join accepts a 3-4-3 Google Meet code, a bare code, or a full URL — all are normalized to https://meet.google.com/<code>. No credentials needed; the user must be signed into Google in the browser.


whatsapp-call <number> [--video] — WhatsApp Desktop voice/video call

# Voice call (default)
bin/ops-voice whatsapp-call "+1234567890"

# Video call
bin/ops-voice whatsapp-call "+1234567890" --video --json

Opens WhatsApp Desktop via the whatsapp://call?phone=<digits> or whatsapp://video?phone=<digits> URL scheme. Requires WhatsApp Desktop installed and signed in. The leading + is stripped — WhatsApp expects digits only after phone=. No credentials needed; the WhatsApp session lives in the desktop app.


join [--at now|next|HH:MM] [--window MIN] [--dry-run] — Smart calendar-driven meeting joiner

Auto-joins the meeting that's happening now (within ±window minutes, default 10) or the next future meeting if nothing is current. Reads gog calendar events --all --today -j --sort start, extracts a conference URL from hangoutLinkconferenceData.entryPoints[] → location → description scan (supports Zoom, Google Meet, Microsoft Teams, Webex), applies the smart AV policy below, and hands off to the native opener (which honors Rule 7 on SSH/mobile).

bin/ops-voice join                 # join current/next meeting
bin/ops-voice join --at next       # skip current, go to next future
bin/ops-voice join --window 5      # only consider events within ±5min of now
bin/ops-voice join --dry-run       # show what would happen — no launch
bin/ops-voice join --dry-run --json

AV policy (smart heuristic):

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

Per-event overrides — tag the event description (case-insensitive):

[cam:on] [mic:off]      → force camera on, mic muted
[cam:off]               → force camera off (keep heuristic mic)
[mic:muted]             → force mic muted

Mic source — lid state:

  • macOS via ioreg AppleClamshellStateYes=closed (external mic), No=open (MacBook mic).
  • Linux via /proc/acpi/button/lid/*/state → "open"/"closed".
  • Other OS / unknown → reports default; the meeting app uses whatever the system has selected.

Note: the script reports the policy and launches Camera Hub when present, but it does not programmatically flip Zoom/FaceTime/Meet in-app device settings — those apps remember the last-selected device, so flipping it once per app is permanent. (A future patch could AppleScript Zoom's preferences pane.)

Elgato Virtual Camera: if Elgato Camera Hub is installed (any OS), it's launched before the meeting opens so the virtual cam is registered. Detection paths:

  • 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 converted to zoommtg://zoom.us/join?confno=<ID>&pwd=<PWD> so the desktop app opens directly (no browser prompt). If cam=off from the policy, &zc=0 is appended.

Dry-run output (text mode):

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

twilio-call <to> <from> --twiml <URL> — Programmatic outbound voice

Real telco call (per-minute cost). Requires TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN. The --twiml URL must return TwiML XML describing call behavior — e.g. https://demo.twilio.com/docs/voice.xml or a custom Function/Studio flow.

bin/ops-voice twilio-call "+1234567890" "+15551234567" \
  --twiml "https://demo.twilio.com/docs/voice.xml" --json

twilio-sms <to> <from> "<body>" — Programmatic outbound SMS

bin/ops-voice twilio-sms "+1234567890" "+15551234567" "<body>" --json

For inbound SMS / WhatsApp routing, point your Twilio webhook at the ops daemon (out of scope for v1).


bland-call <number> "<prompt>" — Bland AI agent phone call

AI agent calls the number and follows the natural-language prompt. Recordings + transcripts available via the poll URL printed on success.

bin/ops-voice bland-call "+1234567890" "<task prompt>" --json

Poll with: curl -H "authorization: $BLAND_AI_API_KEY" https://api.bland.ai/v1/calls/<call_id>.


tts [text] [--voice voice_id] [--out file.mp3] — ElevenLabs text-to-speech

Requires: ELEVENLABS_API_KEY (env / keychain / Doppler).

EL_KEY="${ELEVENLABS_API_KEY:-$(doppler secrets get ELEVENLABS_API_KEY --plain 2>/dev/null || true)}"
VOICE_ID="${ELEVENLABS_VOICE_ID:-21m00Tcm4TlvDq8ikWAM}"  # Rachel
TEXT="<from $ARGUMENTS>"
OUT_FILE="${OUT_FILE:-/tmp/ops-tts-$(date +%s).mp3}"

curl -s -X POST "https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}" \
  -H "xi-api-key: $EL_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"text\": \"$TEXT\",
    \"model_id\": \"eleven_monolingual_v1\",
    \"voice_settings\": {\"stability\": 0.5, \"similarity_boost\": 0.75}
  }" \
  --output "$OUT_FILE"

command -v afplay >/dev/null && afplay "$OUT_FILE" &
echo "Audio saved to: $OUT_FILE"

transcribe [file_path] — Groq Whisper transcription

Requires: GROQ_API_KEY (env / keychain / Doppler).

GROQ_KEY="${GROQ_API_KEY:-$(doppler secrets get GROQ_API_KEY --plain 2>/dev/null || true)}"
AUDIO_FILE="<from $ARGUMENTS>"

[ -f "$AUDIO_FILE" ] || { echo "ERROR: $AUDIO_FILE not found"; exit 1; }

curl -s -X POST "https://api.groq.com/openai/v1/audio/transcriptions" \
  -H "Authorization: Bearer $GROQ_KEY" \
  -F "file=@$AUDIO_FILE" \
  -F "model=whisper-large-v3" \
  -F "response_format=json" | \
  jq -r '.text'

setup — Configure voice channels

Before asking for anything, auto-scan ALL sources in one background batch (Rule 4: run_in_background: true):

# Env vars
printenv \
  TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_FROM_NUMBER \
  BLAND_AI_API_KEY ELEVENLABS_API_KEY GROQ_API_KEY \
  ZOOM_API_TOKEN ZOOM_ACCOUNT_ID ZOOM_CLIENT_ID ZOOM_CLIENT_SECRET 2>/dev/null

# Shell profiles
grep -hE 'TWILIO|BLAND|ELEVENLABS|GROQ|ZOOM' \
  ~/.zshrc ~/.bashrc ~/.zprofile ~/.envrc 2>/dev/null | grep -v '^#'

# Doppler — ALL projects
for proj in $(doppler projects --json 2>/dev/null | jq -r '.[].slug'); do
  for cfg in dev stg prd; do
    doppler secrets --project "$proj" --config "$cfg" --json 2>/dev/null | \
      jq -r --arg p "$proj" --arg c "$cfg" \
        'to_entries[]
         | select(.key | test("TWILIO|BLAND|ELEVENLABS|GROQ|ZOOM"; "i"))
         | "\(.key)=\(.value.computed | .[0:12])... (doppler:\($p)/\($c))"'
  done
done

# Keychain
for svc in twilio bland-ai elevenlabs groq zoom; do
  security find-generic-password -s "$svc" -w 2>/dev/null >/dev/null \
    && echo "[keychain] $svc ✓"
done

# Native macOS prerequisites
ls /Applications/zoom.us.app \
   /System/Applications/FaceTime.app \
   /System/Applications/Phone.app 2>/dev/null

Validate found keys (in parallel):

Channel Probe
Twilio curl -u "$SID:$TOKEN" https://api.twilio.com/2010-04-01/Accounts/$SID.json
Bland curl -H "authorization: $KEY" https://api.bland.ai/v1/me
ElevenLabs curl -H "xi-api-key: $KEY" "https://api.elevenlabs.io/v1/voices?page_size=1"
Groq curl -H "Authorization: Bearer $KEY" https://api.groq.com/openai/v1/models
Zoom curl -H "Authorization: Bearer $TOKEN" https://api.zoom.us/v2/users/me

Report each as [service] ✓ connected or [service] ✗ <error>. Rule 3 applies — never silently skip a channel. For each unset service, present AskUserQuestion with [Paste manually] / [Deep hunt — spawn agent] / [Skip].

Persist to preferences.json:

{
  "channels": {
    "voice": {
      "backend": "native+twilio+zoom+bland",
      "native": { "phone": true, "facetime": true, "zoom": true },
      "twilio": { "status": "configured", "from_number": "env:TWILIO_FROM_NUMBER" },
      "bland": { "status": "configured" },
      "zoom": { "status": "configured" }
    }
  },
  "default_channels": ["whatsapp", "email", "telegram", "slack", "voice"]
}

Routing from /ops:comms

Voice is wired into /ops:comms send-flow. The router resolves intent like:

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 ... "..."
have an AI call <name> and tell ... ops-voice bland-call <number> "..."

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


Mobile / SSH mode (Rule 7)

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

  • bin/ops-voice still works for API channels.
  • Native channels (phone, facetime, zoom start|join) require a local macOS session — the script returns a plain-text instruction to open the URL on the host instead of calling open directly. The script must source lib/opener.sh and use ops_open_url for URL handoff.

(v1 of bin/ops-voice calls /usr/bin/open directly — Rule 7 adapter is a v1.1 follow-up; tracked in CHANGELOG.)


Execution

  1. Resolve the sub-command from $ARGUMENTS (first word).
  2. For native handlers (phone/facetime/zoom start|join), shell out to bin/ops-voice — exit fast.
  3. For API channels (twilio/bland/zoom-schedule/tts/transcribe), resolve credentials in order, then curl.
  4. If a required key is missing, suggest /ops:ops-voice setup.
  5. For 1:1 outbound channels (twilio-call/sms, bland-call): stage one draft → AskUserQuestion → send → next.

Version History

  • 64bad13 Current 2026-08-12 09:02

Same Skill Collection

claude-ops/skills/boss/SKILL.md
claude-ops/skills/flow/SKILL.md
claude-ops/skills/ledger/SKILL.md
claude-ops/skills/ops-accounts/SKILL.md
claude-ops/skills/ops-ar/SKILL.md
claude-ops/skills/ops-aws-audit/SKILL.md
claude-ops/skills/ops-comms/SKILL.md
claude-ops/skills/ops-competitors/SKILL.md
claude-ops/skills/ops-credentials/SKILL.md
claude-ops/skills/ops-daemon/SKILL.md
claude-ops/skills/ops-dash/SKILL.md
claude-ops/skills/ops-deploy-fix/SKILL.md
claude-ops/skills/ops-deploy/SKILL.md
claude-ops/skills/ops-desktop/SKILL.md
claude-ops/skills/ops-doctor/SKILL.md
claude-ops/skills/ops-ecom/SKILL.md
claude-ops/skills/ops-feature-dev/SKILL.md
claude-ops/skills/ops-fires/SKILL.md
claude-ops/skills/ops-fleet/SKILL.md
claude-ops/skills/ops-go/SKILL.md
claude-ops/skills/ops-gtm/SKILL.md
claude-ops/skills/ops-home/SKILL.md
claude-ops/skills/ops-inbox/SKILL.md
claude-ops/skills/ops-integrate/SKILL.md
claude-ops/skills/ops-leadgen/SKILL.md
claude-ops/skills/ops-linear/SKILL.md
claude-ops/skills/ops-mac/SKILL.md
claude-ops/skills/ops-marketing/SKILL.md
claude-ops/skills/ops-mcp/SKILL.md
claude-ops/skills/ops-merge/SKILL.md
claude-ops/skills/ops-monitor/SKILL.md
claude-ops/skills/ops-next/SKILL.md
claude-ops/skills/ops-orchestrate/SKILL.md
claude-ops/skills/ops-package/SKILL.md
claude-ops/skills/ops-pocket/SKILL.md
claude-ops/skills/ops-projects/SKILL.md
claude-ops/skills/ops-recap/SKILL.md
claude-ops/skills/ops-release/SKILL.md
claude-ops/skills/ops-resume/SKILL.md
claude-ops/skills/ops-revenue/SKILL.md
claude-ops/skills/ops-rotate-setup/SKILL.md
claude-ops/skills/ops-rotate/SKILL.md
claude-ops/skills/ops-secret-sync/SKILL.md
claude-ops/skills/ops-settings/SKILL.md
claude-ops/skills/ops-ship/SKILL.md
claude-ops/skills/ops-speedup/SKILL.md
claude-ops/skills/ops-status/SKILL.md
claude-ops/skills/ops-triage/SKILL.md
claude-ops/skills/ops-unifi/SKILL.md
claude-ops/skills/ops-update/SKILL.md

Metadata

Files
0
Version
2dbf8cb
Hash
0500366d
Indexed
2026-08-12 09:02

ホーム - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-13 11:10
浙ICP备14020137号-1 $お客様$