Skip to content
Development
Skill

/debug-voice

Use when the user runs /debug-voice, says voice mode has flaws, asks to see or capture what happened in a Grok realtime voice session, or a voice integration has no debug logging yet. Proposes a plan, then installs a dev-only log pipeline (client logger → local NDJSON) in the

From plugin
cursor-plugins
7.6k89 skills13 agents
Install
$ npx -y skills add cursor/plugins --skill debug-voice --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/debug-voice

Context preview

The summary Claude sees to decide when to auto-load this skill.

Use when the user runs /debug-voice, says voice mode has flaws, asks to see or capture what happened in a Grok realtime voice session, or a voice integration has no debug logging yet. Proposes a plan, then installs a dev-only log pipeline (client logger → local NDJSON) in the

SKILL.md

debug-voice.SKILL.md
name: debug-voice
description: >-
  Use when the user runs /debug-voice, says voice mode has flaws, asks to see or
  capture what happened in a Grok realtime voice session, or a voice integration
  has no debug logging yet. Proposes a plan, then installs a dev-only log
  pipeline (client logger → local NDJSON) in the app's own language and
  conventions, then runs the fix loop: match the user's report to log
  signatures, fix one thing, re-test.

Debug Voice

Make a voice session readable after the fact, then fix from evidence. The agent cannot hear the app; the log is its ears, the user is its judge. No audio, no tokens, never in prod.

Works for any stack. The pipeline is a small contract (below); implement it in whatever the app already uses.

Workflow

1. **Map** the app (read only). 2. **Plan**: write the change list, show it, **stop**. Nothing is edited until the user aligns. 3. **Install** the agreed pieces in the app's language, framework, and conventions. 4. **Verify** the sink, hand the app to the user. 5. **Fix loop** from the log.

1. Map

Find, and note the paths:

  • Voice client: where realtime events are received and sent, mic capture, audio playback, token fetch.
  • Server: framework, how routes are declared, where shared server code lives, how env is read, what "production" means here.
  • Conventions: language(s), module system, formatter, where scripts or tasks live (`package.json`, `Makefile`, `pyproject`, `justfile`), `.gitignore`.
  • Client kind: browser, mobile, desktop, CLI. A non-browser client still POSTs the same JSON; a single-process app can skip HTTP and append to the file directly.
  • Where audio deltas are handled. They must be counted, never logged.

2. Plan, then stop

Fill this in with real paths and the app's language, post it, and wait for a yes or a trimmed list. Do not edit files before that.

## Debug voice: plan

Add
- <path>: client logger (batch, redact, flush) in <language>
- <path>: dev-only sink `POST /api/voice/log` → `.voice-logs/<sessionId>.ndjson`
- <path> (optional): summary command `<cmd>`; otherwise read the NDJSON with jq

Modify
- <voice client file>: hook points start, token, mic, env, ws.*, client/server events,
  audio.in (2 s windows), audio.out.first, audio.out, play.stop, stop
- <token route>: append `server.token { ok, status, ms, upstream }` (never the token)
- <UI file>: session id in the voice status line and in voice error messages
- .gitignore: `/.voice-logs`
- <scripts file>: a `voice:logs` task (only if the summary command is wanted)

Logged: event names and non-audio fields, timings, byte counts, mic RMS.
Never: tokens, API keys, raw audio, strings over 400 chars.
Off in production unless `VOICE_LOG=1`.

Reply "go", or strike lines you do not want.

3. Install: the contract

Match the app. Same language as the surrounding code, same route style, same formatter. Write the pieces from the contract below; do not introduce a second language or toolchain for logging.

**Session id**: 8 lowercase hex chars from a UUID. The sink accepts `^[a-z0-9]{4,64}$`; it becomes a file name.

**Entry** (one JSON object per line):

| Field | Client | Server | | --- | --- | --- | | `t` | ms since the logger started | absent; the reader aligns by `ts` | | `ts` | epoch ms | epoch ms | | `kind` | `start`, `server`, `client`, `error`, `audio.in`, … | `server.token`, … | | `src` | absent | `"server"` | | rest | the hook's fields, redacted | the hook's fields |

**Redaction, applied client side before buffering**: on audio event types (`response.output_audio.delta`, `response.audio.delta`, `input_audio_buffer.append`) replace `delta` / `audio` with `bytes` = decoded base64 length; strings over 400 chars cut to 400 + `…[N chars]`; objects deeper than 4 → `"[depth]"`; arrays over 50 items truncated.

**Client logger**: buffer entries; flush as `POST <sink> {"sessionId","entries":[…]}` every 1 s or at 200 entries; on stop flush with keepalive (or the platform's "survive navigation" equivalent); swallow every transport error, logging must never throw into the voice path. Also mirror entries to the console in dev.

**Sink**: `POST /api/voice/log`, JSON body. `404` unless dev or `VOICE_LOG=1`. `400` if `sessionId` fails the regex or `entries` is not an array. Append at most 500 entries per request, drop any line over 16,000 chars, to `.voice-logs/<sessionId>.ndjson`, creating the directory. Reply `204`.

Pseudocode for any server:

handle POST /api/voice/log:
  if production and VOICE_LOG != "1": return 404
  body = parse json or return 400
  if not regex(body.sessionId) or not list(body.entries): return 400
  mkdir .voice-logs; append join(json(e) for e in body.entries[:500] if len < 16000) to .voice-logs/{sessionId}.ndjson
  return 204

Pseudocode for the client logger:

logger(sessionId, sink):
  buffer = []; started = now()
  log(kind, data): buffer.push({ ...redact(data), t: now() - started, ts: epoch_ms(), kind }); schedule flush (1 s timer, or immediately at 200 entries)
  server(event, extra): log("server", { ...redact_event(event), ...extra })   # never per audio delta
  client(event):        log("client", redact_event(event))                    # never per audio chunk
  error(where, err, extra): log("error", { where, name, message, ...extra })
  flush(final=false): POST sink {"sessionId","entries": buffer}; buffer = []; ignore all errors; keepalive when final
  close(): flush(final=true)

Hook points

`kind` and fields; the shape is the same in every language.

| When | `kind` and fields | | --- | --- | | Session start | `start { url, target_rate }` | | Token fetched / failed | `token.ok { ms }` / `error { where: "token", name, message, ms }` | | Mic granted / denied | `mic.ok { ms, label, settings }` / `error { where: "mic", … }` | | Audio graph ready | `env { ua, mic_rate, mic_state, play_rate, play_state, capture_frames, target_rate }` | | Socket | `ws.connecting`, `ws.open { ms }`, `ws.

Read more
Ships withcursor-plugins

Official Cursor plugins for popular developer tools, frameworks, and SaaS products. Each plugin is a standalone directory at the repository root with its own .cursor-plugin/plugin.json manifest.

Get the whole plugin

Other skills on cursor-plugins.