Metadata-Version: 2.4
Name: vibesignal
Version: 0.1.0
Summary: Physical status light for coding agents (Claude Code / Codex) via busylight
Author-email: Yue Zhao <yzhao010@usc.edu>
License: BSD 2-Clause License
        
        Copyright (c) 2026, Yue Zhao
        All rights reserved.
        
        Redistribution and use in source and binary forms, with or without
        modification, are permitted provided that the following conditions are met:
        
        1. Redistributions of source code must retain the above copyright notice,
           this list of conditions and the following disclaimer.
        
        2. Redistributions in binary form must reproduce the above copyright notice,
           this list of conditions and the following disclaimer in the documentation
           and/or other materials provided with the distribution.
        
        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
        AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
        IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
        ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
        LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
        CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
        SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
        INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
        CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
        ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
        POSSIBILITY OF SUCH DAMAGE.
        
Project-URL: Homepage, https://github.com/yzhao062/vibesignal
Project-URL: Repository, https://github.com/yzhao062/vibesignal
Project-URL: Issues, https://github.com/yzhao062/vibesignal/issues
Keywords: claude-code,codex,ai-agent,status-light,busylight,tkinter,macos,windows,linux,developer-tools
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: busylight-core>=2.3.0
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Provides-Extra: macos
Requires-Dist: pyobjc-framework-Cocoa>=10; extra == "macos"
Dynamic: license-file

<div align="center">

# VibeSignal

**A daemon-free physical status light for AI coding agents (Claude Code, Codex)**

[![PyPI](https://img.shields.io/pypi/v/vibesignal.svg)](https://pypi.org/project/vibesignal/)
[![License: BSD-2-Clause](https://img.shields.io/badge/License-BSD%202--Clause-blue.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org)
[![Status: alpha](https://img.shields.io/badge/status-alpha-orange.svg)](#whats-next)
[![GitHub Stars](https://img.shields.io/github/stars/yzhao062/vibesignal?style=social)](https://github.com/yzhao062/vibesignal)

[Install](#install) · [macOS Quickstart](#macos-quickstart) · [How It Works](#how-it-works) · [Three Renderers](#three-renderers) · [Configure Agents](#configure-agents) · [What's Next](#whats-next)

<br>

<img src="https://raw.githubusercontent.com/yzhao062/vibesignal/main/docs/widget-mockup.png" alt="vibesignal floating widget: a calm grey panel while agents work, turning red the moment a session blocks for your input" width="760">

</div>

> [!TIP]
> Maintained by [Yue Zhao](https://yzhao062.github.io): USC CS faculty, author of [PyOD](https://github.com/yzhao062/pyod) (9.8K stars, 38M+ downloads, ~12K research citations).

> [!NOTE]
> When Claude Code or Codex needs your reply, a USB light on your desk turns amber. When an agent finishes its turn, it turns blue. While an agent is working, green. A light on the desk is harder to miss than another system notification, which is the whole point.

## What You Get

- 🟢 **Solid colors, daemon-free:** the state persists in the hardware after the hook exits; no service to keep alive
- 🪝 **Hook-driven:** `UserPromptSubmit`, `PostToolUse`, `Notification`, `Stop`, `SessionEnd` all wire in via JSON
- 🤖 **Cross-agent:** the same store covers Claude Code and Codex; one light tracks both
- 📺 **Three renderers:** USB busylight (hardware), terminal watch panel, always-on-top Tk widget
- 🚦 **Multi-session aware:** runs 4–5 agents in parallel; the widget shows which one is blocked
- 🍎 **macOS one-click:** `install-launcher` + `install-autostart` wire the `.app` and the LaunchAgent
- 🪟 **Cross-platform:** Windows, macOS, Linux; per-platform fonts and work-area detection

## Install

```bash
pip install vibesignal
```

On macOS, add the `macos` extra for accurate Dock-aware widget placement:

```bash
pip install 'vibesignal[macos]'
```

Then wire autostart for your OS.

**macOS** (one-click launcher in Spotlight and the Dock, plus login autostart):

```bash
vibesignal install-launcher
vibesignal install-autostart
```

**Windows**: put a shortcut to `pythonw -m vibesignal widget` in the Startup folder (Win+R, type `shell:startup`, drop the shortcut there).

**Linux**: add a `.desktop` entry under `~/.config/autostart/` that runs `vibesignal widget`.

<details>
<summary>Install the latest unreleased build from GitHub</summary>

```bash
pip install 'vibesignal @ git+https://github.com/yzhao062/vibesignal.git'

# with the macOS extra:
pip install 'vibesignal[macos] @ git+https://github.com/yzhao062/vibesignal.git'
```

</details>

After install, [configure your agents](#configure-agents) to fire the hooks.

## macOS Quickstart

First-run walk-through on a Mac:

```bash
# 1. Install (with macos extra for accurate Dock-aware placement)
pip install 'vibesignal[macos]'

# 2. Wire Claude Code hooks (one-time, user level)
#    Merge hooks/claude-settings.snippet.json into ~/.claude/settings.json
#    under "hooks". See `Configure Agents` below for details.

# 3. Install the one-click .app launcher
vibesignal install-launcher
#    Spotlight: Cmd+Space, type 'VibeSignal', press Enter.
#    Or drag ~/Applications/VibeSignal.app to the Dock.

# 4. Install login autostart (also starts the widget right now)
vibesignal install-autostart

# 5. Verify
vibesignal status            # active sessions and the resolved color
launchctl print gui/$UID/io.github.yzhao062.vibesignal | grep -E "state|program"
```

After step 4 a small panel appears in the bottom-left of your screen. When any Claude Code session blocks for permission, the panel turns red and shows which session.

**Daily lifecycle on macOS:**

| Action | Command |
|---|---|
| Show widget on demand | Cmd+Space → `VibeSignal` → Enter (Spotlight); or `vibesignal widget &` |
| Quit widget window | Right-click or `Control`-click header → Quit |
| Force-kill a running widget | `pkill -f "vibesignal.widget"` |
| Start the LaunchAgent right now (no relogin) | `launchctl kickstart gui/$UID/io.github.yzhao062.vibesignal` |
| Disable autostart, keep launcher | `vibesignal uninstall-autostart` |
| Re-enable autostart later | `vibesignal install-autostart` (idempotent) |
| Remove the `.app` launcher | `vibesignal uninstall-launcher` |
| Inspect autostart status | `launchctl print gui/$UID/io.github.yzhao062.vibesignal` |
| Tail autostart logs | `tail /tmp/io.github.yzhao062.vibesignal.log /tmp/io.github.yzhao062.vibesignal.err` |
| Re-pin paths after switching conda env | `vibesignal install-autostart` |
| Manually clear stuck sessions | `vibesignal clear` (all) or `vibesignal clear --session <id>` |

## How It Works

Every hook invocation does the full cycle and exits. Nothing has to stay running.

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#dbeafe', 'primaryTextColor': '#1e3a8a',
  'primaryBorderColor': '#3b82f6', 'lineColor': '#475569',
  'fontFamily': 'system-ui', 'fontSize': '13px'
}}}%%
flowchart LR
    H["Claude Code / Codex hook"] --> C["vibesignal event<br/>--agent X --state Y"]
    C --> S[("~/.vibesignal/<br/>sessions/*.json")]
    S --> L["USB busylight"]
    S --> P["Watch panel"]
    S --> W["Floating widget"]
```

Each hook fires `vibesignal event`, which writes one JSON file per (agent, session), reads every active file, resolves the aggregate state by priority (`blocked > error > done > working > idle`), and updates the USB light if the color changed. Sessions that stop emitting events drop off after their per-state TTL.

Concurrent hooks stay honest two ways. The record-resolve-apply cycle holds a short cross-process lock, so two sessions firing at once cannot leave the light on a lower-priority color (a finishing `working` hook overwriting a fresh `blocked` event from another session). Every state file is written atomically (`tempfile` plus `os.replace`), so a reader never sees a half-written file. The lock is bounded: if it cannot be taken quickly it gives up and proceeds, because never blocking the agent's hook matters more than perfect ordering under rare contention.

## State Table

| State | Light | Set by (Claude Code hook) | Meaning |
|-------|-------|---------------------------|---------|
| `blocked` | Amber, solid | `Notification` (`permission_prompt`) | An agent needs you now |
| `done` | Blue, solid | `Stop`, `StopFailure`, `Notification` (`idle_prompt`) | An agent finished its turn; your move |
| `working` | Green, solid | `UserPromptSubmit`, `PostToolUse` | An agent is busy, do not interrupt |
| `error` | Red, solid | Manual only | A failure |
| `idle` | Off | TTL timeout, done-fade | Nothing needs you |

Aggregate priority across active sessions: `blocked > error > done > working > idle`. If any one agent is waiting on you, the light is amber, so the signal you care about most is never hidden.

## Three Renderers

| Renderer | Command | Where It Lives |
|---|---|---|
| 🟢 **USB busylight** | Driven automatically by every `event` call | Physical light on the desk |
| 📋 **Watch panel** | `vibesignal watch` | Live multi-session TUI in a terminal pane |
| 🪟 **Floating widget** | `vibesignal widget` | Always-on-top Tk window |

All three read the same state store, so they stay in sync. The widget shows which session is blocked when several agents run at once. The USB light shows the highest-priority state across all sessions.

### Watch Panel

```bash
vibesignal watch
```

Live table, one row per active session, blocked rows first:

```
  PROJECT          AGENT    STATE      FOR
* aegis            claude   * blocked  1m12s
* agent-audit      codex    * blocked  8s
o iet-paper        claude   o done     3s
. random           claude   . working  --
```

Foreground viewer (Ctrl-C to stop), not a daemon. `vibesignal watch --once` renders a single snapshot.

### Floating Widget

Cross-platform Tk panel, always-on-top, draggable by the header. Right-click to quit; on macOS, `Control`-click also opens the Quit menu.

```bash
# macOS: open via Spotlight ("VibeSignal") after install-launcher, or:
vibesignal widget &

# Windows (no console window):
pythonw -m vibesignal widget

# Linux:
vibesignal widget &
```

The widget pins to the bottom-left of the work area on first launch, then becomes draggable. A `done` row fades after about 90 seconds, a silent `working` row clears after 10 minutes, and `blocked` or `error` rows persist until the state changes or the 8-hour backstop expires.

## Configure Agents

### Claude Code

Merge [`hooks/claude-settings.snippet.json`](hooks/claude-settings.snippet.json) into `~/.claude/settings.json` under `"hooks"`. The keys (`UserPromptSubmit`, `PostToolUse`, `Notification`, `Stop`, `StopFailure`, `SessionEnd`) do not collide with any default hooks. `Notification` is split by matcher: `permission_prompt` sets `blocked`, `idle_prompt` sets `done`. `SessionEnd` clears the session at once instead of waiting out the TTL.

The `vibesignal` command reads the session id from the hook's stdin JSON, so one light tracks every concurrent session.

> [!TIP]
> `PostToolUse` returns the light to green after you approve a mid-task permission prompt, at the cost of one quick call per tool use. Drop it if you prefer zero per-tool overhead; the light then stays amber until the turn ends.

### Codex

The state store is agent-agnostic: events carry an `--agent` tag. Codex points at the same command with `--agent codex`, so one light covers both. See [`hooks/codex-hooks.md`](hooks/codex-hooks.md) for the mapping (Codex's `notify` program or 0.130+ hooks system).

## Test Without Hardware

The light arrives later than the code does, so the whole pipeline is observable without a device:

```bash
vibesignal event --agent claude --state working
vibesignal status        # active sessions and the resolved color
vibesignal off           # clear all sessions
```

With no light connected, `event` records state and prints the color it would set, then exits cleanly. Hooks never fail when the light is missing or unplugged.

## Hardware

There is no purpose-built "AI agent light" product. The proven path is a commercial presence light plus the open-source [`busylight-core`](https://pypi.org/project/busylight-core/) library, which supports many USB lights across multiple vendors.

| Light | Form | Notes |
|---|---|---|
| **Luxafor Flag 2** | Magnet on a monitor edge, USB-C | Eye-level spot, holds its color |
| **blink(1) mk2** | Tiny, fully open | Long-standing developer favorite |

Both are on Amazon US and supported by `busylight-core`. Check the live price before buying.

<details>
<summary><b>Why Solid Colors, Not Blinking</b></summary>

Blinking a USB light needs a process that stays alive to drive the blink, which would mean a daemon. Solid colors persist in the light hardware after the process exits, so they fit the daemon-free design. Amber solid is still very visible.

This assumes a light that holds its last state (Luxafor, blink(1), BlinkStick). Kuando-style lights that need a constant connection would require the daemon mode even for solid colors.

</details>

<details>
<summary><b>Per-State Lifetimes</b></summary>

- **`done`** fades after ~90 seconds (a transient "your move" pulse)
- **`working`** clears after 10 minutes of silence (a silent working session is treated as dead)
- **`blocked`** and **`error`** persist for up to 8 hours: nothing refreshes them while they wait on you, and a shorter TTL would drop a long-pending prompt exactly when it is most overdue. Cleared sooner when you act on it, when the session ends, or by `vibesignal clear`.

The 8-hour backstop only self-cleans a hard-crashed session that left no final event.

</details>

<details>
<summary><b>macOS Launcher and Autostart Internals</b></summary>

Two helper subcommands wire up macOS-native paths without any new package dependency:

```bash
# One-click launcher: compiles an AppleScript .app via `osacompile` into
# ~/Applications/VibeSignal.app. Spotlight-able, draggable to the Dock.
vibesignal install-launcher
vibesignal uninstall-launcher

# Login autostart: writes ~/Library/LaunchAgents/io.github.yzhao062.vibesignal.plist
# with the absolute path of `vibesignal` baked in (so LaunchAgent's empty PATH
# is not an issue), then loads via `launchctl bootstrap gui/<uid>`. The widget
# starts immediately (RunAtLoad=true) AND at every future login. Re-run after
# switching env to re-pin.
vibesignal install-autostart
vibesignal uninstall-autostart
```

The work area is detected per platform: `SPI_GETWORKAREA` on Windows (taskbar excluded); `NSScreen.visibleFrame` on macOS (menu bar and Dock excluded), with a 28 / 80 px heuristic fallback when `pyobjc` is absent; full screen on Linux. The fallback assumes a bottom Dock; install the `macos` extra for accurate placement under any Dock orientation.

Fonts: `Segoe UI` on Windows, `Helvetica Neue` on macOS, `DejaVu Sans` elsewhere.

</details>

<details>
<summary><b>Project Layout</b></summary>

```
vibesignal/
|-- README.md
|-- DESIGN.md
|-- LICENSE
|-- pyproject.toml
|-- vibesignal/
|   |-- __init__.py
|   |-- store.py        # per-session state files + TTL + atomic writes + last-color cache
|   |-- resolve.py      # aggregate + per-session resolution -> colors
|   |-- light.py        # busylight wrapper, no-ops without a device
|   |-- lock.py         # bounded cross-process lock for the event critical section
|   |-- panel.py        # live multi-session TUI panel (foreground viewer)
|   |-- widget.py       # always-on-top floating GUI panel (Tkinter, stdlib)
|   |-- installer.py    # macOS one-click .app + LaunchAgent autostart helpers
|   |-- __main__.py     # CLI invoked by hooks
|-- hooks/
|   |-- claude-settings.snippet.json
|   |-- codex-hooks.md
|-- tests/
    |-- test_resolve.py
    |-- test_store.py
    |-- test_lock.py
    |-- test_main.py
    |-- test_panel.py
    |-- test_widget.py
    |-- test_installer.py
    |-- test_hooks.py
```

</details>

## What's Next

> [!NOTE]
> Shipped since the last release: PyPI packaging (`pip install vibesignal`) and a GitHub Actions CI matrix (Windows / macOS / Linux, Python 3.11 to 3.13).

- **Homebrew tap** at `yzhao062/homebrew-tap` for `brew install yzhao062/tap/vibesignal`
- **Multi-LED strip** support: a BlinkStick Strip with one cell per session (the store already keys by session; needs a `session -> cell` map in `resolve.py`)
- **Daemon mode (opt-in)** for blinking patterns and auto-off after idle, at the cost of a service to keep alive

## License

[BSD 2-Clause](LICENSE) © 2026 Yue Zhao
