Skip to content

Latest commit

 

History

History
111 lines (90 loc) · 22.7 KB

File metadata and controls

111 lines (90 loc) · 22.7 KB

OpenMicro — Codex Micro, replicated with a consumer gamepad

Open-source replica of Work Louder's Codex Micro using a consumer gamepad (Sony DualSense first; DS4/Xbox/generic input-only) as the physical AI-agent controller. Harness-agnostic core targeting Claude Code and Codex CLI from day one; any other agent CLI can be added by implementing one interface.

Foundation: vendored modules from /Users/stephenleo/Developer/vibesense (same author). TypeScript, Node >= 22, ESM. Deps: dualsense-ts, node-hid, node-pty, zod only. Tests: vitest. macOS-first.

Feature parity map (Codex Micro → gamepad)

Codex Micro openmicro
6 RGB agent keys (live thread status) Lightbar = focused session's state color; 5 player LEDs = occupied session slots (cap 5, documented gap vs 6). No rumble — Codex Micro has no haptics, strict parity
Command keys: accept / reject / push-to-talk / new chat Face buttons: south=accept, east=reject, north=push-to-talk, west=new chat
Joystick → 4 preset workflows Left-stick flick (up/down/left/right) → prompt templates (review PR / debug / refactor / write tests), remappable
Rotary dial → thinking depth Right-stick rotation gesture (a real dial replica): accumulate angular sweep while deflected past deadzone, one depth step per 90° — CW = up, CCW = down. Remappable (e.g. onto the left stick) via config; d-pad stays arrow-keys for TUI menus, L2/R2 unbound by default
Touch sensor DualSense touchpad click = cycle focused session
6 programmable layers Hold L1 + face/dpad-up/dpad-down = layer 0–5; lightbar flashes layer tint on switch
Remap in ChatGPT app ~/.openmicro/config.json, zod-validated
Bluetooth / USB-C DualSense USB + Bluetooth via dualsense-ts

Architecture

openmicro [claude|codex] [...args] wraps the agent CLI in a pty (vibesense pattern). First instance = host (binds port 48762, owns controller + session aggregation); later instances = clients (register, receive forwarded keystrokes over SSE). Agent lifecycle hooks POST to http://127.0.0.1:48762/om-hook/<event>.

IMPORTANT — coexistence with vibesense (verified in Phase 2): vibesense's installer purges any hook command containing the bare substring /hook/, so openmicro uses path /om-hook/ (which does not contain /hook/) on port 48762, and identifies its own entries by the full marker 127.0.0.1:48762/om-hook/. Phase 4's server must route /om-hook/<event>. Constants live once in src/ports.ts.

The harness contract (src/harness/types.ts) — the only place agent-specific knowledge lives

export type AgentKind = 'claude' | 'codex' | (string & {}) // widened so third-party kinds satisfy Harness without a cast
export type AgentState = 'executing' | 'waiting' | 'idle' | 'complete' | 'error'

export type Action =
  | { type: 'accept' }
  | { type: 'reject' }
  | { type: 'push_to_talk' }
  | { type: 'new_chat' }
  | { type: 'thinking_depth'; delta: 1 | -1 }
  | { type: 'workflow'; presetId: string } // core resolves presetId → text via config, then calls resolveAction({type:'prompt', text})
  | { type: 'prompt'; text: string }
  | { type: 'focus_session'; index: number } // handled by core, never reaches a Harness
  | { type: 'layer'; index: number } // handled by core, never reaches a Harness
  | { type: 'keys'; bytes: string } // raw pty passthrough (e.g. dpad arrows), user-remappable

export interface InstallResult {
  changed: boolean
  trustNotice: string | null
}

export interface Harness {
  readonly kind: AgentKind
  readonly command: string
  buildArgs(userArgs: string[]): string[]
  installHooks(): InstallResult
  /** Hook event name + raw payload → state, null if not state-relevant. Error/complete are best-effort heuristics per harness. */
  stateForHookEvent(event: string, payload: unknown): AgentState | null
  /** Action → pty bytes (+ new thinking level when applicable). null = harness has no equivalent (documented gap, never faked). */
  resolveAction(
    action: Action,
    ctx: { thinkingLevel: number },
  ): { bytes: string; thinkingLevel?: number } | null
}

export function harnessFor(kind: string): Harness

Public extension API: package.json gets an exports map exposing openmicro/harness (the Harness/Action/AgentState types + registerHarness(harness: Harness) and the built-in registry). AgentKind widens to string at the registry boundary so third-party harnesses (Gemini CLI, opencode, …) register without forking. README gets an "Adding a harness" section with a minimal example. Scope discipline for the initial commit: Codex Micro's current functionality, 100%, nothing else.

Core modules (controller, state, router, layers, feedback, server, dispatch, cli) import only these types — never 'claude'/'codex' literals outside src/harness/. Adding Gemini CLI later = one new file + one registry entry.

Module map (vendor source: /Users/stephenleo/Developer/vibesense/src/)

openmicro file Origin Changes
src/types.ts vendor add 'touchpad' to ButtonId
src/controller/hal.ts vendor verbatim
src/controller/hid-manager.ts vendor verbatim
src/controller/{ds4,xbox,generic,raw-hid}-driver.ts vendor verbatim input-only, no output support
src/controller/dualsense-driver.ts vendor + extend add output: ControllerOutput (lightbar/playerLeds/rumble via dualsense-ts) + touchpad click events
src/controller/output.ts new interface ControllerOutput { setLightbar(rgb): void; setPlayerLeds(bitmask): void }; drivers expose output? — optional-chained no-op on non-DualSense. No rumble (strict Codex Micro parity)
src/pty.ts vendor verbatim AgentPty + spawn-helper permission fix
src/logger.ts vendor log path ~/.openmicro/openmicro.log
src/hooks-install.ts vendor port 48762, base-URL marker (see coexistence note)
src/state.ts vendor + extend keep SessionTracker; drop PauseGate; add complete (transient: green decay N seconds after Stop) and error states; stateForHookEvent moves behind Harness
src/server.ts vendor + trim keep singleton bind, /om-hook/<event>, /register, /instance/<id> SSE, sendKeysToInstance; delete GameProvider/sidebar//events/static serving
src/client.ts vendor rename vibesense strings
src/keymap.ts vendor KeyRepeater only TERMINAL_KEYS replaced by layer config
src/router.ts new (pattern reused) LayerRouter: ControllerEvent + current layer → Action | null; keep vibesense's 750 ms guard-window-kills-held-buttons on every layer flip; stick gesture detectors live here: flick (threshold cross + return-to-center) and rotation (angle accumulation while past deadzone, emit one event per 90° sweep, direction-signed)
src/layers.ts new zod schema: 6 layers, each { name, color, bindings: Partial<Record<ControlId, Action>> } + workflows: Record<string, string>; ControlId = ButtonId | 'lstick_up' | 'lstick_down' | 'lstick_left' | 'lstick_right' | 'lstick_cw' | 'lstick_ccw' | 'rstick_up' | … | 'rstick_cw' | 'rstick_ccw'; load/save ~/.openmicro/config.json (atomic tmp+rename); DEFAULT_CONFIG = parity-map bindings above (lstick flicks = workflows, rstick_cw/ccw = thinking depth ±1)
src/feedback.ts new pure feedbackFor(snapshot, layer) → { lightbar, playerLeds }; state colors: executing=blue, waiting=amber, idle=dim white, complete=green (decay), error=red; debounced apply loop in cli
src/harness/{types,claude,codex,index}.ts new contract above; claude/codex resolveAction keybindings empirically verified against the real CLIs, never guessed; thinking depth: Claude = effort/model cycling, Codex = /model reasoning-effort — verify, and return null where unsupported
src/invocation.ts vendor + trim `openmicro [claude
src/dispatch.ts new dispatchAction(action, deps): Action → effect, split out of cli.ts so it's unit-testable without HID/pty/timer wiring; focus_session/layer are core-only, workflow resolves its preset text through config then hands the harness a plain prompt, everything else goes straight to harness.resolveAction (null = documented gap, silently skipped)
src/cli.ts new (skeleton borrowed) install hooks → bind singleton port (host/client branch) → spawn AgentPty; client: register + stream forwarded keystrokes; host: HID start → route → dispatch (harness actions → pty write; layer/focus → core state; snapshot changes → feedback)
test/* vendor tests for vendored modules; new tests for router/layers/feedback/harness/dispatch keep vibesense's proven edge cases (guard window, hook-merge idempotency)
toolchain configs vendor verbatim from vibesense package name openmicro, bin openmicro

Phases

  1. Scaffold + vendor verbatim modules; npm install, typecheck green. ✅ when done
  2. Controller output + touchpad (dualsense-driver ext, output.ts, feedback.ts) + hardware spike script scripts/spike-output.ts (lightbar/LED/rumble on a real pad)
  3. Harness layer (types/claude/codex/index, hooks-install adaptation, state.ts extension) — includes empirical keybinding verification
  4. Layers + router + gestures (layers.ts, router.ts, KeyRepeater wiring)
  5. Server/client trim + invocation + cli.ts integration
  6. Tests + README; full verification gate: npm test && npm run lint && npm run typecheck && npm run format:check (match vibesense script names)

Known gaps (deliberate)

  • 5 LED slots vs Codex Micro's 6 agent keys (DualSense has 5 player LEDs).
  • error/complete are heuristics — hooks provide no ground-truth error signal.
  • Xbox/DS4/generic: input only, no RGB/rumble feedback path.
  • Xbox driver is wired-USB report layout only (inherited from vibesense).