Skip to content

Commit ae51053

Browse files
committed
release 1.7.1
1 parent fb0b274 commit ae51053

8 files changed

Lines changed: 237 additions & 15 deletions

File tree

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ Desktop notifications for AI coding tools - get alerts when tasks complete or in
1313
<img src="assets/multi-tools-support-02.png" width="48%" alt="All tools enabled"/>
1414
</p>
1515

16-
[![Version](https://img.shields.io/badge/version-1.7.0-blue.svg)](https://github.com/mylee04/code-notify/releases)
16+
[![Version](https://img.shields.io/badge/version-1.7.1-blue.svg)](https://github.com/mylee04/code-notify/releases)
1717
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
1818
[![macOS](https://img.shields.io/badge/macOS-supported-green.svg)](https://www.apple.com/macos)
1919
[![Linux](https://img.shields.io/badge/Linux-supported-green.svg)](https://www.linux.org/)
2020
[![Windows](https://img.shields.io/badge/Windows-supported-green.svg)](https://www.microsoft.com/windows)
2121

2222
---
2323

24-
## What's New in v1.7.0
24+
## What's New in v1.7.1
2525

26-
- **New macOS click-through commands**: use `cn click-through` to choose which app opens when you click a notification
27-
- **Embedded terminal support is much better**: IDE terminals such as PhpStorm can now map their runtime terminal key to the app bundle that should be activated
28-
- **Reviewer edge cases are covered**: click-through lookup now handles empty `TERM_PROGRAM` cases and search-based adds now prefer the live runtime terminal key
26+
- **Generated notification state files now live under `notifications/state`**: root-level `~/.claude/notifications` stays cleaner because internal rate-limit files no longer pile up beside user-facing settings
27+
- **Supported upgrades keep working during the move**: legacy root-level `last_notification_*` and `last_stop_notification` files are still read as fallback and are cleaned up on fresh writes
28+
- **Windows and Unix runtimes stay aligned**: both runtimes now use the same state-folder layout, and regression tests cover the new path behavior
2929

3030
---
3131

@@ -212,6 +212,10 @@ cn test
212212
cn update # Runs: npm install -g code-notify@latest
213213
```
214214

215+
**Too many `last_notification_*` files in `~/.claude/notifications`?**
216+
217+
As of `v1.7.1`, generated rate-limit state files are stored under `~/.claude/notifications/state/` instead of cluttering the root notifications folder.
218+
215219
## Project Structure
216220

217221
```

bin/code-notify

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
set -e
77

88
# Version
9-
VERSION="1.7.0"
9+
VERSION="1.7.1"
1010

1111
# Determine the directory where the script is located (resolve symlinks)
1212
SCRIPT_PATH="${BASH_SOURCE[0]}"

lib/code-notify/core/notifier.sh

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ get_tool_display_name() {
133133
TOOL_DISPLAY=$(get_tool_display_name "$TOOL_NAME")
134134

135135
# Rate limiting for stop notifications (prevents spam from parallel sub-agents)
136-
RATE_LIMIT_DIR="$HOME/.claude/notifications"
136+
NOTIFICATIONS_DIR="$HOME/.claude/notifications"
137+
RATE_LIMIT_DIR="$NOTIFICATIONS_DIR/state"
137138
STOP_RATE_LIMIT_SECONDS="${CODE_NOTIFY_STOP_RATE_LIMIT_SECONDS:-10}"
138139
NOTIFICATION_RATE_LIMIT_SECONDS="${CODE_NOTIFY_NOTIFICATION_RATE_LIMIT_SECONDS:-180}"
139140

@@ -147,6 +148,12 @@ get_rate_limit_file() {
147148
printf '%s/%s\n' "$RATE_LIMIT_DIR" "$key"
148149
}
149150

151+
get_legacy_rate_limit_file() {
152+
local key
153+
key=$(sanitize_rate_limit_key "$1")
154+
printf '%s/%s\n' "$NOTIFICATIONS_DIR" "$key"
155+
}
156+
150157
get_notification_subtype() {
151158
if [[ "$HOOK_DATA" == *"idle_prompt"* ]]; then
152159
printf '%s\n' "idle_prompt"
@@ -180,8 +187,13 @@ get_notification_rate_limit_key() {
180187
is_rate_limited() {
181188
local rate_limit_key="$1"
182189
local rate_limit_seconds="$2"
183-
local lock_file
190+
local lock_file legacy_lock_file
184191
lock_file=$(get_rate_limit_file "$rate_limit_key")
192+
legacy_lock_file=$(get_legacy_rate_limit_file "$rate_limit_key")
193+
194+
if [[ ! -f "$lock_file" ]] && [[ -f "$legacy_lock_file" ]]; then
195+
lock_file="$legacy_lock_file"
196+
fi
185197

186198
if [[ ! -f "$lock_file" ]]; then
187199
return 1 # No previous notification, not rate limited
@@ -202,10 +214,14 @@ is_rate_limited() {
202214

203215
update_rate_limit() {
204216
local rate_limit_key="$1"
205-
local lock_file
217+
local lock_file legacy_lock_file
206218
lock_file=$(get_rate_limit_file "$rate_limit_key")
219+
legacy_lock_file=$(get_legacy_rate_limit_file "$rate_limit_key")
207220
mkdir -p "$RATE_LIMIT_DIR"
208221
date +%s > "$lock_file"
222+
if [[ "$legacy_lock_file" != "$lock_file" ]]; then
223+
rm -f "$legacy_lock_file"
224+
fi
209225
}
210226

211227
is_project_scoped_notification() {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "code-notify",
3-
"version": "1.7.0",
3+
"version": "1.7.1",
44
"description": "Desktop notifications for Claude Code, OpenAI Codex, and Gemini CLI",
55
"license": "MIT",
66
"homepage": "https://github.com/mylee04/code-notify#readme",

scripts/install-windows.ps1

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ param(
2020
$ErrorActionPreference = "Stop"
2121

2222
# Version
23-
$VERSION = "1.7.0"
23+
$VERSION = "1.7.1"
2424

2525
# Colors and formatting
2626
function Write-Success { param([string]$Message) Write-Host "[OK] $Message" -ForegroundColor Green }
@@ -33,6 +33,7 @@ function Write-Header { param([string]$Message) Write-Host "`n$Message" -Foregro
3333
$ClaudeHome = "$env:USERPROFILE\.claude"
3434
$InstallDir = "$env:USERPROFILE\.code-notify"
3535
$NotificationsDir = "$ClaudeHome\notifications"
36+
$NotificationStateDir = "$NotificationsDir\state"
3637
$LogsDir = "$ClaudeHome\logs"
3738

3839
function Show-Banner {
@@ -93,7 +94,7 @@ function Install-ClaudeNotify {
9394
Write-Header "Installing Code-Notify..."
9495

9596
# Create directories
96-
$directories = @($InstallDir, "$InstallDir\bin", "$InstallDir\lib", $NotificationsDir, $LogsDir)
97+
$directories = @($InstallDir, "$InstallDir\bin", "$InstallDir\lib", $NotificationsDir, $NotificationStateDir, $LogsDir)
9798
foreach ($dir in $directories) {
9899
if (-not (Test-Path $dir)) {
99100
New-Item -ItemType Directory -Path $dir -Force | Out-Null
@@ -106,7 +107,7 @@ function Install-ClaudeNotify {
106107
# Code-Notify PowerShell Module
107108
# https://github.com/mylee04/code-notify
108109
109-
$script:VERSION = "1.7.0"
110+
$script:VERSION = "1.7.1"
110111
$script:ClaudeHome = if ($env:CLAUDE_HOME) { $env:CLAUDE_HOME } else { "$env:USERPROFILE\.claude" }
111112
$script:DefaultSettingsFile = "$script:ClaudeHome\settings.json"
112113
$script:AlternateSettingsFile = "$env:USERPROFILE\.config\.claude\settings.json"
@@ -1531,7 +1532,7 @@ function Get-ToolDisplayName {
15311532
15321533
$ToolDisplay = Get-ToolDisplayName $ToolName
15331534
1534-
$NotificationStateDir = "$ClaudeHome\notifications"
1535+
$NotificationStateDir = "$ClaudeHome\notifications\state"
15351536
15361537
try {
15371538
$StopRateLimitSeconds = [int]($env:CODE_NOTIFY_STOP_RATE_LIMIT_SECONDS)
@@ -1597,6 +1598,13 @@ function Get-RateLimitPath {
15971598
return Join-Path $NotificationStateDir $safeKey
15981599
}
15991600
1601+
function Get-LegacyRateLimitPath {
1602+
param([string]$Key)
1603+
1604+
$safeKey = ($Key -replace '[^A-Za-z0-9._-]', '_')
1605+
return Join-Path "$ClaudeHome\notifications" $safeKey
1606+
}
1607+
16001608
function Test-RateLimited {
16011609
param(
16021610
[string]$Key,
@@ -1608,6 +1616,13 @@ function Test-RateLimited {
16081616
}
16091617
16101618
$path = Get-RateLimitPath $Key
1619+
if (-not (Test-Path $path)) {
1620+
$legacyPath = Get-LegacyRateLimitPath $Key
1621+
if (Test-Path $legacyPath) {
1622+
$path = $legacyPath
1623+
}
1624+
}
1625+
16111626
if (-not (Test-Path $path)) {
16121627
return $false
16131628
}
@@ -1630,7 +1645,13 @@ function Update-RateLimit {
16301645
}
16311646
16321647
$currentTime = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
1633-
$currentTime | Set-Content (Get-RateLimitPath $Key) -Encoding ASCII
1648+
$path = Get-RateLimitPath $Key
1649+
$currentTime | Set-Content $path -Encoding ASCII
1650+
1651+
$legacyPath = Get-LegacyRateLimitPath $Key
1652+
if ($legacyPath -ne $path -and (Test-Path $legacyPath)) {
1653+
Remove-Item $legacyPath -Force -ErrorAction SilentlyContinue
1654+
}
16341655
}
16351656
16361657
# Function to check if notification should be suppressed

scripts/run_tests.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,22 @@ else
198198
test_fail "click-through commands failed"
199199
fi
200200

201+
# Test 20: rate-limit state files live under notifications/state with legacy fallback
202+
test_start "notification state dir"
203+
if bash tests/test-notification-state-dir.sh >/dev/null 2>&1; then
204+
test_pass
205+
else
206+
test_fail "notification state dir failed"
207+
fi
208+
209+
# Test 21: Windows notifier keeps rate-limit state under notifications/state
210+
test_start "windows rate-limit state"
211+
if bash tests/test-windows-rate-limit-state.sh >/dev/null 2>&1; then
212+
test_pass
213+
else
214+
test_fail "windows rate-limit state failed"
215+
fi
216+
201217
# Summary
202218
echo ""
203219
echo "Test Summary:"
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
NOTIFIER="$SCRIPT_DIR/../lib/code-notify/core/notifier.sh"
7+
8+
pass() { echo "PASS: $1"; }
9+
fail() { echo "FAIL: $1"; exit 1; }
10+
11+
wait_for_lines() {
12+
local file="$1"
13+
local expected_lines="$2"
14+
15+
for _ in $(seq 1 40); do
16+
if [[ -f "$file" ]] && [[ $(wc -l < "$file") -ge "$expected_lines" ]]; then
17+
return 0
18+
fi
19+
sleep 0.05
20+
done
21+
22+
return 1
23+
}
24+
25+
run_notifier() {
26+
local fake_path="$1"
27+
local subtype="$2"
28+
29+
printf '{"type":"%s"}\n' "$subtype" | \
30+
PATH="$fake_path" \
31+
CODE_NOTIFY_NOTIFICATION_RATE_LIMIT_SECONDS=180 \
32+
bash "$NOTIFIER" notification claude test-project
33+
}
34+
35+
test_dir="$(mktemp -d)"
36+
trap 'rm -rf "$test_dir"' EXIT
37+
38+
export HOME="$test_dir/home"
39+
fake_bin="$test_dir/bin"
40+
log_dir="$test_dir/log"
41+
sound_file="$test_dir/custom.aiff"
42+
mkdir -p "$HOME/.claude/notifications" "$HOME/.claude/logs" "$fake_bin" "$log_dir"
43+
44+
touch "$sound_file"
45+
: > "$HOME/.claude/notifications/sound-enabled"
46+
printf '%s\n' "$sound_file" > "$HOME/.claude/notifications/sound-custom"
47+
48+
case "$(uname -s)" in
49+
Darwin)
50+
notification_log="$log_dir/terminal-notifier.log"
51+
sound_log="$log_dir/afplay.log"
52+
cat > "$fake_bin/terminal-notifier" <<EOF
53+
#!/bin/bash
54+
printf '%s\n' "\$*" >> "$notification_log"
55+
EOF
56+
cat > "$fake_bin/afplay" <<EOF
57+
#!/bin/bash
58+
printf '%s\n' "\$*" >> "$sound_log"
59+
EOF
60+
;;
61+
Linux)
62+
notification_log="$log_dir/notify-send.log"
63+
sound_log="$log_dir/paplay.log"
64+
cat > "$fake_bin/notify-send" <<EOF
65+
#!/bin/bash
66+
printf '%s\n' "\$*" >> "$notification_log"
67+
EOF
68+
cat > "$fake_bin/paplay" <<EOF
69+
#!/bin/bash
70+
printf '%s\n' "\$*" >> "$sound_log"
71+
EOF
72+
;;
73+
*)
74+
echo "SKIP: unsupported OS for notification state-dir test"
75+
exit 0
76+
;;
77+
esac
78+
79+
chmod +x "$fake_bin"/*
80+
81+
fake_path="$fake_bin:/usr/bin:/bin:/usr/sbin:/sbin"
82+
state_file="$HOME/.claude/notifications/state/last_notification_claude_test-project_idle_prompt"
83+
legacy_file="$HOME/.claude/notifications/last_notification_claude_test-project_idle_prompt"
84+
85+
run_notifier "$fake_path" "idle_prompt"
86+
87+
wait_for_lines "$notification_log" 1 || fail "expected a notification delivery"
88+
[[ -f "$state_file" ]] || fail "rate-limit state file should be written under notifications/state"
89+
[[ ! -f "$legacy_file" ]] || fail "new notifications should not write legacy root-level rate-limit files"
90+
91+
rm -f "$state_file"
92+
date +%s > "$legacy_file"
93+
lines_before=$(wc -l < "$notification_log")
94+
95+
run_notifier "$fake_path" "idle_prompt"
96+
97+
sleep 0.1
98+
lines_after=$(wc -l < "$notification_log")
99+
[[ "$lines_before" -eq "$lines_after" ]] || fail "legacy root-level rate-limit files should still suppress duplicates during upgrade"
100+
101+
printf '%s\n' "0" > "$legacy_file"
102+
run_notifier "$fake_path" "idle_prompt"
103+
104+
wait_for_lines "$notification_log" 2 || fail "expected a notification after legacy state aged out"
105+
[[ -f "$state_file" ]] || fail "fresh writes should recreate the state file under notifications/state"
106+
[[ ! -f "$legacy_file" ]] || fail "fresh writes should clean up legacy root-level rate-limit files"
107+
108+
pass "notification state files move under notifications/state without breaking legacy fallback"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
WINDOWS_INSTALLER="$SCRIPT_DIR/../scripts/install-windows.ps1"
7+
8+
pass() { echo "PASS: $1"; }
9+
fail() { echo "FAIL: $1"; exit 1; }
10+
11+
if ! grep -qF '$NotificationStateDir = "$NotificationsDir\state"' "$WINDOWS_INSTALLER"; then
12+
fail "installer should create a dedicated notifications state directory"
13+
fi
14+
15+
if ! grep -qF '$NotificationStateDir = "$ClaudeHome\notifications\state"' "$WINDOWS_INSTALLER"; then
16+
fail "generated Windows notifier should store rate-limit files under notifications\\state"
17+
fi
18+
19+
if ! grep -qF 'function Get-LegacyRateLimitPath' "$WINDOWS_INSTALLER"; then
20+
fail "legacy rate-limit fallback helper is missing from the Windows notifier"
21+
fi
22+
23+
if ! grep -qF 'Join-Path "$ClaudeHome\notifications" $safeKey' "$WINDOWS_INSTALLER"; then
24+
fail "Windows legacy rate-limit fallback path is missing"
25+
fi
26+
27+
if command -v pwsh >/dev/null 2>&1; then
28+
ps_script="$(mktemp)"
29+
trap 'rm -f "$ps_script"' EXIT
30+
cat > "$ps_script" <<'EOF'
31+
$ClaudeHome = "/tmp/tester/.claude"
32+
$NotificationStateDir = "$ClaudeHome\notifications\state"
33+
34+
function Get-RateLimitPath {
35+
param([string]$Key)
36+
$safeKey = ($Key -replace '[^A-Za-z0-9._-]', '_')
37+
return Join-Path $NotificationStateDir $safeKey
38+
}
39+
40+
function Get-LegacyRateLimitPath {
41+
param([string]$Key)
42+
$safeKey = ($Key -replace '[^A-Za-z0-9._-]', '_')
43+
return Join-Path "$ClaudeHome\notifications" $safeKey
44+
}
45+
46+
$expectedState = "/tmp/tester/.claude/notifications/state/last_notification_claude_demo_idle_prompt"
47+
$expectedLegacy = "/tmp/tester/.claude/notifications/last_notification_claude_demo_idle_prompt"
48+
49+
if ((Get-RateLimitPath "last_notification_claude_demo_idle_prompt") -ne $expectedState) { exit 1 }
50+
if ((Get-LegacyRateLimitPath "last_notification_claude_demo_idle_prompt") -ne $expectedLegacy) { exit 1 }
51+
EOF
52+
if ! pwsh -NoProfile -File "$ps_script"; then
53+
fail "Windows rate-limit paths should resolve to state and legacy locations"
54+
fi
55+
fi
56+
57+
pass "Windows notifier stores rate-limit state under notifications\\state with legacy fallback"

0 commit comments

Comments
 (0)