@delorenj/claude-notifications 1.1.0 → 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.
package/README.md CHANGED
@@ -20,8 +20,9 @@ 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
27
28
  - webhook **Webhook Support** - Trigger a webhook in addition to or instead of the sound
@@ -37,9 +38,15 @@ After installation, Claude Code will begin notifying you when it finishes or is
37
38
  # Trigger notification manually
38
39
  claude-notify
39
40
 
41
+ # Trigger bell notification manually
42
+ claude-notify --bell
43
+
40
44
  # Test the system
41
45
  claude-notifications test
42
46
 
47
+ # Test the bell sound
48
+ claude-notifications test-bell
49
+
43
50
  # Reinstall/repair
44
51
  claude-notifications install
45
52
 
@@ -121,6 +128,8 @@ Create a configuration file at `~/.config/claude-notifications/settings.json`.
121
128
  ```json
122
129
  {
123
130
  "sound": true,
131
+ "soundType": "claude-notification",
132
+ "desktopNotification": false,
124
133
  "webhook": {
125
134
  "enabled": true,
126
135
  "url": "https://maker.ifttt.com/trigger/claude_notification/with/key/YOUR_KEY",
@@ -131,7 +140,11 @@ Create a configuration file at `~/.config/claude-notifications/settings.json`.
131
140
 
132
141
  **Configuration Options:**
133
142
 
134
- - `sound`: (boolean) Whether to play the notification sound. Defaults to `true`.
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`.
135
148
  - `webhook.enabled`: (boolean) Whether to trigger the webhook. Defaults to `false`.
136
149
  - `webhook.url`: (string) The URL to send the POST request to.
137
150
  - `webhook.replaceSound`: (boolean) If `true`, the sound will not play when a webhook is triggered. Defaults to `false`.
@@ -144,6 +157,10 @@ The webhook will be sent as a `POST` request with a JSON payload:
144
157
  }
145
158
  ```
146
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
+
147
164
  ### Create Custom Patterns
148
165
 
149
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 {
@@ -207,6 +203,79 @@ function createSoundFile() {
207
203
  }
208
204
  }
209
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}`);
275
+ return false;
276
+ }
277
+ }
278
+
210
279
  function main() {
211
280
  const command = process.argv[2];
212
281
 
@@ -215,7 +284,7 @@ function main() {
215
284
  case undefined:
216
285
  log("blue", "🎵 Installing Claude Notifications...");
217
286
 
218
- if (createSoundFile()) {
287
+ if (createSoundFile() && createBellSoundFile()) {
219
288
  updateClaudeCodeConfig();
220
289
  log("green", "🎉 Installation complete!");
221
290
  log("blue", "🧪 Testing notification...");
@@ -256,21 +325,36 @@ function main() {
256
325
  });
257
326
  break;
258
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
+
259
335
  case "uninstall":
260
336
  log("blue", "🗑️ Uninstalling Claude Notifications...");
261
337
 
262
- const soundFile = path.join(
263
- os.homedir(),
264
- ".local",
265
- "share",
266
- "sounds",
267
- "claude-notification.wav",
268
- );
269
- if (fs.existsSync(soundFile)) {
270
- fs.unlinkSync(soundFile);
271
- 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");
272
342
  }
273
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
+
274
358
  log(
275
359
  "yellow",
276
360
  "⚠️ Please manually remove the stop hook from your Claude Code config",
@@ -289,6 +373,7 @@ function main() {
289
373
  console.log("Commands:");
290
374
  console.log(" install Install notifications (default)");
291
375
  console.log(" test Test the notification");
376
+ console.log(" test-bell Test the bell notification");
292
377
  console.log(" uninstall Remove notifications");
293
378
  console.log(" help Show this help");
294
379
  break;
@@ -6,19 +6,31 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const http = require('http');
8
8
  const https = require('https');
9
- const { getConfig } = require('../lib/config');
9
+ const { getConfig, getSoundPath, SOUND_TYPES } = require('../lib/config');
10
10
 
11
11
  const config = getConfig();
12
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');
17
+
13
18
  function playSound() {
14
19
  if (!config.sound) {
15
20
  return;
16
21
  }
17
22
 
18
- const soundFile = path.join(os.homedir(), '.local', 'share', 'sounds', 'claude-notification.wav');
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);
19
30
 
20
31
  if (!fs.existsSync(soundFile)) {
21
- process.stdout.write('\x07');
32
+ console.warn(`Sound file not found: ${soundFile}`);
33
+ process.stdout.write('\x07'); // Fallback to system beep
22
34
  return;
23
35
  }
24
36
 
@@ -97,7 +109,73 @@ function showNotification() {
97
109
  });
98
110
  }
99
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) ? '✅' : '❌'}`);
165
+ });
166
+
167
+ console.log('');
168
+ console.log('🔧 Command line args:');
169
+ console.log(` useBell: ${useBell}`);
170
+ console.log(` showConfig: ${showConfig}`);
171
+ }
172
+
100
173
  function main() {
174
+ if (showConfig) {
175
+ showConfigInfo();
176
+ return;
177
+ }
178
+
101
179
  if (config.webhook.enabled) {
102
180
  triggerWebhook();
103
181
  if (!config.webhook.replaceSound) {
@@ -105,7 +183,9 @@ function main() {
105
183
  }
106
184
  } else {
107
185
  playSound();
108
- showNotification();
186
+ if (config.desktopNotification) {
187
+ showNotification();
188
+ }
109
189
  }
110
190
  }
111
191
 
package/lib/config.js CHANGED
@@ -3,10 +3,19 @@ const path = require('path');
3
3
  const os = require('os');
4
4
 
5
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
+ };
6
13
 
7
14
  function getConfig() {
8
15
  const defaultConfig = {
9
16
  sound: true,
17
+ soundType: SOUND_TYPES.HARP, // Default to harp sound
18
+ desktopNotification: false,
10
19
  webhook: {
11
20
  enabled: false,
12
21
  url: null,
@@ -21,6 +30,13 @@ function getConfig() {
21
30
  try {
22
31
  const configContent = fs.readFileSync(configPath, 'utf-8');
23
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
+
24
40
  return { ...defaultConfig, ...userConfig };
25
41
  } catch (error) {
26
42
  console.error('Error reading or parsing config file:', error);
@@ -28,4 +44,20 @@ function getConfig() {
28
44
  }
29
45
  }
30
46
 
31
- module.exports = { getConfig };
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/claude-notifications",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Delightful Notification for Claude Code",
5
5
  "main": "index.js",
6
6
  "bin": {