From 24c819d540ff7644224798e189cad03c1b44fced Mon Sep 17 00:00:00 2001 From: Hisku Date: Mon, 18 May 2026 15:01:12 +0100 Subject: [PATCH 1/2] docs: refresh README + fix uninstall regex for quoted hook paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README sync: - Tab count is now 4 (Events / Sessions / Usage / Settings); Cmd+1-4 shortcuts; remove the dead STACKNUDGE_PANEL=true opt-in (panel is always installed since the single-binary merge). - Sharpen platform support: macOS-only for panel, auto-update, quota tracking, click-to-focus, voice. Linux/Windows get audio + libnotify only. - Fix the agent auto-detection claim — install.sh wires Claude Code and Cursor; Gemini/Codex require Manual setup. - Add new sections for Usage tab, threshold notifications, auto-update, phrase editor, and welcome screen. - Update Development section to match the current single-binary layout (drop notifier/; update make targets that referred to "both .app bundles"). - Expand Uninstall section to enumerate what's actually cleaned up. uninstall.sh: - Loosen the stale-hook regex to match quoted commands like `"$HOME/.stack-nudge/notify.sh"`. Previous version required the path to be preceded by `^` or `/` and followed by whitespace/EOF — quoted forms slipped through and left orphan hooks behind. Verified against ten realistic command shapes: matches 8 expected, correctly skips 2 false positives ("legitimate-other-tool" and "something-stack-nudge"). Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 81 ++++++++++++++++++++++++++++++++++++++++------------ uninstall.sh | 9 +++--- 2 files changed, 68 insertions(+), 22 deletions(-) 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 Date: Mon, 18 May 2026 17:17:09 +0100 Subject: [PATCH 2/2] build: prefer Developer ID signing, fall back to ad-hoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build.sh's signing step now resolves an identity in this order: 1. $STACKNUDGE_SIGN_IDENTITY (env override — for CI) 2. First "Developer ID Application" identity in the local keychain 3. Ad-hoc (`codesign -s -`) — previous behaviour When a real identity is used, --options runtime is added so the bundle is notarisation-eligible. Hardened runtime is omitted from the ad-hoc path because it imposes restrictions without any of the benefits (notarisation requires Developer ID anyway). For devs with a Developer ID cert in their keychain, this stabilises the cdhash across rebuilds — TCC and Keychain ACL grants stick across the build/install/restart cycle instead of needing to be re-granted every time. For everyone else, behaviour is unchanged (ad-hoc path, prompts on every cdhash change). Note: end users installing via `./install.sh` still build locally and still get ad-hoc signing. Stable identity for end users requires shipping a pre-built signed bundle as a release artifact, which is a separate CI workflow + auto-updater change. Also: gitignore keys/ — developers may store their local cert backups there. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + build.sh | 42 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) 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/build.sh b/build.sh index 8be54ba..cf5d847 100755 --- a/build.sh +++ b/build.sh @@ -28,9 +28,45 @@ build_app() { cp "$icon_path" "$contents/Resources/Icon.icns" fi - # Sign the bundle so Info.plist is bound into the signature. - # Without this, macOS records the wrong identity for TCC (AXIsProcessTrusted = false). - codesign --force --deep --sign - "$app" + sign_bundle "$app" +} + +# Sign the bundle so Info.plist is bound into the signature. Without this, +# macOS records the wrong identity for TCC (AXIsProcessTrusted = false). +# +# Resolution order: +# 1. $STACKNUDGE_SIGN_IDENTITY (explicit override — used by CI when a +# release artifact needs to be signed with a known identity from a +# secret-loaded keychain). +# 2. First "Developer ID Application" identity in the user's keychain +# (devs with the cert get stable code-sig hashes across rebuilds, so +# macOS TCC/Keychain grants stick). +# 3. Ad-hoc (`codesign -s -`) — old behaviour. Works for everyone but the +# cdhash changes on every build, which means TCC + Keychain prompts +# re-fire on each rebuild and each release. +# +# Hardened runtime (--options runtime) is enabled when a real identity is +# present so the signed bundle is notarisation-eligible. It's omitted from +# the ad-hoc path because it makes the binary slightly more restricted +# without any of the benefits (notarisation requires Developer ID). +sign_bundle() { + local app="$1" + local identity="${STACKNUDGE_SIGN_IDENTITY:-}" + + if [[ -z "$identity" ]]; then + identity=$( + security find-identity -v -p codesigning 2>/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)..."