Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Linux may work if `bleak` can access the Bluetooth adapter. Windows is not a tar
| matching `PostToolUse` after approval | `working` | return to working |
| ordinary `PostToolUse` | `working` | quiet activity heartbeat |
| `Stop` | `tool_done` | green finish flash, then idle |
| `SessionEnd` | `idle` | release session ownership |

Tool-use events are intentionally not used for separate tool-running transitions. Ordinary `PostToolUse` events still refresh `working` so a premature `Stop` event or long-running turn does not leave the lamp idle while Codex is active.

Expand All @@ -51,10 +52,26 @@ When multiple Codex chats are open, the hook uses an active-session lock:
- the latest prompt claims the lamp
- other sessions cannot force idle while that session is active
- permission state cannot be overwritten by another session
- the active session releases the lock on `Stop`
- a new prompt can claim the lamp immediately
- `SessionEnd` or lock expiry releases abandoned ownership

Codex currently does not expose a distinct “user clicked approve” hook. This project treats the matching `PostToolUse` after a permission request as the approval-resolved signal.

## Night Mode

Night mode can turn the lamp off during a local quiet window while still allowing Codex activity to wake it back up. It only applies when the daemon believes the lamp is idle or ambient; `working`, `attention`, and other active states continue to show normally.

```json
{
"night_mode_enabled": true,
"night_mode_off_time": "23:00",
"night_mode_on_time": "07:00",
"night_mode_state": "off"
}
```

When the clock is inside the configured window, idle or ambient states are replaced with `night_mode_state`. A new Codex prompt, permission request, or tool heartbeat still applies its normal state and turns the lamp back on. At the end of the quiet window, the daemon restores the idle or ambient state that night mode replaced.

## Install

```bash
Expand Down Expand Up @@ -222,20 +239,20 @@ If daemon code changes after installing the LaunchAgent, rerun:
scripts/install-macos-service
```

## Optional macOS Menu Bar App
## macOS Menu Bar App

This branch includes an experimental menu bar wrapper for macOS. It does not replace the Python daemon or talk to BLE directly. It reads the existing config, state, and log files, then calls the same local scripts and CLI commands you would run from Terminal.

Build the local unsigned app:
Build, install, launch, and start the local unsigned app at login:

```bash
macos/MenuBarApp/Scripts/package-app
scripts/install-macos-menu-app
```

Launch it:
Remove it with:

```bash
open "macos/MenuBarApp/build/Codex Moonside.app"
scripts/uninstall-macos-menu-app
```

The menu shows daemon status, current lamp state, configured lamp target, and quick actions for testing `attention`, testing `ambient`, turning the lamp off, scanning BLE devices, restarting the daemon, and opening logs/config.
Expand All @@ -244,7 +261,6 @@ Current limitations:

- macOS only
- local unsigned app
- developer convenience wrapper, not a polished installer
- expects the project checkout and `.venv` to remain available
- generated `.app` bundles are not committed to Git

Expand All @@ -264,6 +280,8 @@ Important fields:
"permission_file": "/tmp/codex_moonside_permission_pending.json",
"session_lock_file": "/tmp/codex_moonside_active_session.json",
"session_lock_enabled": true,
"session_lock_max_age_seconds": 43200,
"permission_marker_max_age_seconds": 43200,
"lamp_name_contains": "Moonside",
"ble_address": null,
"poll_interval_seconds": 0.2,
Expand All @@ -275,11 +293,15 @@ Important fields:
"ambient_after_idle_seconds": 1800,
"ambient_state": "ambient",
"codex_process_tracking_enabled": false,
"codex_process_names": ["Codex"],
"codex_process_poll_interval_seconds": 5,
"codex_process_names": ["ChatGPT", "Codex"],
"codex_process_poll_interval_seconds": 15,
"codex_process_timeout_seconds": 5,
"codex_process_warning_interval_seconds": 300,
"codex_process_missing_seconds": 60,
"codex_process_missing_state": "ambient",
"codex_process_return_state": "idle"
"codex_process_return_state": "idle",
"log_max_bytes": 2097152,
"log_backup_count": 3
}
```

Expand All @@ -294,7 +316,7 @@ Optional macOS process tracking lets the daemon switch the lamp to ambient, or t
}
```

This watches process names with `ps`, so it does not require the menu bar app. It is best suited for the Codex desktop app; leave it disabled if you mainly use Codex CLI without the app.
This watches the current `ChatGPT.app` name and the legacy `Codex.app` name with a lightweight exact `pgrep` check, so it does not require the menu bar app. Process checks run outside the BLE event loop and warnings are rate-limited. Leave tracking disabled if you mainly use Codex CLI without the desktop app.

Default attention effect:

Expand Down
14 changes: 12 additions & 2 deletions codex_moonside/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"permission_file": DEFAULT_PERMISSION_FILE,
"session_lock_file": DEFAULT_SESSION_LOCK_FILE,
"session_lock_enabled": True,
"session_lock_max_age_seconds": 43200,
"permission_marker_max_age_seconds": 43200,
"lamp_name_contains": "Moonside",
"ble_address": None,
"poll_interval_seconds": 0.2,
Expand All @@ -42,14 +44,22 @@
"minimum_attention_seconds": 1.0,
"ambient_after_idle_seconds": 1800,
"ambient_state": "ambient",
"night_mode_enabled": False,
"night_mode_off_time": "23:00",
"night_mode_on_time": "07:00",
"night_mode_state": "off",
"codex_process_tracking_enabled": False,
"codex_process_names": ["Codex"],
"codex_process_poll_interval_seconds": 5,
"codex_process_names": ["ChatGPT", "Codex"],
"codex_process_poll_interval_seconds": 15,
"codex_process_timeout_seconds": 5,
"codex_process_warning_interval_seconds": 300,
"codex_process_missing_seconds": 60,
"codex_process_missing_state": "ambient",
"codex_process_return_state": "idle",
"log_file": DEFAULT_DAEMON_LOG_FILE,
"hook_log_file": DEFAULT_HOOK_LOG_FILE,
"log_max_bytes": 2097152,
"log_backup_count": 3,
"states": {
"idle": {
"commands": ["LEDON", "BRIGH060", "COLOR255120040"],
Expand Down
Loading
Loading