# CodexBar Companion Proxy

Lightweight HTTP server that bridges Claude and Codex usage APIs to your ESP32.

## Requirements

- macOS (uses Keychain via `security` CLI)
- Python 3.10+
- Claude CLI installed and authenticated (`claude` command works)
- Optional: Codex authenticated with `~/.codex/auth.json`

## Setup

```bash
# From the CodexBar-ESP32/ root:
python3 companion/server.py
```

On startup it prints your Mac's local IP — paste that into `config.h` as `PROXY_HOST`.

```
CodexBar companion proxy starting on port 7842
ESP32 PROXY_HOST = "192.168.1.42"
Endpoint: http://192.168.1.42:7842/usage
```

Check that the companion is actually listening:

```bash
curl http://127.0.0.1:7842/health
curl http://127.0.0.1:7842/status
```

If `curl` says it could not connect to port `7842`, the companion is not running or did not restart after the Mac slept. That is different from an ESP32 WiFi problem: if the ESP32 says `WiFi not connected`, check router/SSID/password instead.

If `curl /health` times out while a companion process is still listening on port `7842`, restart the companion. That usually means the running process is an older single-threaded server wedged in a slow `/usage` refresh after Mac wake. Current versions use a threaded server so `/health`, `/status`, and hooks remain responsive during provider refreshes.

If another companion is already running, manual startup exits with the PID using port `7842` instead of killing it. To intentionally replace the existing process:

```bash
CODEXBAR_REPLACE_EXISTING=1 python3 companion/server.py
```

## Tests

Run the companion tests from the repo root:

```bash
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements-dev.txt
.venv/bin/python -m unittest discover -s tests
```

The dev requirements are only for local testing/simulation. The companion server itself uses the Python standard library.

## Usage Endpoint

`GET http://<mac-ip>:7842/usage`

```json
{
  "ok": true,
  "updated_at": 1748210000,
  "providers": [
    {
      "name": "Claude",
      "attention": {
        "state": "idle",
        "count": 0,
        "primary_project": "",
        "projects": [],
        "reason": "",
        "updated_at": 1748210000
      },
      "windows": [
        { "label": "5h",  "pct": 16, "resets_in_sec": 10414, "duration_sec": 18000 },
        { "label": "7d",  "pct": 17, "resets_in_sec": 419014, "duration_sec": 604800 }
      ]
    }
  ]
}
```

- `pct`: 0–100 (clamped; >100% usage shows as 100 with red color on device)
- `resets_in_sec`: seconds until this window resets; -1 if unknown

## Attention State

The companion can also act as a local attention-state broker for Claude/Codex hooks and notification-only local model completion events.
For Codex app builds where `~/.codex/hooks.json` is not firing, use
`companion/codex_notify.py` as the `notify` wrapper; see
[attention hook setup](../docs/attention-hook-setup.md).

Post a session lifecycle event:

```bash
curl -X POST http://127.0.0.1:7842/attention \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "Codex",
    "event": "PermissionRequest",
    "session_id": "manual-test",
    "cwd": "'$PWD'"
  }'
```

Check attention state without calling upstream quota APIs:

```bash
curl http://127.0.0.1:7842/status
```

The ESP32 can poll `/status` frequently because it is local-only. It reads the companion's in-memory attention state and does not call Claude, Codex, Anthropic, or OpenAI usage endpoints.

Clear the same session:

```bash
curl -X POST http://127.0.0.1:7842/attention \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "Codex",
    "event": "SessionEnd",
    "session_id": "manual-test",
    "cwd": "'$PWD'"
  }'
```

When a session enters `needs_user`, the companion sends an ntfy notification to the default topic:

```text
codingAgentWaitingOnYou
```

Usage reset notifications are sent to a separate default topic:

```text
codexbarUsageResets
```

Override or disable these ntfy topics with:

```bash
CODEXBAR_NTFY_TOPIC=myTopic python3 companion/server.py
CODEXBAR_NTFY_TOPIC= python3 companion/server.py
CODEXBAR_USAGE_RESET_NTFY_TOPIC=myResetTopic python3 companion/server.py
CODEXBAR_USAGE_RESET_NTFY_TOPIC= python3 companion/server.py
```

Stop events normally become `done`. If the hook payload includes `last_assistant_message` and it looks like a question or choice list, the companion upgrades it to `needs_user` with reason `possible_question`. Disable that heuristic with:

```bash
CODEXBAR_QUESTION_HEURISTIC=0 python3 companion/server.py
```

For LM Studio or another local model runner, post a completion event when your wrapper script finishes:

```bash
curl -X POST http://127.0.0.1:7842/attention \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "LM Studio",
    "event": "TaskComplete",
    "session_id": "lmstudio-manual-test",
    "cwd": "'$PWD'"
  }'
```

Or call the helper:

```bash
python3 companion/lmstudio_notify.py
```

LM Studio is notification-only: `TaskComplete` sends the low-priority ntfy completion alert even if there was no prior `SessionStart`, but LM Studio is not returned from `/status`, `/usage`, usage windows, or the ESP32 UI.

See [attention hook setup](../docs/attention-hook-setup.md) for Claude and Codex hook configuration examples.

## Environment Variables Reference

All env vars and their defaults. Pass on the command line for one-off runs;
inline them in the launchd plist `EnvironmentVariables` dict for permanent setup
(launchd does not source `.env` files or shell profiles).

| Variable | Default | Purpose |
|---|---|---|
| `CODEXBAR_NTFY_TOPIC` | `codingAgentWaitingOnYou` | ntfy topic for agent-waiting push notifications. Set empty to disable. |
| `CODEXBAR_USAGE_RESET_NTFY_TOPIC` | `codexbarUsageResets` | Dedicated ntfy topic for provider usage reset notifications. Set empty to disable reset notifications. |
| `CODEXBAR_SSH_HOST` | _(empty — button disabled)_ | Tailscale (or LAN) hostname for the "Open Terminal" button in ntfy notifications. Termius on iOS handles `ssh://` — tap the button to open a connected shell. |
| `CODEXBAR_SSH_USER` | `$USER` | SSH username for the terminal button URL. |
| `CODEXBAR_QUESTION_HEURISTIC` | `1` | Set `0` to disable the heuristic that upgrades `Stop` events to `needs_user` when the last message looks like a question. |
| `CODEXBAR_REPLACE_EXISTING` | _(unset)_ | Set `1` to kill any process already on port 7842 at startup. |
| `CODEXBAR_UNDERPACE_PCT_THRESHOLD` | `25` | Fire idle-quota ntfy when 7d window usage is below this percent. |
| `CODEXBAR_UNDERPACE_MIN_REMAINING_SEC` | `172800` (2 days) | Minimum seconds left in the 7d window before firing idle-quota ntfy. |
| `CODEXBAR_UNDERPACE_COOLDOWN_SEC` | `1800` | Minimum gap between idle-quota ntfy fires. |
| `CODEXBAR_UNDERPACE_WINDOW` | `7d` | Which usage window label to monitor for idle-quota detection. |
| `CODEXBAR_RESET_5H_REMAINING_THRESHOLD` | `40` | 5h reset notifies only if previous remaining was below this percent. |
| `CODEXBAR_RESET_SLEEP_START_HOUR` | `23` | Start of quiet hours (local time, 0–23). No 5h-reset ntfy during this window. |
| `CODEXBAR_RESET_SLEEP_END_HOUR` | `6` | End of quiet hours (local time, 0–23). |
| `CODEXBAR_NOTIFY_DONE` | `1` | Set `0` to disable the low-priority "agent finished" notification on task completion. |
| `CODEXBAR_ATTENTION_URL` | `http://127.0.0.1:7842/attention` | Override in hooks if companion runs on a non-default port. |

## Notification tiers

The companion sends separate ntfy notifications so you can distinguish
between "needs a response now", "task complete", and "quota reset".

| Situation | Priority | Title | SSH button |
|---|---|---|---|
| Agent waiting for input (`needs_user`) | **high** | "Agent waiting for you" | Yes — tap to open terminal |
| Agent finished a task (`done`) | low | "Agent task complete" | No |
| Usage reset detected | default/low | "CodexBar: quota reset" or "CodexBar: usage refreshed" | No |

The ntfy iOS app surfaces high-priority notifications with a sound and banner;
low-priority ones arrive silently. This means you get paged when action is
needed but can glance at task completions at your leisure.

Disable done notifications with `CODEXBAR_NOTIFY_DONE=0` if the volume is too high.
Subscribe to `codexbarUsageResets` in ntfy to receive reset-only notifications,
or set `CODEXBAR_USAGE_RESET_NTFY_TOPIC` to your own private topic name and
subscribe to that topic instead.

## SSH + tmux workflow (remote access from iPhone)

With Tailscale and a terminal app (Termius or Blink Shell), you can start and
respond to agent sessions from your iPhone without being at your desk.

**One-time setup:**

1. Install tmux: `brew install tmux`
2. Create `~/.tmux.conf` with at minimum `set -g mouse on` (this repo includes
   a starter config — see `../local-setup.sh` for the copy command).
3. Set `CODEXBAR_SSH_HOST` and `CODEXBAR_SSH_USER` in the launchd plist (see above).

**Daily workflow:**

```bash
# On the Mac — start a persistent session (survives SSH disconnects)
tmux new-session -A -s agents

# From Termius/Blink on iPhone via Tailscale
ssh you@your-machine.tail02a1a0.ts.net
tmux attach -t agents      # re-attach to the running session
tmux ls                    # list all sessions
```

**Starting an agent from your iPhone:**
```bash
# Inside the tmux session — same commands as at your desk
claude          # Claude Code
codex           # Codex CLI
```

The ntfy attention hook fires regardless of whether the agent was started
locally or via SSH, so CodexBar tracks all sessions the same way.

**When you get an "Agent waiting" notification:**
1. Tap the ntfy notification
2. Tap "Open Terminal" → Termius opens and SSH-connects to your Mac
3. `tmux attach -t agents` to re-enter the running session
4. Respond via keyboard — iOS dictation works in Termius via the system keyboard

## Machine-specific setup (local-setup.sh)

The repo root contains a gitignored `local-setup.sh` with pre-filled
machine-specific commands (plist reload, health check, tmux session start).
On a new machine, copy `local-setup.sh.example` (if present) or regenerate
it from the template in this README. Never commit `local-setup.sh` — it
contains your hostnames and usernames.

```bash
# After cloning on a new machine:
cp local-setup.sh.example local-setup.sh   # if example exists
# Edit: fill in YOUR_HOST, YOUR_USER, etc.
bash local-setup.sh
```

## Auto-start on login (optional)

Create a launchd plist at `~/Library/LaunchAgents/com.codexbar.companion.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.codexbar.companion</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/YOU/projects/CodexBar-ESP32/.venv/bin/python</string>
        <string>/Users/YOU/projects/CodexBar-ESP32/companion/server.py</string>
    </array>

    <key>WorkingDirectory</key>
    <string>/Users/YOU/projects/CodexBar-ESP32</string>

    <!-- Inline all env vars here — launchd does not source .env files or shell profiles -->
    <key>EnvironmentVariables</key>
    <dict>
        <key>CODEXBAR_NTFY_TOPIC</key>
        <string>codingAgentWaitingOnYou</string>
        <key>CODEXBAR_USAGE_RESET_NTFY_TOPIC</key>
        <string>codexbarUsageResets</string>

        <!-- SSH terminal button: set to your Tailscale hostname.
             Tap "Open Terminal" in the ntfy notification → opens SSH in Termius on iOS.
             Leave empty to disable the button. -->
        <key>CODEXBAR_SSH_HOST</key>
        <string>your-machine.tail02a1a0.ts.net</string>
        <key>CODEXBAR_SSH_USER</key>
        <string>yourusername</string>
    </dict>

    <key>RunAtLoad</key>
    <true/>

    <!-- SuccessfulExit=false: restart on crash, not on clean exit.
         Plain KeepAlive=true restarts even on "port already in use" exits. -->
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>

    <key>StandardOutPath</key>
    <string>/tmp/codexbar-companion.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/codexbar-companion.log</string>
</dict>
</plist>
```

Then:

```bash
launchctl unload ~/Library/LaunchAgents/com.codexbar.companion.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/com.codexbar.companion.plist
launchctl kickstart -k gui/$(id -u)/com.codexbar.companion
```

Use the `KeepAlive` dictionary above rather than plain `<true/>`. With plain `KeepAlive=true`, launchd restarts the job even after a clean "port already in use" exit, which can spam the log if you start `python3 companion/server.py` manually while the launch agent is loaded.

Useful diagnostics:

```bash
launchctl print gui/$(id -u)/com.codexbar.companion
tail -n 100 /tmp/codexbar-companion.log
lsof -nP -iTCP:7842 -sTCP:LISTEN
```
