Skip to content

Latest commit

 

History

History
112 lines (90 loc) · 15.6 KB

CODEMAP.md

File metadata and controls

112 lines (90 loc) · 15.6 KB

CODEMAP

Living map of evenTermidex: every component, what it does, and what depends on what. Keeping this current is mandatory — see CLAUDE.md → "Codemap & audit rules". Update it in the same change that alters structure.

Last updated: 2026-07-29 — Phase 4 + HW feedback (scroll fix = native OS lists): menu/picker screens now ship as frame.list {items,selected} and render as a native ListContainerProperty on the glasses (the OS moves a selection border — no page-scroll animation, which text containers always play and nothing disables); the OS reports the landed index back as gesture.item, which the daemon adopts as highlight truth; menu/picker highlights start at 0 on entry. Both fields additive — old clients still get the text rendering. menu now 8 entries (comm screen, dictate, quick actions, mode, sessions, Esc, output, terminal — 8 is the HARD CAP: title+items must fit TAIL_LINES=9). One generic picker overlay (state on controller, _render_picker sliding window, priority dictation > picker > prompt-fallback > selected) serves four kinds: session (fresh list_sessions + + new session auto-named + - kill session entry when others exist; switching mutates the shared driver.session), kill (sessions minus active; tap kills, stays open for bulk cleanup), mode (picker + verify: mode_steps ring distance + _set_mode step-and-recapture BTab loop; blind cycle retired), action (hardcoded QUICK_ACTIONS, prompt-guarded send_text+Enter). serve auto-creates the claude session at boot (zero onboarding). Done-news DEBOUNCED (NEWS_DEBOUNCE 1.0s — spinner flickers mid-turn; raw falling edge flashed false "done"). ws contract untouched. 167 tests green

Runtime topology

[G2 glasses] --BLE--> [glasses-client] --ws--> [daemon] --tmux--> [claude]
                       (owns BLE)      (contract) (host brain)  (capture/send-keys)
[phone] Tailscale carries the ws link (tailnet IP); Termux/SSH is interactive-shell only

Dependency direction (who knows about whom):

  • glasses-client → depends on → the ws contract only (never on daemon internals).
  • daemon → depends on → tmux/claude + the ws contract.
  • phone → depends on → nothing (pure transport; ssh tunnel).
  • docs → describes all; depended on by humans.

Components

daemon/ — Python 3.12 package eventermidex (status: Phase 4 complete)

File Responsibility Depends on Used by
src/eventermidex/__init__.py Package marker + __version__ — __main__
src/eventermidex/__main__.py CLI: doctor (default; env self-check incl. optional voice extra), launch (start claude in named tmux session), serve (run ws daemon; --host/--port/--session/--interval/--stt-model; transcriber loaded eagerly at boot; auto-creates the session if missing — zero onboarding, Phase 4); default_host() = tailscale ip -4 → fail-closed fallback 127.0.0.1+warning tmux_driver, server, stt, argparse console script eventermidex
src/eventermidex/protocol.py v1 ws contract: hello() (carries the only v), frame(lines, list_=None) (additive list {items,selected} = native-list hint for menu/picker screens), mic(open), app_exit(), parse_event() validator (voice needs str chunk or error; gesture item must be int if present — the OS list's selected index), ProtocolError, GESTURES/EVENT_TYPES stdlib json server, controller, glasses-client (shape only)
src/eventermidex/server.py asyncio ws server, transport-only since Phase 3: HudState (global view index + last PaneState + dismissed prompt + wake event — one wearer, one broadcast frame); current_frame(driver, hud, ctl) = capture→parse_pane→effective_view→render + list_payload → (lines, payload) (TmuxError→3-line placeholder+None); poll() diffs (lines, payload), sleeps on wait_for(wake, interval); make_handler(driver, clients, hud, ctl) delegates gesture→ctl.on_gesture(name, item) (item = OS-list index), voice→ctl.on_voice, disconnect→ctl.on_disconnect; connect resets hud.view="menu" + menu_index=0 (fresh OS list border opens at 0); run(..., transcriber=None) builds the Controller with send_all=broadcast websockets, protocol, renderer, controller, stt, tmux_driver __main__ serve, tests
src/eventermidex/controller.py Events→actions brain: MENU (8 entries = HARD CAP so title+items fit TAIL_LINES: 3 screens via SCREEN_KEYS comm/output/terminal, 3 pickers via PICKER_KEYS session/mode/action, dictate, esc), ControllerState (menu index, dictation machine, PCM buffer, gen stale-guard, news + debounce fields running/last_active, picker state: kind/items/index/active), Controller executor (on_gesture(name, item=None) applies route_gesture actions — item = OS-list selected index, _sync_selection adopts it as highlight truth (scroll-with-item = sync only, tap executes the OS-selected item); menu/picker index resets to 0 on entry to match a fresh OS list border; fresh-capture prompt approve; note_pane(pane) debounced done-news — spinner must stay gone ≥ NEWS_DEBOUNCE 1.0s; dictation machine: mic open/close, asyncio.to_thread transcription, cancel-by-gen; on_voice chunk accumulation; on_disconnect resets dictation AND picker). Phase 4 picker (kinds session/kill/mode/action): _open_picker (session=fresh list_sessions+NEW_SESSION_LABEL+KILL_SESSION_LABEL when others exist, kill=sessions minus active, mode=MODE_RING, action=QUICK_ACTIONS; picker_active marks current), _picker_select → _switch_session (mutate shared driver.session; new = _next_session_name claude-N in daemon cwd, revert on TmuxError) / _kill_session (kill + stay open for bulk cleanup, fall back to session picker when empty) / _set_mode (one BTab per fresh capture, bounded MODE_ATTEMPTS, refuses w/ prompt open) / _run_action (prompt-guarded send_text+Enter, lands comm). User-authored policies: route_gesture() (menu=home wraps/executes; universal 2tap→menu; comm tap follows notifications; prompt tap=approve; dictation per-state; picker: scroll=move, tap=select, 2tap=close — branch BEFORE universal 2tap), finish_review() (send=strip+send_text+Enter; empty=discard), mode_steps() (modular 4-ring BTab distance; None on unknown/bypass = safe noop) protocol, renderer, stt, tmux_driver (via injected driver), os, time server, tests
src/eventermidex/stt.py faster-whisper wrapper (optional [voice] extra): Transcriber(model_size) eager model load, blocking transcribe(pcm)->str (int16 LE mono → float32), load_transcriber()→None+log-hint when extra missing; SAMPLE_RATE=16000/SAMPLE_WIDTH=2 are HW-spike assumptions faster-whisper (optional), numpy (transitive) controller, __main__
src/eventermidex/renderer.py Pure pane→HUD pipeline (no I/O): parse_pane()→PaneState{lines,mode,activity,message,prompt} (never raises; unknown TUI degrades to None/empty), PendingPrompt (numbered-menu detection, ❯-marker false-positive guard), ControlView (frozen controller snapshot incl. picker kind/items/index/active — defined here so renderer never imports controller), reflow() (textwrap at COLS=36), render(pane,view,ctl=None) for VIEWS=(menu,calm,message,tail) (menu=home w/ live mode in title; calm=comm screen: blank while executing, notifications ! input needed+[Tool(args)]+question > * done) + prompt pseudo-view, dictation overlay, and picker overlay (_render_picker: title + sliding 8-row window centered on highlight; >=highlight *=active); activity = live spinner with parenthesized (Ns status only (output invariant: ≤9 rows, ASCII, ≤36 cols). User-authored policies: effective_view(pane,selected,ctl=None) (priority dictation > picker > answered-prompt fallback > selected), extract_message() (bottom-up boundary scan) stdlib re, textwrap server, controller, tests
src/eventermidex/tmux_driver.py The only tmux toucher: has_session/new_session(command,width,height,cwd=)/list_sessions() ([] when no server)/kill_session(name)/capture(ansi=)/tail/send_text(-l literal)/send_key(named); TmuxError; DEFAULT_SESSION="claude"; session field is MUTABLE — Phase 4 session switching retargets by assignment subprocess server, controller, __main__, tests
tests/ pytest: conftest.py shared FakeDriver (capture/send_key/send_text + Phase 4 sessions list/list_sessions/new_session/has_session; session is per-instance) + PROMPT_LINES; protocol round-trip/rejects; tmux driver vs throwaway real session (skipif no tmux; incl. list_sessions + cwd pin); server via real make_handler+poll on ephemeral port; controller unit tests (menu exec, dictation machine, stale-gen, disconnect, picker open/select/switch/create-revert/kill-flow, news debounce, ModeDriver _set_mode convergence, mode_steps/_next_session_name tables) with fake transcribers pytest, websockets dev
pyproject.toml Build + deps + extras (voice=faster-whisper) + eventermidex script setuptools pip/venv

glasses-client/ — Even Hub SDK app (Vite + TS) (status: round-trip verified on G2 hardware)

Form decided: Even Hub WebView app (Kotlin fallback dropped — WebSocket confirmed to work on sideload). Even's runtime owns BLE. Talks to daemon only via the ws contract.

File Responsibility Depends on
src/main.ts Thin client, two page shapes (HW 2026-07-29: the OS scroll-animates any capturing TEXT container — capture-overlay trick did NOT help, reverted): text page (single capturing text container; comm/output/terminal/prompt/dictation) updated via textContainerUpgrade splice, rebuild as fallback; list page (frame.list present: title text container + native ListContainerProperty w/ isItemSelectBorderEn:1 — the OS moves the selection border, no page slide; geometry 48px title + 240px list is a HW-tune guess) rebuilt only when items/page-mode change, title-only via upgrade; listEvent gestures relayed with item=currentSelectItemIndex; startup create retried once, content never '' (first-open crash); handles mic/exit, relays PCM as base64 voice{chunk}; FOREGROUND_ENTER → force-rebuild repaint @evenrealities/even_hub_sdk
index.html WebView entry (#app mirrors HUD text for phone-screen debugging) —
app.json Even Hub manifest (package_id com.aplaino.eventermidex; permissions: [g2-microphone] — without it audioControl(true) fails; valid names: g2-microphone, phone-microphone, album, location, network, camera) evenhub-cli (pack)
vite.config.ts Dev server on 0.0.0.0:5173 (reachable over Tailscale) vite
  • ws target defaults to ws://100.115.25.7:8765, override with ?ws=.
  • Gesture map (spike): tap→tap, double-tap→double-tap+exit, scroll→scroll-up/scroll-down.

phone/ — Termux + Tailscale (status: transport proven)

Transport is Tailscale (WireGuard mesh): the phone reaches the host daemon at its tailnet IP (ws://100.115.25.7:8765), proven both directions 2026-07-28. Works off-LAN, no firewall/ port-forward. SSH also rides the tailnet. No app logic here — pure transport.

spike/ — Phase 0 hardware spike (status: host end working)

Throwaway round-trip test to decide client + relay path. Disposable.

  • host/echo_server.py — stand-in daemon; ws server on 0.0.0.0:8765, pushes text frames, prints events. Depends on: websockets.
  • host/probe_client.py — local stand-in for the glasses client (verify daemon without hardware); updated to the v1 flow (hello → frame → tap → change-triggered frames). Depends on: websockets.
  • echo_server.py still speaks the pre-v1 provisional shape; superseded by eventermidex serve.

docs/ — architecture.md, protocol.md

architecture.md: condensed plan, data path, phases, reuse list. protocol.md: v1 ws contract mirror (evolution rule pending user TODO).

External dependencies

Dep Kind Where Why Phase
websockets Python runtime daemon ws server/client 1
bleak, pillow Python optional ([ble]) daemon only if desktop/BLE-bridge reference path fallback
faster-whisper Python optional ([voice]) daemon local STT for dictation 3
pytest Python dev ([dev]) daemon tests 2+
setuptools Python build daemon build backend —
tmux system binary host run/read/inject the claude session 1
OpenSSH system binary host + phone secure transport / -L tunnel 0/1
Node/npm system glasses-client Even Hub SDK toolchain 0
@evenrealities/even_hub_sdk (0.0.10) npm runtime glasses-client HUD text/containers + tap/scroll events 0
@evenrealities/evenhub-simulator (^0.7.2) npm dev glasses-client desktop simulator for iteration 0
@evenrealities/evenhub-cli (^0.1.12) npm dev glasses-client evenhub qr (sideload), evenhub pack 0
vite + typescript npm dev glasses-client build/dev server 0

Cross-component interfaces (contracts)

  • WebSocket contract (daemon ↔ glasses-client): v1, defined in daemon/src/eventermidex/protocol.py, mirrored in docs/protocol.md. Out: hello {v:1} (once, on connect — only place the version lives), frame {lines, list?} (full replacement, on connect then on change only; list {items,selected} additive Phase 4 — native-list hint for menu/picker screens, older clients ignore it), mic {open} (Phase 3: toggle glasses mic), exit (Phase 3: client shuts down, no reconnect). In: gesture {name, item?} (4 names; item additive = OS-list selected index), voice {chunk:base64-PCM | error} (Phase 3), command reserved. Evolution rule: receivers silently ignore unknown types/fields — older clients keep working against the new daemon until re-sideload.
  • tmux interface (daemon ↔ claude): read via tmux capture-pane -p -e; inject via tmux send-keys.

Invariants (don't break these)

  1. glasses-client and daemon share no code — only the ws contract.
  2. daemon is the only component that touches tmux/claude.
  3. phone carries bytes; it holds no app logic.
  4. The ws contract is identical whichever glasses-client form Phase 0 picks.

Security posture (open hardening items — see AUDIT 2026-07-28)

  • Trust boundary today = the Tailscale tailnet. The ws server has no peer authentication; any device on the tailnet that reaches :8765 can send events, and with --tap-approves on, inject Enter into the Claude session. default_host() fails closed to loopback (never 0.0.0.0) so a Tailscale outage can't silently expose it.
  • OPEN — token auth on the ws contract (flagged CRITICAL by security review). Needs a handshake/token in the connect URL + a client rebuild/re-sideload. Defense-in-depth on top of the tailnet; schedule with Phase 5 (or sooner if the tailnet isn't fully trusted).
  • OPEN — Origin allowlist (serve(origins=[...]), likely [] for a non-browser client). Blocked on one hardware check: confirm whether the Even Hub WebView sends an Origin header before setting it, or it may lock out the real client.
  • Tap→Enter is now prompt-aware and always on (Phase 2 replaced the --tap-approves smoke flag): the daemon injects Enter only when a numbered permission menu with a ❯ default is detected on a fresh capture. Still unauthenticated on the tailnet — the token auth item above is what gates untrusted peers, not the tap heuristic.