{
  "markdown": "# cc-gc-stts\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-blue.svg)](https://nodejs.org/)\n\nTalk to **Claude Code**, **Gemini CLI** or **Antigravity CLI aka agy** and hear them talk back. This project adds seamless Speech-to-Text (STT) and Text-to-Speech (TTS) capabilities via a Model Context Protocol (MCP) server.\n\n📰 **Read the story:** [True voice mode for Claude Code](https://www.linkedin.com/pulse/true-voice-mode-claude-code-sandip-chitale-rz5uc/)\n\n![Talk](screenshots/stts-prompt.png)\n![Listen](screenshots/stts-response.png)\n\n## ✨ Features\n\n- 🎙️ **Speech-to-Text (STT):** Dictate your prompts instead of typing.\n- 🔊 **Text-to-Speech (TTS):** Hear the model's responses read aloud.\n- 🔄 **Conversational Loop:** Use the `/stts` command for a continuous voice-driven session.\n- 🚀 **Persistent Daemon:** Fast startup using a reusable Chrome window.\n- 🛠️ **Cross-Platform:** Works with both Claude Code and Gemini CLI.\n- 🕘 **History:** Recall past prompts and responses from a dropdown above each panel, or with `Alt+↑` / `Alt+↓`.\n\n## 🏗️ How It Works\n\n`stts` uses a background daemon to manage a persistent Chrome/Chromium window:\n\n1.  **MCP Server:** Exposes `stt` and `tts` tools to the AI model. Talks to the daemon over plain HTTP — one short request per call, no polling, no per-call subprocess spawn.\n2.  **Daemon:** A local HTTP + WebSocket server on port `15986` that controls a Chrome instance in \"app mode\". Stores its profile under `$TMPDIR/cc-gc-stts-user-data-dir`.\n3.  **Browser UI ↔ Daemon:** A single persistent **WebSocket** at `/ws` carries every per-turn message. The daemon pushes a `request` frame the moment the model calls `stt` or `tts`; the page pushes back `complete` / `cancel` / `close` when the user is done.\n4.  **Browser UI:** Uses the native **Web Speech API** for recognition and synthesis. Free at the wallet — note that on Linux Chrome routes recognition audio through Google's servers, so this is not a fully offline pipeline.\n5.  **Smart Auto-Advance:** In the `/stts` voice loop, if you simply listen through the response without touching anything, the loop advances automatically the moment speech ends. Only if you press **Stop** or **Play** (or say \"stop it\" / \"play it\") does the page wait for a manual **Got it!** so you stay in control of replays.\n6.  **Automatic Lifecycle:** The daemon starts on demand and shuts down when the Chrome window is closed.\n7.  **Port-collision aware:** If port `15986` is held by a non-stts process, the launcher fails fast with a clear error instead of timing out.\n\n## 🚀 Quick Start\n\n### 1. Build the project\n\n```bash\nnpm install\nnpm run build\n```\n\n### 2. Install\n\n#### **Claude Code**\n```bash\nclaude plugins marketplace add https://github.com/sandipchitale/cc-gc-stts.git\nclaude plugin install stts\n```\n\n#### **Gemini CLI**\n```bash\ngemini extensions install --consent https://github.com/sandipchitale/cc-gc-stts.git\n```\n\n#### **Antigravity CLI**\n```bash\nagy plugin install --consent https://github.com/sandipchitale/cc-gc-stts.git\n```\n\n## ⌨️ Usage\n\n### Conversational Loop\nRun the voice-driven loop where you speak, the model processes, and the response is read back to you:\n- **Claude Code:** `/stts`\n- **Gemini CLI:** `/stts`\n- **Antigravity CLI:** `/stts`\n\n### Direct Tool Usage\nYou can also ask the model to \"use the stt tool\" or \"speak this using tts\" directly in your prompts.\n\n## 🗣️ Voice Commands & Shortcuts\n\nBoth STT and TTS modes support voice-activated commands for a hands-free experience.\n\n### Popular Commands\n| Command | Action |\n| :--- | :--- |\n| `send prompt` | Submits your dictated text |\n| `cancel prompt` | Aborts the current recording |\n| `new paragraph` | Inserts a line break |\n| `got it` | (TTS mode) Acknowledges the response and continues — only required if you used **Stop** or **Play** during playback; otherwise the loop auto-advances |\n| `stop it` | (TTS mode) Stops the current playback (after this, **Got it!** is required to advance) |\n| `play it` | (TTS mode) Replays the response (after this, **Got it!** is required to advance) |\n\n> **Note:** Many more punctuation and formatting commands are supported (e.g., `insert comma`, `select all`, `undo it`). Toggle the side panel to see the full list.\n\n**Keyboard Shortcuts:**\n- `Ctrl+R`: Toggle recording/playback side panel.\n- `Enter`: Send prompt (Talk side).\n- `Escape`: Stop recording or close the commands panel.\n- `Alt+↑` / `Alt+↓`: Cycle through prompt or response history when the textarea is focused.\n\n![Voice command side panel](screenshots/stts-voice-commands.png)\n\n## 🕘 Prompt & Response History\n\nEach panel has a **History** bar above its textarea:\n\n- **Talk** stores every submitted prompt; **Listen** stores every response received from the model.\n- Pick an entry from the dropdown to load it into the textarea — fully editable. Hit **Enter** / **Send** to resubmit a prompt, or **Play** to replay a response.\n- `Alt+↑` walks back through history; `Alt+↓` walks forward (your in-progress draft is preserved and restored at the bottom of the stack).\n- History persists across sessions in `localStorage`, capped at 50 entries per side. Consecutive duplicates are not stored.\n- Use the **Clear** button to wipe one side's history.\n\n## 🛠️ Development & Manual Install\n\n### Install from a local source\n\n**Claude Code:**\n```bash\nclaude plugins marketplace add \"$PWD\"\nclaude plugin install stts\n```\n\n**Gemini CLI:**\n```bash\ngemini extensions install --consent \"$PWD\"\n```\n\n**Antigravity CLI:**\n```bash\nagy plugin install \"$PWD\"\n```\n\n### Daemon Control\nThe daemon usually runs automatically, but you can manually stop it by closing the Chrome window or:\n```bash\ncurl -X POST http://127.0.0.1:15986/api/shutdown\n```\n\n### Project Layout\n- `src/stts-mcp-server.ts` — MCP server exposing the `stt` and `tts` tools. Calls the daemon HTTP API directly.\n- `src/stts-daemon.ts` — local HTTP + WebSocket server on port `15986` that owns the Chrome window.\n- `src/daemon-client.ts` — shared HTTP client used by the MCP server and the CLI.\n- `src/stts.ts` — standalone CLI (`stts stt` / `stts tts`) for manual use and diagnostics.\n- `src/stts_ui.html` — the Web Speech API UI rendered inside the Chrome window. Connects to the daemon over WebSocket at `/ws`.\n\n### Daemon endpoints\n\n| Path | Method | Used by | Purpose |\n| :--- | :--- | :--- | :--- |\n| `/` | GET | Chrome | Serves the UI HTML |\n| `/api/ping` | GET | daemon-client | Health check (`ok` body confirms it's our daemon, not a foreign process) |\n| `/request` | POST | daemon-client | MCP/CLI submits an `stt` or `tts` request; response body carries the result |\n| `/api/shutdown` | POST | UI / CLI | Cleanly stops the daemon and Chrome |\n| `/ws` | WebSocket | Browser UI | Single persistent channel — daemon pushes `request` frames; browser pushes `ready` / `complete` / `cancel` / `close` |\n\n## 📋 Requirements\n\n- **Node.js:** v18 or higher.\n- **Chrome/Chromium:** Must be installed and discoverable.\n- **Microphone:** Required for STT functionality.\n\n## 📄 License\n\nMIT — [Sandip Chitale](https://github.com/sandipchitale)\n\n---\n\n## Appendix A — One-shot prompt\n\nThe following prompt is self-contained: handed to a capable coding agent (Claude Code, Gemini CLI, etc.) in an empty repository, it should produce an implementation equivalent to the one in this project.\n\n> **Build a voice-loop plugin for Claude Code and Gemini CLI called `stts`.** It exposes two MCP tools — `stt` (capture a spoken prompt and return the transcript) and `tts` (read a string aloud) — plus a slash command `/stts` that loops `stt → answer → tts` until the user is silent. Target Node.js 18+, TypeScript, esbuild for bundling.\n>\n> **Architecture.** Three processes:\n>\n> 1. **MCP server (stdio)** — registers `stt` and `tts` with the official `@modelcontextprotocol/sdk`. Each tool call POSTs JSON to a local daemon and returns the daemon's response.\n> 2. **Daemon** — a single Node process listening on a fixed loopback port (use `15986`). It serves both an HTTP API and a WebSocket endpoint at `/ws` from the same `http.Server`, and it owns one persistent Chrome/Chromium window launched via `chrome-launcher` in `--app=` mode pointed at `http://127.0.0.1:15986/`. The daemon's HTTP routes are: `GET /` (the UI HTML), `GET /api/ping` (health probe — body `ok` identifies \"our\" daemon vs. a foreign process holding the port), `POST /request` (the MCP/CLI submission, body is `{ mode: 'stt' | 'tts', ... }`, response body is the result), `POST /api/shutdown`. Long-poll endpoints are explicitly **not** used. The daemon keeps at most one in-flight `Pending` request; a second `/request` while another is open returns `409`.\n> 3. **Browser page** — a single HTML file the daemon serves. On load it opens a WebSocket to `/ws` and sends `{ \"type\": \"ready\" }`. The daemon pushes `{ \"type\": \"request\", \"config\": {...} }` frames; the page replies with `{ \"type\": \"complete\", \"text\": \"...\" }`, `{ \"type\": \"cancel\" }`, or `{ \"type\": \"close\" }`. The browser auto-reconnects on socket close. The page must reset to an idle UI on connect and re-activate when a `request` frame arrives.\n>\n> **Daemon-client.** Provide a shared module used by both the MCP server and a small CLI. It must: (a) ping the daemon; (b) if absent, spawn it detached with `unref()`; (c) if the port is held by something foreign, fail with a clear error; (d) POST `/request` and return the parsed `text` field.\n>\n> **Browser UI.** A two-panel page — *Talk* (STT) and *Listen* (TTS) — using only the browser's Web Speech API. Behaviors:\n>\n> - STT panel uses `webkitSpeechRecognition` with `continuous = true`, `interimResults = true`. Buttons: Send, Cancel, Dictate (toggle), Commands (panel), End conversation. Voice commands inserted into recognized text trigger UI actions: `send prompt`, `cancel prompt`, plus punctuation/formatting helpers (`new line`, `new paragraph`, `insert comma`, `select all`, `undo it`, etc.).\n> - TTS panel uses `speechSynthesis`. Buttons: Play, Stop, Got it!, Refresh. Voice commands while listening: `play it`, `stop it`, `got it`. On a fresh request set `userInteracted = false`. Stop and Play set `userInteracted = true`.\n> - **Smart auto-advance:** when `currentUtterance.onend` fires in oneshot mode (`config.oneshot === true`) **and** `userInteracted === false`, the page treats it like Got it! and immediately sends `{ type: 'close' }`. Otherwise it shows \"Finished. Play again or click Got it! to continue.\" and waits for an explicit gesture.\n> - On first load, `speechSynthesis.getVoices()` may return empty — nudge it with a zero-volume dummy `SpeechSynthesisUtterance` and listen for `voiceschanged`. When activating TTS, retry `synth.speak(...)` up to ~20×100 ms while voices are still loading, then fall back to a 5-second polling window before giving up.\n> - Maintain per-side history (prompts, responses) in `localStorage`, capped at 50 entries each, with `Alt+↑` / `Alt+↓` cycling and a Clear button. Consecutive duplicates are not stored.\n>\n> **`/stts` slash command.** A markdown command file whose body instructs the model: \"Call the `stt` tool. If the response is empty, output `Done.` and stop. Otherwise treat the response as a prompt, answer it, pass the answer to the `tts` tool. Repeat. While the loop runs, do not output anything else.\" The MCP `tts` tool sends `oneshot: true` so the page applies smart auto-advance.\n>\n> **Plugin packaging.** Provide `.claude-plugin/plugin.json` registering `stts-mcp` as an stdio MCP server pointing at the bundled daemon entrypoint, plus a Gemini extension manifest mirroring it. `package.json` declares dependencies `@modelcontextprotocol/sdk`, `chrome-launcher`, `commander`, `ws`. Build with esbuild: `bundle: true`, `platform: 'node'`, `format: 'esm'`, `target: 'node18'`, output to `dist/` as `.mjs`, copy `stts_ui.html` alongside.\n>\n> **Cross-cutting requirements.** Persist Chrome under `${tmpdir}/cc-gc-stts-user-data-dir` so cookies/voices/microphone permissions survive restarts. Disable the daemon's HTTP timeouts (`requestTimeout`, `headersTimeout`, `timeout`, `keepAliveTimeout` all `0`) so a parked `/request` cannot be killed by Node. On `EADDRINUSE` exit cleanly. On Chrome process exit, null out the WebSocket reference and resolve any pending request with `''`. Write everything in TypeScript with strict typing for the request/config shapes.\n",
  "bytes": 12454,
  "sha": "d04fbccb39e6593f3ef574519accaca1d6bff781770c3cb175ce61a084558f50",
  "repo_slug": "sandipchitale/cc-gc-stts",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_sandipchitale_cc_gc_stts_bb3cf780/readme"
}