@delorenj/claude-notifications 1.0.1 โ†’ 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ - run: npm ci
19
+ - run: npm test
20
+
21
+ publish-gpr:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: read
26
+ packages: write
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-node@v4
30
+ with:
31
+ node-version: 20
32
+ registry-url: https://npm.pkg.github.com/
33
+ - run: npm ci
34
+ - run: npm publish
35
+ env:
36
+ NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
package/README.md CHANGED
@@ -24,6 +24,7 @@ That's it! ๐ŸŽ‰ The package will automatically:
24
24
  - ๐Ÿ–ฅ๏ธ **Desktop Notifications** - Visual notifications with Claude Code branding
25
25
  - ๐Ÿช **Auto-Integration** - Automatically configures Claude Code hooks
26
26
  - โšก **Zero Configuration** - Works out of the box
27
+ - webhook **Webhook Support** - Trigger a webhook in addition to or instead of the sound
27
28
  - ๐ŸŽจ **Customizable** - Easy to modify sounds and settings
28
29
 
29
30
  ## Usage
@@ -109,6 +110,40 @@ ls ~/.local/share/sounds/claude-notification.wav
109
110
  cp your-custom-sound.wav ~/.local/share/sounds/claude-notification.wav
110
111
  ```
111
112
 
113
+ ### Configure Webhooks
114
+
115
+ You can configure a webhook to be triggered when a notification occurs. This is useful for integrating with other services, such as IFTTT, Zapier, or a custom server.
116
+
117
+ Create a configuration file at `~/.config/claude-notifications/settings.json`.
118
+
119
+ **Example `settings.json`:**
120
+
121
+ ```json
122
+ {
123
+ "sound": true,
124
+ "webhook": {
125
+ "enabled": true,
126
+ "url": "https://maker.ifttt.com/trigger/claude_notification/with/key/YOUR_KEY",
127
+ "replaceSound": false
128
+ }
129
+ }
130
+ ```
131
+
132
+ **Configuration Options:**
133
+
134
+ - `sound`: (boolean) Whether to play the notification sound. Defaults to `true`.
135
+ - `webhook.enabled`: (boolean) Whether to trigger the webhook. Defaults to `false`.
136
+ - `webhook.url`: (string) The URL to send the POST request to.
137
+ - `webhook.replaceSound`: (boolean) If `true`, the sound will not play when a webhook is triggered. Defaults to `false`.
138
+
139
+ The webhook will be sent as a `POST` request with a JSON payload:
140
+
141
+ ```json
142
+ {
143
+ "message": "Claude is waiting for you..."
144
+ }
145
+ ```
146
+
112
147
  ### Create Custom Patterns
113
148
 
114
149
  Use `sox` to create new victory fanfares:
@@ -136,33 +136,73 @@ function createSoundFile() {
136
136
  // Generate pleasant notification scale
137
137
  log("blue", "๐ŸŽผ Generating a pleasant notification scale...");
138
138
 
139
- const soxCommand =
140
- `sox -n "${soundFile}" ` +
141
- "synth 0.12 sine 523.25 fade 0.01 0.12 0.01 : " +
142
- "synth 0.12 sine 587.33 fade 0.01 0.12 0.01 : " +
143
- "synth 0.12 sine 659.25 fade 0.01 0.12 0.01 : " +
144
- "synth 0.12 sine 783.99 fade 0.01 0.12 0.01 : " +
145
- "synth 0.12 sine 1046.50 fade 0.01 0.12 0.01 : " +
146
- "synth 0.12 sine 1174.66 fade 0.01 0.12 0.01 : " +
147
- "synth 0.12 sine 1318.51 fade 0.01 0.12 0.01 : " +
148
- "synth 0.12 sine 1567.98 fade 0.01 0.12 0.01 : " +
149
- "synth 0.12 sine 2093.00 fade 0.01 0.12 0.01 : " +
150
- "synth 0.12 sine 1567.98 fade 0.01 0.12 0.01 : " +
151
- "synth 0.12 sine 1318.51 fade 0.01 0.12 0.01 : " +
152
- "synth 0.12 sine 1174.66 fade 0.01 0.12 0.01 : " +
153
- "synth 0.12 sine 1046.50 fade 0.01 0.12 0.01 : " +
154
- "synth 0.12 sine 783.99 fade 0.01 0.12 0.01 : " +
155
- "synth 0.12 sine 659.25 fade 0.01 0.12 0.01 : " +
156
- "synth 0.12 sine 587.33 fade 0.01 0.12 0.01 : " +
157
- "synth 0.12 sine 523.25 fade 0.01 0.12 0.01 " +
158
- "vol 0.7";
139
+ // Create individual note files first (safer approach)
140
+ const tempDir = path.join(os.tmpdir(), "claude-notifications");
141
+ if (!fs.existsSync(tempDir)) {
142
+ fs.mkdirSync(tempDir, { recursive: true });
143
+ }
144
+
145
+ const notes = [
146
+ { freq: 523.25, name: "C5" }, // C5
147
+ { freq: 587.33, name: "D5" }, // D5
148
+ { freq: 659.25, name: "E5" }, // E5
149
+ { freq: 783.99, name: "G5" }, // G5
150
+ { freq: 1046.5, name: "C6" }, // C6
151
+ { freq: 1174.66, name: "D6" }, // D6
152
+ { freq: 1318.51, name: "E6" }, // E6
153
+ { freq: 1567.98, name: "G6" }, // G6
154
+ { freq: 2093.0, name: "C7" }, // C7
155
+ ];
159
156
 
160
157
  try {
161
- execSync(soxCommand, { stdio: "ignore" });
158
+ const noteFiles = [];
159
+
160
+ // Generate each note individually (much safer)
161
+ for (let i = 0; i < notes.length; i++) {
162
+ const noteFile = path.join(tempDir, `note_${i}.wav`);
163
+ const noteCommand = `sox -n "${noteFile}" synth 0.08 sine ${notes[i].freq} fade 0.01 0.08 0.01 vol 0.7`;
164
+ execSync(noteCommand, { stdio: "ignore", timeout: 5000 }); // 5 second timeout per note
165
+ noteFiles.push(noteFile);
166
+ }
167
+
168
+ // Concatenate all notes into final file
169
+ const concatCommand = `sox ${noteFiles.map((f) => `"${f}"`).join(" ")} "${soundFile}"`;
170
+ execSync(concatCommand, { stdio: "ignore", timeout: 10000 }); // 10 second timeout for concat
171
+
172
+ // Clean up temp files
173
+ noteFiles.forEach((file) => {
174
+ if (fs.existsSync(file)) {
175
+ fs.unlinkSync(file);
176
+ }
177
+ });
178
+
179
+ // Remove temp directory if empty
180
+ try {
181
+ fs.rmdirSync(tempDir);
182
+ } catch (e) {
183
+ // Directory might not be empty, that's ok
184
+ }
185
+
162
186
  log("green", "โœ… Sound file created successfully!");
163
187
  return true;
164
188
  } catch (error) {
165
189
  log("red", `โŒ Error creating sound file: ${error.message}`);
190
+
191
+ // Clean up any temp files on error
192
+ try {
193
+ const tempFiles = fs
194
+ .readdirSync(tempDir)
195
+ .filter((f) => f.startsWith("note_"));
196
+ tempFiles.forEach((file) => {
197
+ const filePath = path.join(tempDir, file);
198
+ if (fs.existsSync(filePath)) {
199
+ fs.unlinkSync(filePath);
200
+ }
201
+ });
202
+ } catch (cleanupError) {
203
+ // Ignore cleanup errors
204
+ }
205
+
166
206
  return false;
167
207
  }
168
208
  }
@@ -1,79 +1,112 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync, spawn } = require('child_process');
3
+ const { execSync } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
+ const http = require('http');
8
+ const https = require('https');
9
+ const { getConfig } = require('../lib/config');
10
+
11
+ const config = getConfig();
7
12
 
8
13
  function playSound() {
14
+ if (!config.sound) {
15
+ return;
16
+ }
17
+
9
18
  const soundFile = path.join(os.homedir(), '.local', 'share', 'sounds', 'claude-notification.wav');
10
19
 
11
20
  if (!fs.existsSync(soundFile)) {
12
- // Fallback to system bell
13
21
  process.stdout.write('\x07');
14
22
  return;
15
23
  }
16
24
 
17
25
  try {
18
- // Try different audio players based on platform
19
26
  if (process.platform === 'linux') {
20
- // Try paplay first (PulseAudio)
21
27
  try {
22
28
  execSync(`paplay "${soundFile}"`, { stdio: 'ignore' });
23
- return;
24
29
  } catch (e) {
25
- // Try aplay (ALSA)
26
30
  try {
27
31
  execSync(`aplay "${soundFile}"`, { stdio: 'ignore' });
28
- return;
29
32
  } catch (e2) {
30
- // Try play (sox)
31
33
  try {
32
34
  execSync(`play "${soundFile}"`, { stdio: 'ignore' });
33
- return;
34
35
  } catch (e3) {
35
- // Fallback to system bell
36
36
  process.stdout.write('\x07');
37
37
  }
38
38
  }
39
39
  }
40
40
  } else if (process.platform === 'darwin') {
41
- // macOS
42
41
  try {
43
42
  execSync(`afplay "${soundFile}"`, { stdio: 'ignore' });
44
- return;
45
43
  } catch (e) {
46
- // Fallback to system bell
47
44
  process.stdout.write('\x07');
48
45
  }
49
46
  } else {
50
- // Other platforms - just system bell
51
47
  process.stdout.write('\x07');
52
48
  }
53
49
  } catch (error) {
54
- // Final fallback
55
50
  process.stdout.write('\x07');
56
51
  }
57
52
  }
58
53
 
54
+ function triggerWebhook() {
55
+ if (!config.webhook || !config.webhook.enabled || !config.webhook.url) {
56
+ return;
57
+ }
58
+
59
+ const { url } = config.webhook;
60
+ const data = JSON.stringify({ message: 'Claude is waiting for you...' });
61
+
62
+ const protocol = url.startsWith('https') ? https : http;
63
+
64
+ const options = {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ 'Content-Length': data.length
69
+ }
70
+ };
71
+
72
+ const req = protocol.request(url, options, (res) => {
73
+ // We don't really care about the response, but it's good practice to handle it
74
+ res.on('data', () => {});
75
+ res.on('end', () => {});
76
+ });
77
+
78
+ req.on('error', (error) => {
79
+ console.error('Error triggering webhook:', error);
80
+ });
81
+
82
+ req.write(data);
83
+ req.end();
84
+ }
85
+
59
86
  function showNotification() {
60
87
  const notifier = require('node-notifier');
61
88
 
62
89
  notifier.notify({
63
90
  title: 'Claude Code',
64
91
  message: 'Waiting for you...',
65
- icon: path.join(os.homedir(), 'Pictures', 'claude.png'), // Will fallback gracefully if not found
66
- sound: false, // We handle sound separately
67
- urgency: 'critical'
92
+ icon: path.join(os.homedir(), 'Pictures', 'claude.png'),
93
+ sound: false,
94
+ urgency: 'critical',
95
+ id: 'claude-code-notification',
96
+ replaceId: 'claude-code-notification'
68
97
  });
69
98
  }
70
99
 
71
100
  function main() {
72
- // Play sound in background
73
- playSound();
74
-
75
- // Show desktop notification
76
- showNotification();
101
+ if (config.webhook.enabled) {
102
+ triggerWebhook();
103
+ if (!config.webhook.replaceSound) {
104
+ playSound();
105
+ }
106
+ } else {
107
+ playSound();
108
+ showNotification();
109
+ }
77
110
  }
78
111
 
79
112
  if (require.main === module) {
package/lib/config.js ADDED
@@ -0,0 +1,31 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const configPath = path.join(os.homedir(), '.config', 'claude-notifications', 'settings.json');
6
+
7
+ function getConfig() {
8
+ const defaultConfig = {
9
+ sound: true,
10
+ webhook: {
11
+ enabled: false,
12
+ url: null,
13
+ replaceSound: false
14
+ }
15
+ };
16
+
17
+ if (!fs.existsSync(configPath)) {
18
+ return defaultConfig;
19
+ }
20
+
21
+ try {
22
+ const configContent = fs.readFileSync(configPath, 'utf-8');
23
+ const userConfig = JSON.parse(configContent);
24
+ return { ...defaultConfig, ...userConfig };
25
+ } catch (error) {
26
+ console.error('Error reading or parsing config file:', error);
27
+ return defaultConfig;
28
+ }
29
+ }
30
+
31
+ module.exports = { getConfig };
package/mise.toml ADDED
@@ -0,0 +1,3 @@
1
+ [tasks.play-sound]
2
+ description = "Play the Claude notification sound"
3
+ run = "node bin/claude-notify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/claude-notifications",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Delightful Notification for Claude Code",
5
5
  "main": "index.js",
6
6
  "bin": {
Binary file
package/install.sh DELETED
@@ -1,161 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Claude Code Notifications Installer
4
- # Installs a much needed notification system for Claude Code
5
-
6
- set -e
7
-
8
- echo "๐ŸŽต Installing Claude Code Notifications..."
9
-
10
- # Colors for output
11
- RED='\033[0;31m'
12
- GREEN='\033[0;32m'
13
- YELLOW='\033[1;33m'
14
- BLUE='\033[0;34m'
15
- NC='\033[0m' # No Color
16
-
17
- # Check if we're on Linux
18
- if [[ "$OSTYPE" != "linux-gnu"* ]]; then
19
- echo -e "${RED}โŒ This installer is currently Linux-only. macOS support coming soon!${NC}"
20
- exit 1
21
- fi
22
-
23
- # Create necessary directories
24
- echo -e "${BLUE}๐Ÿ“ Creating directories...${NC}"
25
- mkdir -p ~/.local/bin
26
- mkdir -p ~/.local/share/sounds
27
-
28
- # Check for required tools and install if needed
29
- echo -e "${BLUE}๐Ÿ”ง Checking dependencies...${NC}"
30
-
31
- # Check for sox
32
- if ! command -v sox &>/dev/null; then
33
- echo -e "${YELLOW}โš ๏ธ sox not found. Installing...${NC}"
34
- if command -v apt &>/dev/null; then
35
- sudo apt update && sudo apt install -y sox
36
- elif command -v dnf &>/dev/null; then
37
- sudo dnf install -y sox
38
- elif command -v pacman &>/dev/null; then
39
- sudo pacman -S sox
40
- else
41
- echo -e "${RED}โŒ Could not install sox. Please install it manually.${NC}"
42
- exit 1
43
- fi
44
- fi
45
-
46
- # Check for notify-send
47
- if ! command -v notify-send &>/dev/null; then
48
- echo -e "${YELLOW}โš ๏ธ notify-send not found. Installing...${NC}"
49
- if command -v apt &>/dev/null; then
50
- sudo apt install -y libnotify-bin
51
- elif command -v dnf &>/dev/null; then
52
- sudo dnf install -y libnotify
53
- elif command -v pacman &>/dev/null; then
54
- sudo pacman -S libnotify
55
- fi
56
- fi
57
-
58
- # Generate that sweet Final Fantasy scale
59
- echo -e "${BLUE}๐ŸŽผ Generating dreamy notification sound...${NC}"
60
- sox -n ~/.local/share/sounds/claude-notification.wav \
61
- synth 0.12 sine 523.25 fade 0.01 0.12 0.01 : \
62
- synth 0.12 sine 587.33 fade 0.01 0.12 0.01 : \
63
- synth 0.12 sine 659.25 fade 0.01 0.12 0.01 : \
64
- synth 0.12 sine 783.99 fade 0.01 0.12 0.01 : \
65
- synth 0.12 sine 1046.50 fade 0.01 0.12 0.01 : \
66
- synth 0.12 sine 1174.66 fade 0.01 0.12 0.01 : \
67
- synth 0.12 sine 1318.51 fade 0.01 0.12 0.01 : \
68
- synth 0.12 sine 1567.98 fade 0.01 0.12 0.01 : \
69
- synth 0.12 sine 2093.00 fade 0.01 0.12 0.01 :
70
- # Uncomment for the longer, more nostalgic version
71
- # synth 0.12 sine 1567.98 fade 0.01 0.12 0.01 : \
72
- # synth 0.12 sine 1318.51 fade 0.01 0.12 0.01 : \
73
- # synth 0.12 sine 1174.66 fade 0.01 0.12 0.01 : \
74
- # synth 0.12 sine 1046.50 fade 0.01 0.12 0.01 : \
75
- # synth 0.12 sine 783.99 fade 0.01 0.12 0.01 : \
76
- # synth 0.12 sine 659.25 fade 0.01 0.12 0.01 : \
77
- # synth 0.12 sine 587.33 fade 0.01 0.12 0.01 : \
78
- # synth 0.12 sine 523.25 fade 0.01 0.12 0.01 \
79
- vol 0.7
80
-
81
- # Create the claude-notify script
82
- echo -e "${BLUE}๐Ÿ“ Creating claude-notify script...${NC}"
83
- cat >~/.local/bin/claude-notify <<'EOF'
84
- #!/usr/bin/env bash
85
-
86
- # Claude Code notification script
87
- # Sends a desktop notification for Claude Code along with a dreamy notification sound
88
-
89
- # Play notification sound (try multiple methods for compatibility)
90
- play_sound() {
91
- local sound_file="$HOME/.local/share/sounds/claude-notification.wav"
92
-
93
- # Try paplay first (PulseAudio - most common on Ubuntu)
94
- if command -v paplay >/dev/null 2>&1 && [[ -f "$sound_file" ]]; then
95
- paplay "$sound_file" 2>/dev/null &
96
- # Fallback to aplay (ALSA)
97
- elif command -v aplay >/dev/null 2>&1 && [[ -f "$sound_file" ]]; then
98
- aplay "$sound_file" 2>/dev/null &
99
- # Fallback to system sounds
100
- elif command -v paplay >/dev/null 2>&1; then
101
- paplay /usr/share/sounds/alsa/Front_Left.wav 2>/dev/null &
102
- # Final fallback to system bell
103
- else
104
- printf '\a'
105
- fi
106
- }
107
-
108
- # Play sound in background
109
- play_sound
110
-
111
- # Send desktop notification
112
- notify-send \
113
- --app-name="Claude Code" \
114
- --icon /home/clouedoc/Pictures/claude.png \
115
- --urgency critical \
116
- "Claude Code" \
117
- "Waiting for you..."
118
- EOF
119
-
120
- # Make the script executable
121
- chmod +x ~/.local/bin/claude-notify
122
-
123
- # Add ~/.local/bin to PATH if not already there
124
- if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
125
- echo -e "${BLUE}๐Ÿ›ค๏ธ Adding ~/.local/bin to PATH...${NC}"
126
-
127
- # Detect shell and add to appropriate config file
128
- if [[ -n "$ZSH_VERSION" ]] || [[ "$SHELL" == *"zsh"* ]]; then
129
- echo 'export PATH="$HOME/.local/bin:$PATH"' >>~/.zshrc
130
- echo -e "${YELLOW}โš ๏ธ Added to ~/.zshrc - restart your terminal or run: source ~/.zshrc${NC}"
131
- elif [[ -n "$BASH_VERSION" ]] || [[ "$SHELL" == *"bash"* ]]; then
132
- echo 'export PATH="$HOME/.local/bin:$PATH"' >>~/.bashrc
133
- echo -e "${YELLOW}โš ๏ธ Added to ~/.bashrc - restart your terminal or run: source ~/.bashrc${NC}"
134
- else
135
- echo -e "${YELLOW}โš ๏ธ Please add ~/.local/bin to your PATH manually${NC}"
136
- fi
137
- fi
138
-
139
- # Test the installation
140
- echo -e "${BLUE}๐Ÿงช Testing installation...${NC}"
141
- if command -v claude-notify &>/dev/null; then
142
- echo -e "${GREEN}โœ… claude-notify command is available!${NC}"
143
- echo -e "${BLUE}๐Ÿ”Š Playing test notification...${NC}"
144
- claude-notify
145
- else
146
- echo -e "${YELLOW}โš ๏ธ claude-notify not in PATH yet. Restart your terminal or source your shell config.${NC}"
147
- fi
148
-
149
- echo ""
150
- echo -e "${GREEN}๐ŸŽ‰ Installation complete!${NC}"
151
- echo ""
152
- echo -e "${BLUE}Next steps:${NC}"
153
- echo "1. Test the notification: ${YELLOW}claude-notify${NC}"
154
- echo "2. Add to Claude Code settings (see README.md for JSON config)"
155
- echo "3. Never alt-tab back to a unexpected Y/n again! ๐ŸŽต"
156
- echo ""
157
- echo -e "${BLUE}Configuration location:${NC}"
158
- echo "โ€ข Script: ~/.local/bin/claude-notify"
159
- echo "โ€ข Sound: ~/.local/share/sounds/claude-notification.wav"
160
- echo ""
161
- echo -e "${BLUE}Need help?${NC} Check the README.md for customization options!"