SLOPSHOPPER

elevenlabs-call

📞 Call Claude Code: puts the chat you are in on a live ElevenLabs voice call (local mic and speakers, animated orb); hang up any time

newbandcommandtoastprocessnetwork
v0.4.0no licenseupdated 2026-10-20bhargava-gumpula/claude-mods/elevenlabs-call
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · elevenlabs-call
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ elevenlabs-call │ ⏺ Read(src/auth.ts) │ Set your ElevenLabs agent ID: /config → │ ⎿ Read 6 lines │ elevenlabs-call │ ⏺ Update(src/auth.ts) ╰────────────────────────────────────────────╯ ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /call ⎿ elevenlabs-call: Set your ElevenLabs agent ID first: /config → elevenlabs-call. [ 📞 Call Claude Code ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
[ 📞 Call Claude Code ]
README

claude-mods

Mods for Claude Code (terminal and the desktop Code tab).

ModWhat it does
usage-bandA band above the prompt: 5h and 7d limits as bars with time to reset, session cost with a mood emoji, total session tokens, and a pacing hint
chat-rosterA floating org-chart window for the sidebar group of the chat you're in: the orchestrator on top, the group's chats below, running subagents under their chat. Filled = working, hover a dot for its title, a flash runs along the lines when messages pass. Shown only while Claude is in front; opens when a chat starts
cube/cube, /cube 2, /cube 4: random-state scrambles (the WCA method, via cubing.js)
handoff/handoff [focus]: runs the project's checks, writes the dev log entry and HANDOFF.md update, commits (never pushes), and summarizes
streakYour streak of days in a row using Claude Code in the status line (streak 🔥 3 days); /streak adds your longest streak and total days
elevenlabs-call📞 Call Claude Code above the prompt puts the chat you are in on a live ElevenLabs voice call through your mic and speakers (no browser), with an animated orb. What you say is typed into the chat; the agent handles small talk and reads back a short summary of each reply. Hang up from the band or say bye. Needs the claude-voice bridge
prompt-cleanupFixes typos in what you type before Claude reads it (Haiku; skips commands, code and long pastes)
 5h ████░░░ 52% (1h 30m)     7d ██░░░░░ 23% (3d 5h)     $1.23   💩     6.1M tokens     👌 on pace

Cost emoji: 💩 $0–$5 · 😒 >$5 · 🥱 >$10 · 😮‍💨 >$25 · 🫪 >$50 · 😛 >$100 · 🤑 >$500 · 🙀 >$1000. Pacing: 🚀 spend freely (under an hour to reset, 70% or less used) · 🐢 slow down (15+ points ahead of the clock) · 👌 on pace · 🛑 nearly out (90%+).

Needs: python3 (usage-band tokens, chat-roster, streak), Xcode command line tools for swiftc (chat-roster builds its window on first use, macOS only), node and npm (cube installs cubing.js on first use).

Install

In a Claude Code terminal session, one line per mod you want:

/plugin install usage-band --marketplace bhargava-gumpula/claude-mods
/plugin install chat-roster --marketplace bhargava-gumpula/claude-mods
/plugin install cube --marketplace bhargava-gumpula/claude-mods
/plugin install handoff --marketplace bhargava-gumpula/claude-mods
/plugin install prompt-cleanup --marketplace bhargava-gumpula/claude-mods
/plugin install streak --marketplace bhargava-gumpula/claude-mods
/plugin install elevenlabs-call --marketplace bhargava-gumpula/claude-mods

Answer y to add the marketplace, then pick the user scope.

Source 2 files
hooks/register.tsx 200 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface as Engine, Register } from 'claude-code'
3
4import type { CallState, Task } from '../types'
5
6const BRIDGE = 'http://127.0.0.1:8787'
7const DIR = '"$HOME/Projects/claude-voice"'
8// run.sh keeps the bridge + tunnel alive after this session ends
9const START_BRIDGE = ['/bin/sh', '-c', `cd ${DIR} && nohup ./run.sh > bridge.log 2>&1 &`]
10const READ_TOKEN = ['/bin/sh', '-c', `cat ${DIR}/.token`]
11const HANG_UP = ['/bin/sh', '-c', `kill $(cat ${DIR}/.call.pid) 2>/dev/null; rm -f ${DIR}/.call.pid`]
12
13const call = atom({ plugin: 'elevenlabs-call', key: 'call' } as const, null)
14
15let token = ''
16let isOnCall = false // the session.start ticker polls the bridge only while true
17let isPolling = false
18let hasTicker = false
19let hasConnected = false // the call reported in once; 'ended' after that means it hung up
20// ponytail: replies are matched to voice tasks in order; a prompt you type mid-call shifts them
21const inflight: string[] = []
22
23function post($: Engine, path: string, body: object) {
24  return $.http.fetch(BRIDGE + path, {
25    method: 'POST',
26    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
27    body: JSON.stringify(body),
28  })
29}
30
31// The bridge answers 401 without its token: that means it is up.
32async function isBridgeUp($: Engine) {
33  try {
34    return (await $.http.fetch(BRIDGE + '/status', { method: 'POST' })).status === 401
35  } catch {
36    return false
37  }
38}
39
40async function ensureBridge($: Engine) {
41  if (!(await isBridgeUp($))) {
42    await $.process.run(START_BRIDGE)
43    await $.clock.sleep(2500)
44  }
45  token = (await $.process.run(READ_TOKEN)).stdout.trim()
46}
47
48// A voice task becomes a prompt in this chat, as if typed; the orb follows the call.
49async function pollOnce($: Engine) {
50  if (isPolling) return
51  isPolling = true
52  try {
53    const res = await post($, '/poll', {})
54    if (!res.ok) return
55    const { task, orb } = JSON.parse(res.text) as { task: Task | null; orb: { mode: string } }
56    if (task) {
57      inflight.push(task.id)
58      void $.prompt.submit({ text: `🎙️ ${task.text}`, asUser: true })
59    }
60    if (orb.mode !== 'ended') hasConnected = true
61    else if (hasConnected) return void (await hangUp($)) // you said bye, or the call dropped
62    const mode = orb.mode === 'ended' ? 'ended' : inflight.length > 0 && orb.mode === 'listening' ? 'working' : orb.mode
63    const now = await read($, call)
64    if (now && now.mode !== mode) await update($, call, () => ({ mode }))
65  } catch {
66    // bridge restarting: try again next tick
67  } finally {
68    isPolling = false
69  }
70}
71
72async function hangUp($: Engine) {
73  isOnCall = false
74  hasConnected = false
75  inflight.length = 0
76  await $.process.run(HANG_UP)
77  await update($, call, () => null)
78}
79
80// Call: this chat goes on the call, through this Mac's mic and speakers.
81async function goOnCall($: Engine, agentId: string) {
82  if (!agentId) {
83    $.ui.toast('Set your ElevenLabs agent ID: /config → elevenlabs-call')
84    return
85  }
86  await ensureBridge($)
87  await $.process.run(['/bin/sh', '-c', `cd ${DIR} && nohup .venv/bin/python call.py '${agentId.replace(/[^\w-]/g, '')}' > call.log 2>&1 &`])
88  isOnCall = true
89  if (!hasTicker) {
90    hasTicker = true
91    $.clock.every(400, () => {
92      if (isOnCall) void pollOnce($)
93    })
94  }
95  await update($, call, () => ({ mode: 'listening' }))
96}
97
98// An ElevenLabs-style orb: soft blue, cyan and violet light turning inside a sphere.
99function orb(mode: string) {
100  const [spin, pulse, grow] =
101    mode === 'speaking' ? [2.4, 0.7, 1.09] : mode === 'working' ? [4, 1.4, 1.04] : mode === 'ended' ? [0, 0, 1] : [9, 3.5, 1.03]
102  const turn = (dir: number) =>
103    spin ? `<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="${360 * dir} 60 60" dur="${spin}s" repeatCount="indefinite"/>` : ''
104  const breathe = pulse
105    ? `<animateTransform attributeName="transform" type="scale" values="1;${grow};1" dur="${pulse}s" repeatCount="indefinite"/>`
106    : ''
107  const ring = mode === 'working'
108    ? `<circle cx="60" cy="60" r="56" fill="none" stroke="#7CC4FA" stroke-width="1.5" stroke-dasharray="6 10" opacity=".7">${turn(1)}</circle>`
109    : ''
110  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
111<defs>
112<radialGradient id="base" cx="35%" cy="30%" r="80%"><stop offset="0" stop-color="#EAF6FF"/><stop offset=".45" stop-color="#8CCBFA"/><stop offset="1" stop-color="#3D5AFE"/></radialGradient>
113<filter id="soft" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="9"/></filter>
114<clipPath id="ball"><circle cx="60" cy="60" r="50"/></clipPath>
115</defs>
116${ring}
117<g transform="translate(60 60)"><g>${breathe}<g transform="translate(-60 -60)">
118<g clip-path="url(#ball)">
119<circle cx="60" cy="60" r="50" fill="url(#base)"${mode === 'ended' ? ' opacity=".35"' : ''}/>
120<g filter="url(#soft)">
121<ellipse cx="42" cy="72" rx="36" ry="22" fill="#A78BFA" opacity=".85">${turn(1)}</ellipse>
122<ellipse cx="78" cy="44" rx="32" ry="20" fill="#22D3EE" opacity=".8">${turn(-1)}</ellipse>
123<ellipse cx="62" cy="90" rx="30" ry="14" fill="#F0ABFC" opacity=".55">${turn(1)}</ellipse>
124</g>
125<ellipse cx="44" cy="36" rx="20" ry="12" fill="#FFFFFF" opacity=".35"/>
126</g>
127<circle cx="60" cy="60" r="50" fill="none" stroke="#FFFFFF" stroke-opacity=".35"/>
128</g></g></g>
129</svg>`
130}
131
132const LABEL: Record<string, string> = {
133  listening: 'Listening',
134  speaking: 'Speaking',
135  working: 'Working on it',
136  ended: 'Call ended',
137}
138
139export const register: Register = (on, options) => {
140  const agentId = String(options.agentId ?? '').trim()
141
142  on('session.start', async ($, e, next) => {
143    await $.command.register({ name: 'call', description: 'Put this chat on a voice call (ElevenLabs); /call again hangs up' })
144    return next(e)
145  })
146
147  on('command.run', { command: 'call' }, async $ => {
148    if (await read($, call)) {
149      await hangUp($)
150      return { text: 'Call ended.' }
151    }
152    await goOnCall($, agentId)
153    return { text: agentId ? 'On the call. Talk; this chat does the work.' : 'Set your ElevenLabs agent ID first: /config → elevenlabs-call.' }
154  })
155
156  // The chat's answer to a voice task goes back to be spoken.
157  on('turn.complete', async ($, e, next) => {
158    const id = e.agentId === undefined ? inflight.shift() : undefined
159    if (id) {
160      try {
161        await post($, '/done', { id, text: e.answer })
162      } catch {
163        // bridge gone: the call has ended
164      }
165    }
166    return next(e)
167  })
168
169  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
170    if (e.props.hasSurvey) return next(e)
171    const state: CallState | null = await read($, call)
172    if (!state) {
173      const { Box, Button } = $.ui.resolve(e)
174      return (
175        <Box>
176          <Button key="call" label="📞 Call Claude Code" onPress={() => goOnCall($, agentId)} />
177        </Box>
178      )
179    }
180    const label = `${LABEL[state.mode] ?? 'On call'} `
181    if (e.surface === 'desktop') {
182      const { Box, Button, Svg, Text } = $.ui.resolve(e)
183      return (
184        <Box flexDirection="row" alignItems="center" gap={2}>
185          <Svg source={orb(state.mode)} alt={`Voice call: ${label}`} width={64} height={64} />
186          <Text>{label}</Text>
187          <Button key="hangup" label="Hang up" onPress={() => hangUp($)} />
188        </Box>
189      )
190    }
191    const { Box, Button, Text } = $.ui.resolve(e)
192    return (
193      <Box>
194        <Text color={state.mode === 'speaking' ? 'magenta' : 'cyan'}>● {label}</Text>
195        <Button key="hangup" label="Hang up" onPress={() => hangUp($)} />
196      </Box>
197    )
198  })
199}
200
types/index.d.ts 9 lines
1export type Task = { id: string; text: string }
2export type CallState = { mode: string }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'elevenlabs-call': { call: CallState | null }
7  }
8}
9