@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.
- package/.github/workflows/npm-publish-github-packages.yml +36 -0
- package/README.md +53 -1
- package/bin/claude-notifications.js +164 -39
- package/bin/claude-notify.js +137 -24
- package/lib/config.js +63 -0
- package/mise.toml +3 -0
- package/package.json +1 -1
- package/ruv-swarm-mcp.db +0 -0
- package/install.sh +0 -161
|
@@ -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
|
-
|
|
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 {
|
|
@@ -136,33 +132,146 @@ function createSoundFile() {
|
|
|
136
132
|
// Generate pleasant notification scale
|
|
137
133
|
log("blue", "๐ผ Generating a pleasant notification scale...");
|
|
138
134
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"
|
|
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;
|
package/bin/claude-notify.js
CHANGED
|
@@ -1,79 +1,192 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { execSync
|
|
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
|
-
|
|
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
|
-
|
|
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'),
|
|
66
|
-
sound: false,
|
|
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
|
-
|
|
73
|
-
|
|
174
|
+
if (showConfig) {
|
|
175
|
+
showConfigInfo();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
74
178
|
|
|
75
|
-
|
|
76
|
-
|
|
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
package/package.json
CHANGED
package/ruv-swarm-mcp.db
ADDED
|
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!"
|