diff --git a/.gitignore b/.gitignore index 37f8c76..f71f514 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ venv/ # Stack-nudge dev artifacts /tmp/ *.log +keys/ diff --git a/README.md b/README.md index a5dab05..9f97a70 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ | Codex | ✅ *(experimental)* | | Any hooks-capable agent | ✅ — point it at `notify.sh` | -**Platforms:** macOS (native banners + click-to-focus) · Linux (PulseAudio / ALSA / libnotify) · Windows (Git Bash / WSL) +**Platforms:** macOS — full app with panel, click-to-focus banners, auto-update, quota tracking, voice. Linux (PulseAudio / ALSA / libnotify) and Windows (Git Bash / WSL) get audio + basic notifications via `notify.sh` only. ## Install @@ -28,9 +28,9 @@ cd stack-nudge ./install.sh ``` -The installer auto-detects which agents you have configured (`~/.claude`, `~/.cursor`, `~/.gemini`) and wires up their hooks. +The installer auto-wires hooks for **Claude Code** (`~/.claude/settings.json`) and **Cursor** (`~/.cursor/hooks.json`). Gemini CLI and Codex are supported through the same `notify.sh` entry-point, but their hooks must be wired manually — see [Manual setup](#manual-setup) below. -On macOS it also installs the native `stack-nudge.app` for click-to-focus banners. Without the binary, macOS falls back to `osascript` notifications (no click-to-focus). +On macOS it also installs the native `stack-nudge.app`, which provides the floating panel, click-to-focus banners, auto-update, and quota tracking. The first launch will show a welcome screen with a "Grant permissions" button — taking that step up-front unlocks click-to-focus and the keystroke-based "Allow" approvals. ## How it works @@ -72,18 +72,15 @@ Add that to your shell profile. ### Keyboard-native panel (macOS) -If you'd rather not click banners with the mouse, stack-nudge can run a small floating panel that you summon with a hotkey. It has three tabs — **Events**, **Sessions**, and **Settings** — and is fully keyboard-driven. +If you'd rather not click banners with the mouse, stack-nudge runs a small floating panel that you summon with a hotkey. It has four tabs — **Events**, **Sessions**, **Usage**, and **Settings** — and is fully keyboard-driven. -Enable it in `~/.stack-nudge/config`: +The panel is installed and registered as a launchd agent by `./install.sh` — no opt-in needed. To run quietly without macOS banners (panel-only): ```bash -STACKNUDGE_PANEL=true STACKNUDGE_BANNER=false # optional — suppress macOS banners when using the panel ``` -Default hotkey is `cmd+opt+n`. Hit it from anywhere to summon the panel; hit it again while focused to hide. Switch tabs with `Cmd+1` (Events), `Cmd+2` (Sessions), `Cmd+3` (Settings) — or click them. - -The panel registers a launchd agent so it starts at login when enabled. Banner and panel can run together, alone, or both off — the sound and voice still fire as passive signals. +Default hotkey is `cmd+opt+n`. Hit it from anywhere to summon the panel; hit it again while focused to hide. Switch tabs with `Cmd+1` (Events), `Cmd+2` (Sessions), `Cmd+3` (Usage), `Cmd+4` (Settings) — or click them. Banner and panel can run together, alone, or both off — the sound and voice still fire as passive signals. #### Events tab @@ -111,9 +108,31 @@ Live list of running agent processes (`claude`, `gemini`, `codex` — including | `⌫` | Send SIGTERM to the agent process | | `Esc` | Hide the panel | +#### Usage tab + +Reachable from the tab strip or `Cmd+3`. Renders your Claude Code subscription quota — the same numbers `claude /usage` shows in the terminal — but always available without typing the command: + +- **Current session** (5-hour rolling window) +- **Current week (all models)** +- **Current week (Opus only)** *(when your plan has the tier)* +- **Current week (Sonnet only)** *(when your plan has the tier)* + +Bars are color-coded: green below 50%, yellow 50–80%, red 80%+. Reset times shown per tier. + +Data is fetched from the same endpoint Claude Code's own statusline uses (`/api/oauth/usage`), reading your OAuth token from the macOS Keychain. **The first time stack-nudge polls you'll see a keychain dialog — click "Always Allow"** to grant access (one-time, per release). + +Polls every 60 seconds while the panel is visible, or every 5 minutes (`STACKNUDGE_USAGE_POLL_MIN`) in the background. + +#### Threshold-crossing notifications + +When any tier reaches your configured threshold, stack-nudge fires a banner — *"Weekly quota at 85% — resets May 17"* — once per period per tier, so you get a heads-up before hitting the cap. Configure in Settings → Usage: + +- **Quota alerts** — master switch (default on) +- **Alert threshold** — 50% / 70% / 80% / 90% / 95% (default 80%) + #### Settings tab -Reachable from the tab strip or `Cmd+3`. Keyboard-driven rows for hotkey, banner/voice toggles, sound picks (with preview-on-cycle), voice picker (with preview-on-cycle using a random conversational phrase), speed, and shortcuts to the permissions checker, config file, and quit. +Reachable from the tab strip or `Cmd+4`. Keyboard-driven rows for hotkey, banner/voice toggles, sound picks (with preview-on-cycle), voice picker (with preview-on-cycle using a random conversational phrase), speed, quota alert config, and shortcuts to the permissions checker, config file, phrase editor, and quit. | Key | Action | |-----|--------| @@ -164,6 +183,27 @@ stack-nudge's apps are **ad-hoc signed**, so every rebuild produces a new cdhash If approval has stopped working after a rebuild, hit **Reset & prompt** in the permissions checker. It runs `tccutil reset`, then triggers a fresh dialog bound to the current cdhash. +### Auto-update + +stack-nudge polls GitHub Releases on launch and every 6 hours. When a newer release exists, the Settings tab gets a small accent dot and an "Update available · vX.Y.Z" row at the top of the list. Click it (or press Enter while it's selected) for a confirmation view with the release notes, then "Update Now" runs the install: + +1. Clones the repo to `/tmp` +2. Runs `install.sh` against the cloned source (rebuild + replace `~/Applications/stack-nudge.app` + reload launchd) +3. After completion, the panel auto-quits; launchd brings up the new bundle +4. The new bundle's first launch shows a welcome-style "Updated to vX.Y.Z" screen with the release notes + +While the StackOne stack-nudge repo is private the auto-updater falls back to your local `gh` CLI auth (`gh api`) to read the release metadata; the in-app git clone uses your existing git credentials (keychain or SSH). Org members with `gh` configured see no friction. + +### Phrase editor + +The phrase pools that power [Voice notifications](#voice-notifications) can be customised in-app. Settings → "Edit phrases…" opens a keyboard-driven editor where you can: + +- Toggle individual built-in phrases on or off (`Space`) +- Add your own custom phrases (typed inline, `Enter` to commit) +- Remove custom phrases (`⌫`) + +Per-pool customisations are stored in `~/.stack-nudge/phrases.user.json` and merged with the built-in pools at notification time. Disable a built-in phrase you find too cheery, add ones in your own voice — the same random-selection logic still applies. + ### Voice notifications stack-nudge uses [stackvox](https://github.com/StackOneHQ/stackvox), an offline Kokoro-82M TTS engine that speaks notifications aloud with ~13 ms latency. `./install.sh` pip-installs it from PyPI into an isolated venv at `~/.stack-nudge/venv` — no separate setup needed. @@ -215,10 +255,16 @@ The Settings tab exposes the same picks with audio preview on each change. ## Uninstall ```bash +git pull # if you cloned a while back — older uninstall.sh lacks hook cleanup ./uninstall.sh ``` -Removes the hooks from each agent's config and deletes `~/.stack-nudge/`. +Cleans up: + +- Hook entries in `~/.claude/settings.json`, `~/.cursor/hooks.json`, and `~/.gemini/settings.json` +- The launchd agents (`com.stackonehq.stack-nudge`, `…-daemon`) +- `~/Applications/stack-nudge.app` +- `~/.stack-nudge/` (including the Python venv and `notify.sh`) ## Manual setup @@ -240,22 +286,21 @@ Every supported agent just needs a hook that runs `notify.sh /dev/null \ + | awk -F'"' '/"Developer ID Application/ {print $2; exit}' + ) + fi + + if [[ -n "$identity" ]]; then + codesign --force --deep --options runtime --sign "$identity" "$app" + echo " Signed: $identity" + else + codesign --force --deep --sign - "$app" + echo " Signed: ad-hoc (no Developer ID Application cert in keychain)" + fi } echo "Building stack-nudge ($ARCH)..." diff --git a/uninstall.sh b/uninstall.sh index fcb6ce9..705a0c8 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -8,15 +8,16 @@ INSTALL_DIR="${HOME}/.stack-nudge" echo "Uninstalling stack-nudge..." # Remove hooks from Claude Code. Matches anything inside a `tinynudge` or -# `stack-nudge` install dir so legacy entries (and dev checkouts pointing at -# moved paths) get cleaned up too, not just the current $NOTIFY path. +# `stack-nudge` install dir — including quoted forms like +# `"$HOME/.stack-nudge/notify.sh"` — so legacy entries (and dev checkouts +# pointing at moved paths) get cleaned up too, not just the current $NOTIFY. if [[ -f "$HOME/.claude/settings.json" ]]; then python3 - "$HOME/.claude/settings.json" <<'PY' import json, re, sys from pathlib import Path path = Path(sys.argv[1]) -STALE = re.compile(r"(?:^|/)\.?(?:tinynudge|stack-nudge)/notify\.sh(?:\s|$)") +STALE = re.compile(r'(?:^|/|")\.?(?:tinynudge|stack-nudge)/notify\.sh') settings = json.loads(path.read_text()) hooks = settings.get("hooks", {}) for event in list(hooks.keys()): @@ -46,7 +47,7 @@ import json, re, sys from pathlib import Path path = Path(sys.argv[1]) -STALE = re.compile(r"(?:^|/)\.?(?:tinynudge|stack-nudge)/notify\.sh(?:\s|$)") +STALE = re.compile(r'(?:^|/|")\.?(?:tinynudge|stack-nudge)/notify\.sh') settings = json.loads(path.read_text()) hooks = settings.get("hooks", {}) for event in list(hooks.keys()):