@delorenj/claude-notifications 1.0.2 โ†’ 1.2.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
@@ -20,10 +20,12 @@ That's it! ๐ŸŽ‰ The package will automatically:
20
20
  ## Features
21
21
 
22
22
  - ๐ŸŽต **Final Fantasy Dream Harp** - Classic C-D-E-G ascending/(optional)descending pattern
23
+ - ๐Ÿ”” **Service Desk Bell** - Optional short, crisp bell sound for a quick "done!" signal
23
24
  - ๐Ÿ”Š **Cross-Platform Audio** - Works on Linux and macOS
24
- - ๐Ÿ–ฅ๏ธ **Desktop Notifications** - Visual notifications with Claude Code branding
25
+ - ๐Ÿ–ฅ๏ธ **Desktop Notifications** - Visual notifications with Claude Code branding (optional)
25
26
  - ๐Ÿช **Auto-Integration** - Automatically configures Claude Code hooks
26
27
  - โšก **Zero Configuration** - Works out of the box
28
+ - webhook **Webhook Support** - Trigger a webhook in addition to or instead of the sound
27
29
  - ๐ŸŽจ **Customizable** - Easy to modify sounds and settings
28
30
 
29
31
  ## Usage
@@ -36,9 +38,15 @@ After installation, Claude Code will begin notifying you when it finishes or is
36
38
  # Trigger notification manually
37
39
  claude-notify
38
40
 
41
+ # Trigger bell notification manually
42
+ claude-notify --bell
43
+
39
44
  # Test the system
40
45
  claude-notifications test
41
46
 
47
+ # Test the bell sound
48
+ claude-notifications test-bell
49
+
42
50
  # Reinstall/repair
43
51
  claude-notifications install
44
52
 
@@ -109,6 +117,50 @@ ls ~/.local/share/sounds/claude-notification.wav
109
117
  cp your-custom-sound.wav ~/.local/share/sounds/claude-notification.wav
110
118
  ```
111
119
 
120
+ ### Configure Webhooks
121
+
122
+ 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.
123
+
124
+ Create a configuration file at `~/.config/claude-notifications/settings.json`.
125
+
126
+ **Example `settings.json`:**
127
+
128
+ ```json
129
+ {
130
+ "sound": true,
131
+ "soundType": "claude-notification",
132
+ "desktopNotification": false,
133
+ "webhook": {
134
+ "enabled": true,
135
+ "url": "https://maker.ifttt.com/trigger/claude_notification/with/key/YOUR_KEY",
136
+ "replaceSound": false
137
+ }
138
+ }
139
+ ```
140
+
141
+ **Configuration Options:**
142
+
143
+ - `sound`: (boolean) Whether to play notification sounds. Defaults to `true`.
144
+ - `soundType`: (string) Which sound to play. Available options:
145
+ - `"claude-notification"` - Final Fantasy dream harp (default)
146
+ - `"claude-notification-bell"` - Service desk bell
147
+ - `desktopNotification`: (boolean) Whether to show desktop notification banners. Defaults to `false`.
148
+ - `webhook.enabled`: (boolean) Whether to trigger the webhook. Defaults to `false`.
149
+ - `webhook.url`: (string) The URL to send the POST request to.
150
+ - `webhook.replaceSound`: (boolean) If `true`, the sound will not play when a webhook is triggered. Defaults to `false`.
151
+
152
+ The webhook will be sent as a `POST` request with a JSON payload:
153
+
154
+ ```json
155
+ {
156
+ "message": "Claude is waiting for you..."
157
+ }
158
+ ```
159
+
160
+ ### Sound Files
161
+
162
+ Sound files are stored in `~/.config/claude-notifications/sounds/` and can be customized by replacing the `.wav` files in that directory.
163
+
112
164
  ### Create Custom Patterns
113
165
 
114
166
  Use `sox` to create new victory fanfares:
@@ -4,6 +4,7 @@ const { execSync, spawn } = require("child_process");
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
6
  const os = require("os");
7
+ const { ensureSoundsDirectory, getSoundPath, SOUND_TYPES, soundsDir } = require("../lib/config");
7
8
 
8
9
  const colors = {
9
10
  red: "\x1b[31m",
@@ -106,13 +107,8 @@ function updateClaudeCodeConfig() {
106
107
  }
107
108
 
108
109
  function createSoundFile() {
109
- const soundDir = path.join(os.homedir(), ".local", "share", "sounds");
110
- const soundFile = path.join(soundDir, "claude-notification.wav");
111
-
112
- // Create directory if it doesn't exist
113
- if (!fs.existsSync(soundDir)) {
114
- fs.mkdirSync(soundDir, { recursive: true });
115
- }
110
+ ensureSoundsDirectory();
111
+ const soundFile = getSoundPath(SOUND_TYPES.HARP);
116
112
 
117
113
  // Check if sox is available
118
114
  try {
@@ -136,33 +132,146 @@ function createSoundFile() {
136
132
  // Generate pleasant notification scale
137
133
  log("blue", "๐ŸŽผ Generating a pleasant notification scale...");
138
134
 
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";
135
+ // Create individual note files first (safer approach)
136
+ const tempDir = path.join(os.tmpdir(), "claude-notifications");
137
+ if (!fs.existsSync(tempDir)) {
138
+ fs.mkdirSync(tempDir, { recursive: true });
139
+ }
140
+
141
+ const notes = [
142
+ { freq: 523.25, name: "C5" }, // C5
143
+ { freq: 587.33, name: "D5" }, // D5
144
+ { freq: 659.25, name: "E5" }, // E5
145
+ { freq: 783.99, name: "G5" }, // G5
146
+ { freq: 1046.5, name: "C6" }, // C6
147
+ { freq: 1174.66, name: "D6" }, // D6
148
+ { freq: 1318.51, name: "E6" }, // E6
149
+ { freq: 1567.98, name: "G6" }, // G6
150
+ { freq: 2093.0, name: "C7" }, // C7
151
+ ];
159
152
 
160
153
  try {
161
- execSync(soxCommand, { stdio: "ignore" });
154
+ const noteFiles = [];
155
+
156
+ // Generate each note individually (much safer)
157
+ for (let i = 0; i < notes.length; i++) {
158
+ const noteFile = path.join(tempDir, `note_${i}.wav`);
159
+ const noteCommand = `sox -n "${noteFile}" synth 0.08 sine ${notes[i].freq} fade 0.01 0.08 0.01 vol 0.7`;
160
+ execSync(noteCommand, { stdio: "ignore", timeout: 5000 }); // 5 second timeout per note
161
+ noteFiles.push(noteFile);
162
+ }
163
+
164
+ // Concatenate all notes into final file
165
+ const concatCommand = `sox ${noteFiles.map((f) => `"${f}"`).join(" ")} "${soundFile}"`;
166
+ execSync(concatCommand, { stdio: "ignore", timeout: 10000 }); // 10 second timeout for concat
167
+
168
+ // Clean up temp files
169
+ noteFiles.forEach((file) => {
170
+ if (fs.existsSync(file)) {
171
+ fs.unlinkSync(file);
172
+ }
173
+ });
174
+
175
+ // Remove temp directory if empty
176
+ try {
177
+ fs.rmdirSync(tempDir);
178
+ } catch (e) {
179
+ // Directory might not be empty, that's ok
180
+ }
181
+
162
182
  log("green", "โœ… Sound file created successfully!");
163
183
  return true;
164
184
  } catch (error) {
165
185
  log("red", `โŒ Error creating sound file: ${error.message}`);
186
+
187
+ // Clean up any temp files on error
188
+ try {
189
+ const tempFiles = fs
190
+ .readdirSync(tempDir)
191
+ .filter((f) => f.startsWith("note_"));
192
+ tempFiles.forEach((file) => {
193
+ const filePath = path.join(tempDir, file);
194
+ if (fs.existsSync(filePath)) {
195
+ fs.unlinkSync(filePath);
196
+ }
197
+ });
198
+ } catch (cleanupError) {
199
+ // Ignore cleanup errors
200
+ }
201
+
202
+ return false;
203
+ }
204
+ }
205
+
206
+ function generateBellSoxCommand(outputFile) {
207
+ // Bell sound parameters - adjust these to customize the bell
208
+ const bellParams = {
209
+ // Base tone generation
210
+ duration: 0.1, // Length of the initial bell strike (seconds)
211
+ frequency: 1600, // Pitch of the bell (Hz) - higher = more "ting", lower = more "dong"
212
+
213
+ // Fade envelope
214
+ fadeIn: 0, // Fade in time (seconds) - 0 for immediate attack
215
+ fadeDuration: 0.1, // Total fade duration (seconds)
216
+ fadeOut: 0.05, // Fade out time (seconds) - creates the bell decay
217
+
218
+ // Volume
219
+ volume: 0.9, // Master volume (0.0 to 1.0)
220
+
221
+ // Echo effect parameters (creates the "ringing" quality)
222
+ echoGain: 0.5, // Overall echo volume (0.0 to 1.0)
223
+ echoDecay: 0.5, // How quickly echoes fade (0.0 to 1.0)
224
+
225
+ // Individual echo delays and volumes (milliseconds, volume)
226
+ echo1: { delay: 250, volume: 0.2 }, // First echo - quarter second delay
227
+ echo2: { delay: 500, volume: 0.05 }, // Second echo - half second delay
228
+ echo3: { delay: 750, volume: 0.01 }, // Third echo - three quarter second delay
229
+
230
+ // Reverb parameters (adds spatial depth)
231
+ reverb: {
232
+ roomSize: 40, // Room size percentage (0-100) - larger = more spacious
233
+ preDelay: 65, // Pre-delay in ms - time before reverb starts
234
+ reverbTime: 100, // Reverb decay time percentage (0-100)
235
+ wetGain: 100, // Wet signal gain percentage (0-100) - reverb volume
236
+ dryGain: 12, // Dry signal gain percentage (0-100) - original signal volume
237
+ stereoDepth: 0, // Stereo depth (0-100) - 0 = mono, higher = wider stereo
238
+ },
239
+ };
240
+
241
+ // Build the sox command with clear parameter mapping
242
+ const command = [
243
+ "sox -n", // Generate from nothing (null input)
244
+ `"${outputFile}"`, // Output file
245
+ `synth ${bellParams.duration} sine ${bellParams.frequency}`, // Generate sine wave
246
+ `fade ${bellParams.fadeIn} ${bellParams.fadeDuration} ${bellParams.fadeOut}`, // Apply fade envelope
247
+ `vol ${bellParams.volume}`, // Set volume
248
+ `echos ${bellParams.echoGain} ${bellParams.echoDecay}`, // Echo effect base settings
249
+ `${bellParams.echo1.delay} ${bellParams.echo1.volume}`, // Echo 1: 250ms delay, 0.2 volume
250
+ `${bellParams.echo2.delay} ${bellParams.echo2.volume}`, // Echo 2: 500ms delay, 0.1 volume
251
+ `${bellParams.echo3.delay} ${bellParams.echo3.volume}`, // Echo 3: 750ms delay, 0.05 volume
252
+ `reverb ${bellParams.reverb.roomSize} ${bellParams.reverb.preDelay}`, // Reverb room & pre-delay
253
+ `${bellParams.reverb.reverbTime} ${bellParams.reverb.wetGain}`, // Reverb time & wet gain
254
+ `${bellParams.reverb.dryGain} ${bellParams.reverb.stereoDepth}`, // Dry gain & stereo depth
255
+ ].join(" ");
256
+
257
+ return command;
258
+ }
259
+
260
+ function createBellSoundFile() {
261
+ ensureSoundsDirectory();
262
+ const soundFile = getSoundPath(SOUND_TYPES.BELL);
263
+
264
+ log("blue", "๐Ÿ”” Generating service desk bell sound...");
265
+
266
+ try {
267
+ // Generate the bell sound using our documented sox command builder
268
+ const bellCommand = generateBellSoxCommand(soundFile);
269
+ execSync(bellCommand, { stdio: "ignore", timeout: 5000 });
270
+
271
+ log("green", "โœ… Bell sound file created successfully!");
272
+ return true;
273
+ } catch (error) {
274
+ log("red", `โŒ Error creating bell sound file: ${error.message}`);
166
275
  return false;
167
276
  }
168
277
  }
@@ -175,7 +284,7 @@ function main() {
175
284
  case undefined:
176
285
  log("blue", "๐ŸŽต Installing Claude Notifications...");
177
286
 
178
- if (createSoundFile()) {
287
+ if (createSoundFile() && createBellSoundFile()) {
179
288
  updateClaudeCodeConfig();
180
289
  log("green", "๐ŸŽ‰ Installation complete!");
181
290
  log("blue", "๐Ÿงช Testing notification...");
@@ -216,21 +325,36 @@ function main() {
216
325
  });
217
326
  break;
218
327
 
328
+ case "test-bell":
329
+ log("blue", "๐Ÿ”” Testing bell notification...");
330
+ spawn("node", [path.join(__dirname, "claude-notify.js"), "--bell"], {
331
+ stdio: "inherit",
332
+ });
333
+ break;
334
+
219
335
  case "uninstall":
220
336
  log("blue", "๐Ÿ—‘๏ธ Uninstalling Claude Notifications...");
221
337
 
222
- const soundFile = path.join(
223
- os.homedir(),
224
- ".local",
225
- "share",
226
- "sounds",
227
- "claude-notification.wav",
228
- );
229
- if (fs.existsSync(soundFile)) {
230
- fs.unlinkSync(soundFile);
231
- log("green", "โœ… Removed sound file");
338
+ // Remove sounds directory
339
+ if (fs.existsSync(soundsDir)) {
340
+ fs.rmSync(soundsDir, { recursive: true, force: true });
341
+ log("green", "โœ… Removed sounds directory");
232
342
  }
233
343
 
344
+ // Also clean up old sound files if they exist
345
+ const oldSoundDir = path.join(os.homedir(), ".local", "share", "sounds");
346
+ const oldSoundFiles = [
347
+ path.join(oldSoundDir, "claude-notification.wav"),
348
+ path.join(oldSoundDir, "claude-notification-bell.wav")
349
+ ];
350
+
351
+ oldSoundFiles.forEach(file => {
352
+ if (fs.existsSync(file)) {
353
+ fs.unlinkSync(file);
354
+ log("green", `โœ… Removed old sound file: ${path.basename(file)}`);
355
+ }
356
+ });
357
+
234
358
  log(
235
359
  "yellow",
236
360
  "โš ๏ธ Please manually remove the stop hook from your Claude Code config",
@@ -249,6 +373,7 @@ function main() {
249
373
  console.log("Commands:");
250
374
  console.log(" install Install notifications (default)");
251
375
  console.log(" test Test the notification");
376
+ console.log(" test-bell Test the bell notification");
252
377
  console.log(" uninstall Remove notifications");
253
378
  console.log(" help Show this help");
254
379
  break;
@@ -1,79 +1,192 @@
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, getSoundPath, SOUND_TYPES } = require('../lib/config');
10
+
11
+ const config = getConfig();
12
+
13
+ // Check for command line arguments
14
+ const args = process.argv.slice(2);
15
+ const useBell = args.includes('--bell') || args.includes('-b');
16
+ const showConfig = args.includes('-c') || args.includes('--config');
7
17
 
8
18
  function playSound() {
9
- const soundFile = path.join(os.homedir(), '.local', 'share', 'sounds', 'claude-notification.wav');
19
+ if (!config.sound) {
20
+ return;
21
+ }
22
+
23
+ // Determine which sound to play
24
+ let soundType = config.soundType;
25
+ if (useBell) {
26
+ soundType = SOUND_TYPES.BELL;
27
+ }
28
+
29
+ const soundFile = getSoundPath(soundType);
10
30
 
11
31
  if (!fs.existsSync(soundFile)) {
12
- // Fallback to system bell
13
- process.stdout.write('\x07');
32
+ console.warn(`Sound file not found: ${soundFile}`);
33
+ process.stdout.write('\x07'); // Fallback to system beep
14
34
  return;
15
35
  }
16
36
 
17
37
  try {
18
- // Try different audio players based on platform
19
38
  if (process.platform === 'linux') {
20
- // Try paplay first (PulseAudio)
21
39
  try {
22
40
  execSync(`paplay "${soundFile}"`, { stdio: 'ignore' });
23
- return;
24
41
  } catch (e) {
25
- // Try aplay (ALSA)
26
42
  try {
27
43
  execSync(`aplay "${soundFile}"`, { stdio: 'ignore' });
28
- return;
29
44
  } catch (e2) {
30
- // Try play (sox)
31
45
  try {
32
46
  execSync(`play "${soundFile}"`, { stdio: 'ignore' });
33
- return;
34
47
  } catch (e3) {
35
- // Fallback to system bell
36
48
  process.stdout.write('\x07');
37
49
  }
38
50
  }
39
51
  }
40
52
  } else if (process.platform === 'darwin') {
41
- // macOS
42
53
  try {
43
54
  execSync(`afplay "${soundFile}"`, { stdio: 'ignore' });
44
- return;
45
55
  } catch (e) {
46
- // Fallback to system bell
47
56
  process.stdout.write('\x07');
48
57
  }
49
58
  } else {
50
- // Other platforms - just system bell
51
59
  process.stdout.write('\x07');
52
60
  }
53
61
  } catch (error) {
54
- // Final fallback
55
62
  process.stdout.write('\x07');
56
63
  }
57
64
  }
58
65
 
66
+ function triggerWebhook() {
67
+ if (!config.webhook || !config.webhook.enabled || !config.webhook.url) {
68
+ return;
69
+ }
70
+
71
+ const { url } = config.webhook;
72
+ const data = JSON.stringify({ message: 'Claude is waiting for you...' });
73
+
74
+ const protocol = url.startsWith('https') ? https : http;
75
+
76
+ const options = {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ 'Content-Length': data.length
81
+ }
82
+ };
83
+
84
+ const req = protocol.request(url, options, (res) => {
85
+ // We don't really care about the response, but it's good practice to handle it
86
+ res.on('data', () => {});
87
+ res.on('end', () => {});
88
+ });
89
+
90
+ req.on('error', (error) => {
91
+ console.error('Error triggering webhook:', error);
92
+ });
93
+
94
+ req.write(data);
95
+ req.end();
96
+ }
97
+
59
98
  function showNotification() {
60
99
  const notifier = require('node-notifier');
61
100
 
62
101
  notifier.notify({
63
102
  title: 'Claude Code',
64
103
  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'
104
+ icon: path.join(os.homedir(), 'Pictures', 'claude.png'),
105
+ sound: false,
106
+ urgency: 'critical',
107
+ id: 'claude-code-notification',
108
+ replaceId: 'claude-code-notification'
109
+ });
110
+ }
111
+
112
+ function showConfigInfo() {
113
+ const configPath = path.join(os.homedir(), '.config', 'claude-notifications', 'settings.json');
114
+ const soundsDir = path.join(os.homedir(), '.config', 'claude-notifications', 'sounds');
115
+
116
+ console.log('๐Ÿ” Claude Notifications Config Debug Info:');
117
+ console.log('');
118
+ console.log('๐Ÿ“ Config file location:');
119
+ console.log(` ${configPath}`);
120
+ console.log(` Exists: ${fs.existsSync(configPath) ? 'โœ…' : 'โŒ'}`);
121
+
122
+ if (fs.existsSync(configPath)) {
123
+ try {
124
+ const configContent = fs.readFileSync(configPath, 'utf-8');
125
+ console.log(' Content:');
126
+ console.log(` ${configContent.split('\n').map(line => ` ${line}`).join('\n')}`);
127
+ } catch (error) {
128
+ console.log(` Error reading: ${error.message}`);
129
+ }
130
+ }
131
+
132
+ console.log('');
133
+ console.log('๐Ÿ”Š Sounds directory:');
134
+ console.log(` ${soundsDir}`);
135
+ console.log(` Exists: ${fs.existsSync(soundsDir) ? 'โœ…' : 'โŒ'}`);
136
+
137
+ if (fs.existsSync(soundsDir)) {
138
+ try {
139
+ const soundFiles = fs.readdirSync(soundsDir);
140
+ console.log(' Files:');
141
+ soundFiles.forEach(file => {
142
+ const filePath = path.join(soundsDir, file);
143
+ const stats = fs.statSync(filePath);
144
+ console.log(` - ${file} (${Math.round(stats.size / 1024)}KB)`);
145
+ });
146
+ } catch (error) {
147
+ console.log(` Error reading directory: ${error.message}`);
148
+ }
149
+ }
150
+
151
+ console.log('');
152
+ console.log('โš™๏ธ Current config values:');
153
+ console.log(` sound: ${config.sound}`);
154
+ console.log(` soundType: ${config.soundType}`);
155
+ console.log(` desktopNotification: ${config.desktopNotification}`);
156
+ console.log(` webhook.enabled: ${config.webhook.enabled}`);
157
+
158
+ console.log('');
159
+ console.log('๐ŸŽต Sound file paths:');
160
+ const { SOUND_TYPES, getSoundPath } = require('../lib/config');
161
+ Object.values(SOUND_TYPES).forEach(soundType => {
162
+ const soundPath = getSoundPath(soundType);
163
+ console.log(` ${soundType}: ${soundPath}`);
164
+ console.log(` Exists: ${fs.existsSync(soundPath) ? 'โœ…' : 'โŒ'}`);
68
165
  });
166
+
167
+ console.log('');
168
+ console.log('๐Ÿ”ง Command line args:');
169
+ console.log(` useBell: ${useBell}`);
170
+ console.log(` showConfig: ${showConfig}`);
69
171
  }
70
172
 
71
173
  function main() {
72
- // Play sound in background
73
- playSound();
174
+ if (showConfig) {
175
+ showConfigInfo();
176
+ return;
177
+ }
74
178
 
75
- // Show desktop notification
76
- showNotification();
179
+ if (config.webhook.enabled) {
180
+ triggerWebhook();
181
+ if (!config.webhook.replaceSound) {
182
+ playSound();
183
+ }
184
+ } else {
185
+ playSound();
186
+ if (config.desktopNotification) {
187
+ showNotification();
188
+ }
189
+ }
77
190
  }
78
191
 
79
192
  if (require.main === module) {
package/lib/config.js ADDED
@@ -0,0 +1,63 @@
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
+ const soundsDir = path.join(os.homedir(), '.config', 'claude-notifications', 'sounds');
7
+
8
+ // Available sound types (filenames without extension)
9
+ const SOUND_TYPES = {
10
+ HARP: 'claude-notification',
11
+ BELL: 'claude-notification-bell'
12
+ };
13
+
14
+ function getConfig() {
15
+ const defaultConfig = {
16
+ sound: true,
17
+ soundType: SOUND_TYPES.HARP, // Default to harp sound
18
+ desktopNotification: false,
19
+ webhook: {
20
+ enabled: false,
21
+ url: null,
22
+ replaceSound: false
23
+ }
24
+ };
25
+
26
+ if (!fs.existsSync(configPath)) {
27
+ return defaultConfig;
28
+ }
29
+
30
+ try {
31
+ const configContent = fs.readFileSync(configPath, 'utf-8');
32
+ const userConfig = JSON.parse(configContent);
33
+
34
+ // Handle migration from old 'secondSound' config
35
+ if (userConfig.secondSound === true && !userConfig.soundType) {
36
+ userConfig.soundType = SOUND_TYPES.BELL;
37
+ delete userConfig.secondSound;
38
+ }
39
+
40
+ return { ...defaultConfig, ...userConfig };
41
+ } catch (error) {
42
+ console.error('Error reading or parsing config file:', error);
43
+ return defaultConfig;
44
+ }
45
+ }
46
+
47
+ function getSoundPath(soundType) {
48
+ return path.join(soundsDir, `${soundType}.wav`);
49
+ }
50
+
51
+ function ensureSoundsDirectory() {
52
+ if (!fs.existsSync(soundsDir)) {
53
+ fs.mkdirSync(soundsDir, { recursive: true });
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ getConfig,
59
+ getSoundPath,
60
+ ensureSoundsDirectory,
61
+ SOUND_TYPES,
62
+ soundsDir
63
+ };
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.2",
3
+ "version": "1.2.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!"