@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 +19 -2
- package/bin/claude-notifications.js +103 -18
- package/bin/claude-notify.js +84 -4
- package/lib/config.js +33 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
110
|
-
const soundFile =
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
"
|
|
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;
|
package/bin/claude-notify.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|