Skip to content

Commit 392012e

Browse files
authored
Merge pull request #39 from StackOneHQ/chore/post-v1.6-cleanups
docs: refresh README + fix uninstall regex for quoted hook paths
2 parents 0de5dc5 + 9a09fce commit 392012e

4 files changed

Lines changed: 108 additions & 25 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ venv/
2626
# Stack-nudge dev artifacts
2727
/tmp/
2828
*.log
29+
keys/

README.md

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
| Codex |*(experimental)* |
1717
| Any hooks-capable agent | ✅ — point it at `notify.sh` |
1818

19-
**Platforms:** macOS (native banners + click-to-focus) · Linux (PulseAudio / ALSA / libnotify) · Windows (Git Bash / WSL)
19+
**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.
2020

2121
## Install
2222

@@ -28,9 +28,9 @@ cd stack-nudge
2828
./install.sh
2929
```
3030

31-
The installer auto-detects which agents you have configured (`~/.claude`, `~/.cursor`, `~/.gemini`) and wires up their hooks.
31+
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.
3232

33-
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).
33+
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.
3434

3535
## How it works
3636

@@ -72,18 +72,15 @@ Add that to your shell profile.
7272

7373
### Keyboard-native panel (macOS)
7474

75-
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.
75+
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.
7676

77-
Enable it in `~/.stack-nudge/config`:
77+
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):
7878

7979
```bash
80-
STACKNUDGE_PANEL=true
8180
STACKNUDGE_BANNER=false # optional — suppress macOS banners when using the panel
8281
```
8382

84-
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.
85-
86-
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.
83+
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.
8784

8885
#### Events tab
8986

@@ -111,9 +108,31 @@ Live list of running agent processes (`claude`, `gemini`, `codex` — including
111108
| `` | Send SIGTERM to the agent process |
112109
| `Esc` | Hide the panel |
113110

111+
#### Usage tab
112+
113+
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:
114+
115+
- **Current session** (5-hour rolling window)
116+
- **Current week (all models)**
117+
- **Current week (Opus only)** *(when your plan has the tier)*
118+
- **Current week (Sonnet only)** *(when your plan has the tier)*
119+
120+
Bars are color-coded: green below 50%, yellow 50–80%, red 80%+. Reset times shown per tier.
121+
122+
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).
123+
124+
Polls every 60 seconds while the panel is visible, or every 5 minutes (`STACKNUDGE_USAGE_POLL_MIN`) in the background.
125+
126+
#### Threshold-crossing notifications
127+
128+
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:
129+
130+
- **Quota alerts** — master switch (default on)
131+
- **Alert threshold** — 50% / 70% / 80% / 90% / 95% (default 80%)
132+
114133
#### Settings tab
115134

116-
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.
135+
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.
117136

118137
| Key | Action |
119138
|-----|--------|
@@ -164,6 +183,27 @@ stack-nudge's apps are **ad-hoc signed**, so every rebuild produces a new cdhash
164183

165184
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.
166185

186+
### Auto-update
187+
188+
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:
189+
190+
1. Clones the repo to `/tmp`
191+
2. Runs `install.sh` against the cloned source (rebuild + replace `~/Applications/stack-nudge.app` + reload launchd)
192+
3. After completion, the panel auto-quits; launchd brings up the new bundle
193+
4. The new bundle's first launch shows a welcome-style "Updated to vX.Y.Z" screen with the release notes
194+
195+
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.
196+
197+
### Phrase editor
198+
199+
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:
200+
201+
- Toggle individual built-in phrases on or off (`Space`)
202+
- Add your own custom phrases (typed inline, `Enter` to commit)
203+
- Remove custom phrases (``)
204+
205+
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.
206+
167207
### Voice notifications
168208

169209
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.
215255
## Uninstall
216256

217257
```bash
258+
git pull # if you cloned a while back — older uninstall.sh lacks hook cleanup
218259
./uninstall.sh
219260
```
220261

221-
Removes the hooks from each agent's config and deletes `~/.stack-nudge/`.
262+
Cleans up:
263+
264+
- Hook entries in `~/.claude/settings.json`, `~/.cursor/hooks.json`, and `~/.gemini/settings.json`
265+
- The launchd agents (`com.stackonehq.stack-nudge`, `…-daemon`)
266+
- `~/Applications/stack-nudge.app`
267+
- `~/.stack-nudge/` (including the Python venv and `notify.sh`)
222268

223269
## Manual setup
224270

@@ -240,22 +286,21 @@ Every supported agent just needs a hook that runs `notify.sh <agent-name> <event
240286
## Development
241287

242288
```bash
243-
make build # builds both .app bundles into build/
289+
make build # builds stack-nudge.app into build/
244290
make install # full install (build + copy + register hooks + launchd)
245-
make reload # rebuild + replace installed panel + refresh notify.sh + bounce daemon
291+
make reload # rebuild + replace installed app + refresh notify.sh + bounce the daemon
246292
make dev # watch sources; auto-reload on .swift / Info.plist / notify.sh / phrase changes
247-
make uninstall # remove apps, hooks, launchd agents, ~/.stack-nudge/
293+
make uninstall # remove app, hooks, launchd agents, ~/.stack-nudge/
248294
```
249295

250296
`make dev` is the inner-loop tool — leave it running in another terminal, save a Swift file or `notify.sh`, and the daemon bounces with the new build in ~2 seconds.
251297

252298
Source layout:
253299

254-
- `notifier/` — the transient banner-renderer that fires per nudge and exits
255-
- `panel/` — the persistent floating-panel daemon (hotkey, NSPanel, socket listener, menu bar, sessions list, settings, permissions window)
256-
- `shared/` — code shared by both binaries (currently `AppActivator.swift`)
300+
- `panel/` — the single persistent `stack-nudge.app` binary: hotkey, floating NSPanel, socket listener for incoming events, macOS banner posting via `UNUserNotificationCenter`, sessions list, settings, permissions window, auto-updater, quota probe
301+
- `shared/` — code shared with the standalone Linux/Windows surfaces (currently `AppActivator.swift`)
257302
- `phrases/` — per-language voice phrase pools sourced by `notify.sh` at hook time
258-
- `notify.sh` — the shell entry-point CC/Cursor/Gemini hooks invoke; routes events to banner / panel / voice surfaces
303+
- `notify.sh` — the shell entry-point CC/Cursor/Gemini hooks invoke; on macOS posts events to the running app via Unix-domain socket, on Linux/Windows handles audio + libnotify directly
259304

260305
Swift compiled with `swiftc` directly. No Xcode, no SPM, no dependencies.
261306

build.sh

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,45 @@ build_app() {
2828
cp "$icon_path" "$contents/Resources/Icon.icns"
2929
fi
3030

31-
# Sign the bundle so Info.plist is bound into the signature.
32-
# Without this, macOS records the wrong identity for TCC (AXIsProcessTrusted = false).
33-
codesign --force --deep --sign - "$app"
31+
sign_bundle "$app"
32+
}
33+
34+
# Sign the bundle so Info.plist is bound into the signature. Without this,
35+
# macOS records the wrong identity for TCC (AXIsProcessTrusted = false).
36+
#
37+
# Resolution order:
38+
# 1. $STACKNUDGE_SIGN_IDENTITY (explicit override — used by CI when a
39+
# release artifact needs to be signed with a known identity from a
40+
# secret-loaded keychain).
41+
# 2. First "Developer ID Application" identity in the user's keychain
42+
# (devs with the cert get stable code-sig hashes across rebuilds, so
43+
# macOS TCC/Keychain grants stick).
44+
# 3. Ad-hoc (`codesign -s -`) — old behaviour. Works for everyone but the
45+
# cdhash changes on every build, which means TCC + Keychain prompts
46+
# re-fire on each rebuild and each release.
47+
#
48+
# Hardened runtime (--options runtime) is enabled when a real identity is
49+
# present so the signed bundle is notarisation-eligible. It's omitted from
50+
# the ad-hoc path because it makes the binary slightly more restricted
51+
# without any of the benefits (notarisation requires Developer ID).
52+
sign_bundle() {
53+
local app="$1"
54+
local identity="${STACKNUDGE_SIGN_IDENTITY:-}"
55+
56+
if [[ -z "$identity" ]]; then
57+
identity=$(
58+
security find-identity -v -p codesigning 2>/dev/null \
59+
| awk -F'"' '/"Developer ID Application/ {print $2; exit}'
60+
)
61+
fi
62+
63+
if [[ -n "$identity" ]]; then
64+
codesign --force --deep --options runtime --sign "$identity" "$app"
65+
echo " Signed: $identity"
66+
else
67+
codesign --force --deep --sign - "$app"
68+
echo " Signed: ad-hoc (no Developer ID Application cert in keychain)"
69+
fi
3470
}
3571

3672
echo "Building stack-nudge ($ARCH)..."

uninstall.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ INSTALL_DIR="${HOME}/.stack-nudge"
88
echo "Uninstalling stack-nudge..."
99

1010
# Remove hooks from Claude Code. Matches anything inside a `tinynudge` or
11-
# `stack-nudge` install dir so legacy entries (and dev checkouts pointing at
12-
# moved paths) get cleaned up too, not just the current $NOTIFY path.
11+
# `stack-nudge` install dir — including quoted forms like
12+
# `"$HOME/.stack-nudge/notify.sh"` — so legacy entries (and dev checkouts
13+
# pointing at moved paths) get cleaned up too, not just the current $NOTIFY.
1314
if [[ -f "$HOME/.claude/settings.json" ]]; then
1415
python3 - "$HOME/.claude/settings.json" <<'PY'
1516
import json, re, sys
1617
from pathlib import Path
1718
1819
path = Path(sys.argv[1])
19-
STALE = re.compile(r"(?:^|/)\.?(?:tinynudge|stack-nudge)/notify\.sh(?:\s|$)")
20+
STALE = re.compile(r'(?:^|/|")\.?(?:tinynudge|stack-nudge)/notify\.sh')
2021
settings = json.loads(path.read_text())
2122
hooks = settings.get("hooks", {})
2223
for event in list(hooks.keys()):
@@ -46,7 +47,7 @@ import json, re, sys
4647
from pathlib import Path
4748
4849
path = Path(sys.argv[1])
49-
STALE = re.compile(r"(?:^|/)\.?(?:tinynudge|stack-nudge)/notify\.sh(?:\s|$)")
50+
STALE = re.compile(r'(?:^|/|")\.?(?:tinynudge|stack-nudge)/notify\.sh')
5051
settings = json.loads(path.read_text())
5152
hooks = settings.get("hooks", {})
5253
for event in list(hooks.keys()):

0 commit comments

Comments
 (0)