claude-code-sounds 1.5.4 → 1.6.1
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 +77 -49
- package/bin/cli.js +31 -6
- package/bin/lib.js +73 -14
- package/hooks/play-sound.sh +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,18 +39,6 @@ The bash installer requires `jq` (`brew install jq`).
|
|
|
39
39
|
|
|
40
40
|
</details>
|
|
41
41
|
|
|
42
|
-
## Usage
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
npx claude-code-sounds # Interactive install
|
|
46
|
-
npx claude-code-sounds --theme portal # Install a specific theme directly
|
|
47
|
-
npx claude-code-sounds --mix # Jump to sound assignment grid
|
|
48
|
-
npx claude-code-sounds --yes # Install defaults, skip all prompts
|
|
49
|
-
npx claude-code-sounds --list # List available themes
|
|
50
|
-
npx claude-code-sounds --uninstall # Remove all sounds and hooks
|
|
51
|
-
npx claude-code-sounds --help # Show help
|
|
52
|
-
```
|
|
53
|
-
|
|
54
42
|
## Themes
|
|
55
43
|
|
|
56
44
|
| Theme | Sounds | Vibe |
|
|
@@ -73,7 +61,78 @@ npx claude-code-sounds --help # Show help
|
|
|
73
61
|
|
|
74
62
|
Each theme maps sounds across all 11 Claude Code lifecycle events.
|
|
75
63
|
|
|
76
|
-
##
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx claude-code-sounds # Interactive install
|
|
68
|
+
npx claude-code-sounds --theme portal # Install a specific theme directly
|
|
69
|
+
npx claude-code-sounds --mix # Jump to sound assignment grid
|
|
70
|
+
npx claude-code-sounds --yes # Install defaults, skip all prompts
|
|
71
|
+
npx claude-code-sounds --list # List available themes
|
|
72
|
+
npx claude-code-sounds --mute # Mute all sounds
|
|
73
|
+
npx claude-code-sounds --unmute # Unmute all sounds
|
|
74
|
+
npx claude-code-sounds --dnd # Auto-mute when in video calls
|
|
75
|
+
npx claude-code-sounds --no-dnd # Disable auto-mute
|
|
76
|
+
npx claude-code-sounds --uninstall # Remove all sounds and hooks
|
|
77
|
+
npx claude-code-sounds --help # Show help
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Muting
|
|
81
|
+
|
|
82
|
+
Mute sounds without uninstalling — three ways:
|
|
83
|
+
|
|
84
|
+
- **Slash command** (inside Claude Code): type `/mute` or `/unmute`
|
|
85
|
+
- **CLI flag**: `npx claude-code-sounds --mute` or `--unmute`
|
|
86
|
+
- **Interactive menu**: run `npx claude-code-sounds` and select "Mute sounds" / "Unmute sounds"
|
|
87
|
+
|
|
88
|
+
Muting creates a sentinel file at `~/.claude/sounds/.muted`. The hook script checks for it and exits immediately, so there's zero overhead when muted.
|
|
89
|
+
|
|
90
|
+
### Do Not Disturb
|
|
91
|
+
|
|
92
|
+
Sounds are automatically muted when active video calls are detected (Zoom, FaceTime, Webex). This is enabled by default.
|
|
93
|
+
|
|
94
|
+
Edit `~/.claude/sounds/.dnd` to add or remove app names — one process name per line, `#` for comments.
|
|
95
|
+
|
|
96
|
+
To disable: `npx claude-code-sounds --no-dnd` or use the interactive menu. Re-enable with `--dnd`.
|
|
97
|
+
|
|
98
|
+
## Customizing
|
|
99
|
+
|
|
100
|
+
Re-run with `--mix` to open the sound assignment grid, where you can reassign sounds to hooks, add themes, or preview clips:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx claude-code-sounds --mix
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+

|
|
107
|
+
|
|
108
|
+
You can also drop any `.wav` or `.mp3` into the sound directories manually:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
~/.claude/sounds/
|
|
112
|
+
├── start/ # add files here for session start
|
|
113
|
+
├── stop/ # add files here for response complete
|
|
114
|
+
├── error/ # add files here for failures
|
|
115
|
+
└── ...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The script picks randomly from whatever files are in each directory.
|
|
119
|
+
|
|
120
|
+
## Uninstalling
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx claude-code-sounds --uninstall
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This removes all sound files, the hook script, and the hooks config from `settings.json`.
|
|
127
|
+
|
|
128
|
+
<details>
|
|
129
|
+
<summary><h2 style="display:inline">How It Works</h2></summary>
|
|
130
|
+
|
|
131
|
+
A single script (`~/.claude/hooks/play-sound.sh`) handles all events. It takes a category name as an argument, picks a random `.wav` or `.mp3` from `~/.claude/sounds/<category>/`, and plays it with `afplay`.
|
|
132
|
+
|
|
133
|
+
Hooks are configured in `~/.claude/settings.json` — each Claude Code lifecycle event calls the script with the appropriate category.
|
|
134
|
+
|
|
135
|
+
### Hook Events
|
|
77
136
|
|
|
78
137
|
| Event | Hook | When |
|
|
79
138
|
|---|---|---|
|
|
@@ -89,7 +148,10 @@ Each theme maps sounds across all 11 Claude Code lifecycle events.
|
|
|
89
148
|
| `compact` | `PreCompact` | Context compaction |
|
|
90
149
|
| `teammate-idle` | `TeammateIdle` | Teammate went idle |
|
|
91
150
|
|
|
92
|
-
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary><h2 style="display:inline">Creating a Theme</h2></summary>
|
|
93
155
|
|
|
94
156
|
Themes live in `themes/<name>/` with two items:
|
|
95
157
|
|
|
@@ -116,41 +178,7 @@ Defines metadata and maps sound files to hook categories:
|
|
|
116
178
|
|
|
117
179
|
Place audio files (`.wav` or `.mp3`) in `themes/<name>/sounds/` with filenames matching the `name` field in `theme.json`.
|
|
118
180
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
A single script (`~/.claude/hooks/play-sound.sh`) handles all events. It takes a category name as an argument, picks a random `.wav` or `.mp3` from `~/.claude/sounds/<category>/`, and plays it with `afplay`.
|
|
122
|
-
|
|
123
|
-
Hooks are configured in `~/.claude/settings.json` — each Claude Code lifecycle event calls the script with the appropriate category.
|
|
124
|
-
|
|
125
|
-
## Customizing
|
|
126
|
-
|
|
127
|
-
Re-run with `--mix` to open the sound assignment grid, where you can reassign sounds to hooks, add themes, or preview clips:
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
npx claude-code-sounds --mix
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-

|
|
134
|
-
|
|
135
|
-
You can also drop any `.wav` or `.mp3` into the sound directories manually:
|
|
136
|
-
|
|
137
|
-
```
|
|
138
|
-
~/.claude/sounds/
|
|
139
|
-
├── start/ # add files here for session start
|
|
140
|
-
├── stop/ # add files here for response complete
|
|
141
|
-
├── error/ # add files here for failures
|
|
142
|
-
└── ...
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
The script picks randomly from whatever files are in each directory.
|
|
146
|
-
|
|
147
|
-
## Uninstalling
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
npx claude-code-sounds --uninstall
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
This removes all sound files, the hook script, and the hooks config from `settings.json`.
|
|
181
|
+
</details>
|
|
154
182
|
|
|
155
183
|
## Disclaimer
|
|
156
184
|
|
package/bin/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const {
|
|
5
|
+
const { execFileSync, spawn } = require("child_process");
|
|
6
6
|
const p = require("@clack/prompts");
|
|
7
7
|
const { Prompt } = require("@clack/core");
|
|
8
8
|
const color = require("picocolors");
|
|
@@ -25,13 +25,9 @@ const installHooksConfig = () => lib.installHooksConfig(paths);
|
|
|
25
25
|
|
|
26
26
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
function exec(cmd, opts = {}) {
|
|
29
|
-
return execSync(cmd, { encoding: "utf-8", stdio: "pipe", ...opts });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
function hasCommand(name) {
|
|
33
29
|
try {
|
|
34
|
-
|
|
30
|
+
execFileSync("which", [name], { stdio: "pipe" });
|
|
35
31
|
return true;
|
|
36
32
|
} catch {
|
|
37
33
|
return false;
|
|
@@ -381,6 +377,8 @@ function showHelp() {
|
|
|
381
377
|
npx claude-code-sounds --list List available themes
|
|
382
378
|
npx claude-code-sounds --mute Mute all sounds
|
|
383
379
|
npx claude-code-sounds --unmute Unmute all sounds
|
|
380
|
+
npx claude-code-sounds --dnd Auto-mute when in video calls
|
|
381
|
+
npx claude-code-sounds --no-dnd Disable auto-mute
|
|
384
382
|
npx claude-code-sounds --uninstall Remove all sounds and hooks
|
|
385
383
|
npx claude-code-sounds --help Show this help
|
|
386
384
|
|
|
@@ -391,6 +389,8 @@ function showHelp() {
|
|
|
391
389
|
-l, --list List available themes
|
|
392
390
|
--mute Mute all sounds
|
|
393
391
|
--unmute Unmute all sounds
|
|
392
|
+
--dnd Auto-mute when in video calls
|
|
393
|
+
--no-dnd Disable auto-mute
|
|
394
394
|
-h, --help Show this help
|
|
395
395
|
`);
|
|
396
396
|
}
|
|
@@ -636,11 +636,17 @@ async function interactiveInstall(autoYes) {
|
|
|
636
636
|
? { value: "unmute", label: "Unmute sounds", hint: "Sounds are currently muted" }
|
|
637
637
|
: { value: "mute", label: "Mute sounds", hint: "Silence sounds without uninstalling" };
|
|
638
638
|
|
|
639
|
+
const dnd = lib.isDnd(paths);
|
|
640
|
+
const dndOption = dnd
|
|
641
|
+
? { value: "dnd-off", label: "Disable Do Not Disturb", hint: "Stop auto-muting during calls" }
|
|
642
|
+
: { value: "dnd-on", label: "Enable Do Not Disturb", hint: "Auto-mute during video calls" };
|
|
643
|
+
|
|
639
644
|
const action = await p.select({
|
|
640
645
|
message: "What would you like to do?",
|
|
641
646
|
options: [
|
|
642
647
|
{ value: "modify", label: "Modify install", hint: "Add themes, change sounds" },
|
|
643
648
|
muteOption,
|
|
649
|
+
dndOption,
|
|
644
650
|
{ value: "fresh", label: "Fresh install", hint: "Start over from scratch" },
|
|
645
651
|
{ value: "uninstall", label: "Uninstall", hint: "Remove all sounds and hooks" },
|
|
646
652
|
],
|
|
@@ -663,6 +669,18 @@ async function interactiveInstall(autoYes) {
|
|
|
663
669
|
return;
|
|
664
670
|
}
|
|
665
671
|
|
|
672
|
+
if (action === "dnd-on") {
|
|
673
|
+
lib.setDnd(true, paths);
|
|
674
|
+
p.outro("Do Not Disturb enabled. Edit ~/.claude/sounds/.dnd to customize.");
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (action === "dnd-off") {
|
|
679
|
+
lib.setDnd(false, paths);
|
|
680
|
+
p.outro("Do Not Disturb disabled.");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
666
684
|
if (action === "uninstall") {
|
|
667
685
|
uninstallAll();
|
|
668
686
|
p.outro("All sounds removed.");
|
|
@@ -839,6 +857,13 @@ if (flags.has("--help") || flags.has("-h")) {
|
|
|
839
857
|
} else if (flags.has("--unmute")) {
|
|
840
858
|
lib.setMuted(false, paths);
|
|
841
859
|
console.log(" Sounds unmuted.");
|
|
860
|
+
} else if (flags.has("--dnd")) {
|
|
861
|
+
lib.setDnd(true, paths);
|
|
862
|
+
console.log(" Do Not Disturb enabled. Sounds auto-mute when video call apps are detected.");
|
|
863
|
+
console.log(" Edit ~/.claude/sounds/.dnd to customize the app list.");
|
|
864
|
+
} else if (flags.has("--no-dnd")) {
|
|
865
|
+
lib.setDnd(false, paths);
|
|
866
|
+
console.log(" Do Not Disturb disabled.");
|
|
842
867
|
} else if (flags.has("--uninstall") || flags.has("--remove")) {
|
|
843
868
|
p.intro(color.bold("claude-code-sounds"));
|
|
844
869
|
uninstallAll();
|
package/bin/lib.js
CHANGED
|
@@ -67,7 +67,12 @@ function listThemes(paths) {
|
|
|
67
67
|
for (const name of fs.readdirSync(paths.THEMES_DIR)) {
|
|
68
68
|
const themeJson = path.join(paths.THEMES_DIR, name, "theme.json");
|
|
69
69
|
if (!fs.existsSync(themeJson)) continue;
|
|
70
|
-
|
|
70
|
+
let meta;
|
|
71
|
+
try {
|
|
72
|
+
meta = JSON.parse(fs.readFileSync(themeJson, "utf-8"));
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
71
76
|
let soundCount = 0;
|
|
72
77
|
if (meta.sounds) {
|
|
73
78
|
for (const cat of Object.values(meta.sounds)) {
|
|
@@ -97,26 +102,34 @@ function resolveThemeSoundPath(themeName, fileName, paths) {
|
|
|
97
102
|
|
|
98
103
|
function readSettings(paths) {
|
|
99
104
|
if (fs.existsSync(paths.SETTINGS_PATH)) {
|
|
100
|
-
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(fs.readFileSync(paths.SETTINGS_PATH, "utf-8"));
|
|
107
|
+
} catch {}
|
|
101
108
|
}
|
|
102
109
|
return {};
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
function writeSettings(settings, paths) {
|
|
106
113
|
mkdirp(paths.CLAUDE_DIR);
|
|
107
|
-
|
|
114
|
+
const tmp = paths.SETTINGS_PATH + ".tmp";
|
|
115
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
116
|
+
fs.renameSync(tmp, paths.SETTINGS_PATH);
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
function readInstalled(paths) {
|
|
111
120
|
if (fs.existsSync(paths.INSTALLED_PATH)) {
|
|
112
|
-
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(fs.readFileSync(paths.INSTALLED_PATH, "utf-8"));
|
|
123
|
+
} catch {}
|
|
113
124
|
}
|
|
114
125
|
return null;
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
function writeInstalled(data, paths) {
|
|
118
129
|
mkdirp(paths.SOUNDS_DIR);
|
|
119
|
-
|
|
130
|
+
const tmp = paths.INSTALLED_PATH + ".tmp";
|
|
131
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
132
|
+
fs.renameSync(tmp, paths.INSTALLED_PATH);
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
function isMuted(paths) {
|
|
@@ -133,6 +146,41 @@ function setMuted(muted, paths) {
|
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
const DND_DEFAULTS = [
|
|
150
|
+
"# Auto-mute during active calls",
|
|
151
|
+
"# One process name per line, # for comments",
|
|
152
|
+
"# Tip: use meeting-specific processes, not main apps (they stay open after calls)",
|
|
153
|
+
"# Find process names with: pgrep -la <app>",
|
|
154
|
+
"CptHost",
|
|
155
|
+
"FaceTime",
|
|
156
|
+
"Webex",
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
function isDnd(paths) {
|
|
160
|
+
return fs.existsSync(path.join(paths.SOUNDS_DIR, ".dnd"));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function setDnd(enabled, paths) {
|
|
164
|
+
const dndPath = path.join(paths.SOUNDS_DIR, ".dnd");
|
|
165
|
+
if (enabled) {
|
|
166
|
+
mkdirp(paths.SOUNDS_DIR);
|
|
167
|
+
fs.writeFileSync(dndPath, DND_DEFAULTS.join("\n") + "\n");
|
|
168
|
+
// Ensure the installed hook script supports DND
|
|
169
|
+
_updateHookScript(paths);
|
|
170
|
+
} else if (fs.existsSync(dndPath)) {
|
|
171
|
+
fs.unlinkSync(dndPath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function _updateHookScript(paths) {
|
|
176
|
+
const hookSrc = path.join(paths.PKG_DIR, "hooks", "play-sound.sh");
|
|
177
|
+
const hookDest = path.join(paths.HOOKS_DIR, "play-sound.sh");
|
|
178
|
+
if (fs.existsSync(hookDest) && fs.existsSync(hookSrc)) {
|
|
179
|
+
fs.copyFileSync(hookSrc, hookDest);
|
|
180
|
+
fs.chmodSync(hookDest, 0o755);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
136
184
|
// ─── Detect Existing Install ─────────────────────────────────────────────────
|
|
137
185
|
|
|
138
186
|
function detectExistingInstall(paths) {
|
|
@@ -186,23 +234,26 @@ function installSounds(selections, paths) {
|
|
|
186
234
|
const catDir = path.join(paths.SOUNDS_DIR, cat);
|
|
187
235
|
mkdirp(catDir);
|
|
188
236
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (f.endsWith(".wav") || f.endsWith(".mp3")) {
|
|
192
|
-
fs.unlinkSync(path.join(catDir, f));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} catch {}
|
|
196
|
-
|
|
237
|
+
// Copy new files first
|
|
238
|
+
const newFiles = new Set();
|
|
197
239
|
for (const item of items) {
|
|
198
240
|
const srcPath = resolveThemeSoundPath(item.themeName, item.fileName, paths);
|
|
199
241
|
const destPath = path.join(catDir, item.fileName);
|
|
200
|
-
|
|
201
242
|
if (fs.existsSync(srcPath)) {
|
|
202
243
|
fs.copyFileSync(srcPath, destPath);
|
|
203
244
|
total++;
|
|
204
245
|
}
|
|
246
|
+
newFiles.add(item.fileName);
|
|
205
247
|
}
|
|
248
|
+
|
|
249
|
+
// Then remove old files not in the new set
|
|
250
|
+
try {
|
|
251
|
+
for (const f of fs.readdirSync(catDir)) {
|
|
252
|
+
if ((f.endsWith(".wav") || f.endsWith(".mp3")) && !newFiles.has(f)) {
|
|
253
|
+
fs.unlinkSync(path.join(catDir, f));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch {}
|
|
206
257
|
}
|
|
207
258
|
|
|
208
259
|
return total;
|
|
@@ -230,6 +281,11 @@ function installHooksConfig(paths) {
|
|
|
230
281
|
}
|
|
231
282
|
}
|
|
232
283
|
}
|
|
284
|
+
|
|
285
|
+
// Enable Do Not Disturb by default (auto-mute during video calls)
|
|
286
|
+
if (!isDnd(paths)) {
|
|
287
|
+
setDnd(true, paths);
|
|
288
|
+
}
|
|
233
289
|
}
|
|
234
290
|
|
|
235
291
|
function uninstallAll(paths) {
|
|
@@ -300,6 +356,9 @@ module.exports = {
|
|
|
300
356
|
writeInstalled,
|
|
301
357
|
isMuted,
|
|
302
358
|
setMuted,
|
|
359
|
+
DND_DEFAULTS,
|
|
360
|
+
isDnd,
|
|
361
|
+
setDnd,
|
|
303
362
|
detectExistingInstall,
|
|
304
363
|
installSounds,
|
|
305
364
|
installHooksConfig,
|
package/hooks/play-sound.sh
CHANGED
|
@@ -6,6 +6,15 @@ CATEGORY="${1:-}"
|
|
|
6
6
|
cat > /dev/null 2>&1
|
|
7
7
|
|
|
8
8
|
[[ -f "$SOUNDS_DIR/.muted" ]] && exit 0
|
|
9
|
+
|
|
10
|
+
# Auto-mute: skip when video call apps are running
|
|
11
|
+
if [[ -f "$SOUNDS_DIR/.dnd" ]]; then
|
|
12
|
+
while IFS= read -r proc || [[ -n "$proc" ]]; do
|
|
13
|
+
[[ -z "$proc" || "$proc" == \#* ]] && continue
|
|
14
|
+
pgrep -xi "$proc" > /dev/null 2>&1 && exit 0
|
|
15
|
+
done < "$SOUNDS_DIR/.dnd"
|
|
16
|
+
fi
|
|
17
|
+
|
|
9
18
|
[[ -z "$CATEGORY" ]] && exit 0
|
|
10
19
|
DIR="$SOUNDS_DIR/$CATEGORY"
|
|
11
20
|
[[ ! -d "$DIR" ]] && exit 0
|