Text-first bridge to use Codex from Even G2 glasses.
- Even web app captures mic PCM frames and forwards to backend.
- Backend streams PCM to ElevenLabs Scribe v2 Realtime STT.
- Final STT text (or manual text submission) is sent to
codex app-server. - Codex deltas/finals are rendered back on glasses.
This repo is configured for YOLO mode:
approvalPolicy: "never"sandboxPolicy: { type: "dangerFullAccess" }
server/index.js: websocket bridge entrypointserver/codexRpcClient.js: JSON-RPC stdio client forcodex app-serverserver/codexSessionBridge.js: thread/turn orchestration + notification mappingserver/elevenLabsSttClient.js: realtime STT client for ElevenLabsserver/protocol.js: frontend/backend message validationsrc/: Even web app frontend
- Node 20+
codexCLI installed and available in PATH (or setCODEX_BIN)- Even beta app + Even Hub dev mode
- ElevenLabs API key with realtime speech-to-text access
- Copy
.env.exampleto.env. - Set at minimum:
CLIENT_SHARED_TOKENVITE_CLIENT_SHARED_TOKEN(must match exactly)CODEX_CWDVITE_WS_BASE_URL
- Optional model toggle config:
CODEX_MAIN_MODEL(default:gpt-5.3-codex; legacy fallback:CODEX_MODEL)CODEX_FAST_MODEL(default:gpt-5.3-codex-spark)
- Optional session guidance config:
CODEX_SESSION_GUIDANCEsets thread-leveldeveloperInstructionsfor boththread/startandthread/resume.- If unset, a smart-glasses-safe default is applied.
- Set
CODEX_SESSION_GUIDANCE=(empty) to disable this guidance.
- Optional thread persistence (recommended):
ENABLE_DEFAULT_THREAD_RESUME=1ENABLE_PERSIST_THREAD_STATE=1THREAD_STATE_FILE=.runtime/thread-state.json
- STT configuration:
STT_PROVIDER=elevenlabsSTT_API_KEY=<your key>STT_MODEL_ID=scribe_v2_realtimeSTT_COMMIT_STRATEGY=vad- Optional VAD tuning (noise/false-positive control):
STT_VAD_THRESHOLDSTT_MIN_SPEECH_DURATION_MSSTT_MIN_SILENCE_DURATION_MSSTT_VAD_SILENCE_THRESHOLD_SECS
For text-only mode without mic STT, set STT_PROVIDER=none and use manual text submit.
Terminal A:
npm run dev:servernpm run dev:server is supervised:
- enforces Codex CLI version
>= 0.101.0 - prevents duplicate runs with
.bridge-run.lock - auto-relaunches only when server exits with code
42(used by/restart bridge|both)
Terminal B:
npm run dev:clientOr both:
npm run devRun server without supervision (rarely needed):
npm run dev:server:rawFor simulator/automation loops, you can boot directly into the running assistant:
- Query flag:
?autostart=1 - Env flag:
VITE_AUTO_START=1 - Optional query overrides (for ephemeral test sessions):
?ws=<wss-url>&token=<shared-token>&autostart=1
Example:
https://codex-even-app.example.com/?autostart=1
Use this only when you want remote/public access to your local dev server. Local development on the same machine/LAN does not require Cloudflare.
Caution
Exposing local services to the internet increases risk.
Use a strong CLIENT_SHARED_TOKEN, keep ALLOWED_ORIGINS tight, and prefer Cloudflare Access/WAF controls for public deployments.
Never commit secrets (.env, tunnel tokens, credentials files, runtime logs).
One-time setup for a named tunnel + DNS routes:
cloudflared tunnel create codex-even
cloudflared tunnel route dns codex-even codex-even-app.example.com
cloudflared tunnel route dns codex-even codex-even-api.example.comRun the dual-host tunnel (frontend + backend):
npm run tunnelRun tunnel and provision DNS in the same step:
npm run tunnel -- --provision-dnsRun a temporary quick tunnel instead:
npm run tunnel:quickSet matching env values:
VITE_WS_BASE_URL=wss://codex-even-api.example.com/wsALLOWED_ORIGINS=https://codex-even-app.example.com,http://localhost:5173,http://127.0.0.1:5173- Include your public app host in
ALLOWED_ORIGINS(or setVITE_ALLOWED_HOSTS) so Vite dev/preview host checks allow that domain.
By default the script runs tunnel codex-even.
The script resolves the tunnel UUID and uses that for route/run calls (avoids name-resolution fallback to default tunnel config).
Auth for npm run tunnel can come from any one of:
~/.cloudflared/<tunnel-uuid>.jsoncredentials fileTUNNEL_TOKENenvironment variable- runtime token generation via
cloudflared tunnel token <tunnel-name>(requires authenticated cloudflared CLI)
Client -> server:
session.start:{ appVersion, deviceInfo?, clientTs, resumeThreadId? }audio.chunk:{ seq, pcmB64, sampleRate:16000, format:"pcm16le", durationMs, byteLength }text.submit:{ text, clientTs? }session.stop:{ reason? }ping:{ clientTs }
Server -> client:
status:{ phase, detail?, sessionId? }transcript.delta:{ text, turnId, role, replace?, ts }transcript.final:{ text, turnId, role, ts }metrics: runtime counterserror:{ code, message, recoverable }pong:{ serverTs, clientTs? }
Accepted in text.submit as either typed slash or spoken "slash":
/helporslash help/statusorslash status/debugorslash debug/stoporslash stop/resetorslash reset/compactorslash compact/effortorslash effort/sparkorslash spark/fastorslash fast/restart codexorslash restart codex/restart bridgeorslash restart bridge/restart bothorslash restart both/threadorslash thread/thread neworslash thread new
Behavior:
help: lists supported commands.status: prints runtime status and key config toggles.debug: prints current turn/transport diagnostics.stop: requests interrupt of the active turn.reset: starts a fresh thread (same effect asthread new).compact: requests thread compaction.- During compaction lifecycle, bridge emits:
Compaction started.Compaction complete.
effort: without args shows current model/effort; with arg sets effort for current model.- Allowed values:
none,minimal,low,medium,high,xhigh - STT aliases like
ex high/extra highnormalize toxhigh
- Allowed values:
spark/fast: toggle between current model and configured fast model, restoring prior model+effort on toggle off.restart codex: restarts only Codex app-server in-process, attempts thread resume, then falls back to a fresh thread.restart bridge: exits server with code42; supervised launcher restarts process.restart both: same process restart path asrestart bridge.- Spoken STT variants
codecandcodecsare accepted ascodexfor/restart. thread: prints current thread id and active turn id.thread new: starts a fresh thread (intentionally bypasses resume).
Stop/start behavior:
ENABLE_DEFAULT_THREAD_RESUME=1enables resume-by-default onsession.start.ENABLE_DEFAULT_THREAD_RESUME=0forces freshthread/startonsession.start(client/server resume IDs ignored).- The web app caches the latest
sessionIdand sends it asresumeThreadIdon nextsession.start. - If
ENABLE_PERSIST_THREAD_STATE=1, backend also persists the latest thread id toTHREAD_STATE_FILE. - Backend attempts
thread/resumefirst (clientresumeThreadId, else persisted server thread id), then falls back tothread/startif resume is unavailable. CODEX_SESSION_GUIDANCEis applied as threaddeveloperInstructionson boththread/startandthread/resume.
text.submit(manual prompt box) remains a permanent dev/QA path.- Single click (
CLICK_EVENT) toggles focus mode:- Focus mode shows assistant output + compact status (
listening|muted+ connection state). - Normal mode keeps richer transcript/status context.
- Focus mode shows assistant output + compact status (
- Double click (
DOUBLE_CLICK_EVENT) toggles mic state:listening= mic on and STT ingest active.muted= mic off (audioControl(false)), STT ingest paused.
- Web app
Mute Mic/Unmute Micbutton mirrors the same mic toggle for manual-text-focused testing.- If
audioControl(false)is rejected by host firmware, bridge applies a local mute fallback (stops forwarding audio frames to STT).
- If
- Ring paging is SDK-native.
- App-side scroll math/windowing is disabled.
- Scroll events are only logged for diagnostics; no app-level offset/direction transform is applied.
- The SDK controls direction, paging, and scrollbar behavior.
- Transcript is rendered newest-first (latest content at top) to align with SDK update behavior during streaming.
- Transcript text is still capped (
maxChars) before render to keep updates stable.- Older content outside the cap is truncated from the head.
- Auto-mute safety policy:
- Triggers on backend disconnect/reconnect only.
- Auto-resume only occurs when mute reason is
autoand backend connectivity recovers. - Manual mute is never auto-resumed.
- Codex websocket transport is intentionally not used here; this bridge uses stdio app-server transport.
src/evenBridge.jsincludes SDK-safe startup fallback forcreateStartUpPageContainerone-time behavior.