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 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
- ## Hook Events
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
+ ![Sound assignment grid](images/sound-grid.png)
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
- ## Creating a Theme
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
- ## How It Works
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
- ![Sound assignment grid](images/sound-grid.png)
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 { execSync, spawn } = require("child_process");
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
- exec(`which ${name}`);
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
- const meta = JSON.parse(fs.readFileSync(themeJson, "utf-8"));
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
- return JSON.parse(fs.readFileSync(paths.SETTINGS_PATH, "utf-8"));
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
- fs.writeFileSync(paths.SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
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
- return JSON.parse(fs.readFileSync(paths.INSTALLED_PATH, "utf-8"));
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
- fs.writeFileSync(paths.INSTALLED_PATH, JSON.stringify(data, null, 2) + "\n");
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
- try {
190
- for (const f of fs.readdirSync(catDir)) {
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,
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-sounds",
3
- "version": "1.5.4",
3
+ "version": "1.6.1",
4
4
  "description": "Sound themes for Claude Code lifecycle hooks",
5
5
  "bin": {
6
6
  "claude-code-sounds": "bin/cli.js"