claude-sound 0.1.8 → 0.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
@@ -1,8 +1,8 @@
1
1
  # claude-sound
2
2
 
3
- macOS-only CLI that configures **Claude Code Hooks** to play **bundled sounds** using `afplay`.
3
+ Cross-platform CLI (macOS, Windows, Linux) that configures **Claude Code Hooks** to play **bundled sounds**.
4
4
 
5
- ![claude-sound CLI](assets/images/captured.png)
5
+ ![claude-sound CLI](assets/images/cli-installation.png)
6
6
 
7
7
  - Setup UI: `npx claude-sound@latest`
8
8
  - Hook runner: `npx --yes claude-sound@latest play --event <Event> --sound <SoundId> --managed-by claude-sound`
@@ -84,7 +84,14 @@ Add `assets/sounds/order.json` to control order and display names:
84
84
  - Use full IDs (e.g. `common/baemin`). Sounds not listed append at the end.
85
85
  - Use `{ "id": "...", "label": "Display Name" }` for custom labels; otherwise the filename is shown.
86
86
 
87
+ ## Platform support
88
+
89
+ | Platform | Audio player | Notes |
90
+ |----------|--------------|-------|
91
+ | **macOS** | `afplay` | Built-in, no setup needed |
92
+ | **Windows** | `ffplay`, `mpv`, `mpg123`, or PowerShell | Install [ffmpeg](https://ffmpeg.org/) (includes `ffplay`) or [mpv](https://mpv.io/) for best support. PowerShell (built-in) plays WAV only. |
93
+ | **Linux** | `ffplay`, `mpv`, `mpg123`, `aplay`, etc. | Install ffmpeg or mpv for MP3 support. |
94
+
87
95
  ## Notes
88
96
 
89
- - macOS only (requires `afplay`).
90
97
  - Hooks run `npx` each time the event fires. It’s simple and works everywhere, but may be slower than a local install.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-sound",
3
- "version": "0.1.8",
4
- "description": "Configure Claude Code hooks to play bundled sounds on macOS (afplay).",
3
+ "version": "0.2.0",
4
+ "description": "Configure Claude Code hooks to play bundled sounds (macOS, Windows, Linux).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/beefiker/claude-sound",
7
7
  "repository": {
package/src/cli.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
 
19
19
  function usage(exitCode = 0) {
20
20
  process.stdout.write(`\
21
- claude-sound (macOS)\n\nUsage:\n npx claude-sound@latest Interactive hook sound setup\n claude-sound Interactive hook sound setup\n\n claude-sound play --sound <id> Play a bundled sound (uses afplay)\n claude-sound list-sounds List bundled sound ids\n claude-sound list-events List Claude hook event names\n\nOptions:\n -h, --help Show help\n\nExamples:\n npx claude-sound@latest\n npx claude-sound@latest play --sound ring1\n`);
21
+ claude-sound (macOS, Windows, Linux)\n\nUsage:\n npx claude-sound@latest Interactive hook sound setup\n claude-sound Interactive hook sound setup\n\n claude-sound play --sound <id> Play a bundled sound\n claude-sound list-sounds List bundled sound ids\n claude-sound list-events List Claude hook event names\n\nOptions:\n -h, --help Show help\n\nExamples:\n npx claude-sound@latest\n npx claude-sound@latest play --sound ring1\n`);
22
22
  process.exit(exitCode);
23
23
  }
24
24
 
package/src/play.js CHANGED
@@ -1,6 +1,114 @@
1
- import { execFile, spawn } from 'node:child_process';
1
+ import { execFile, execFileSync, spawn } from 'node:child_process';
2
+ import { platform } from 'node:os';
2
3
  import { resolveSoundPath } from './sounds.js';
3
4
 
5
+ /**
6
+ * Player config: { name, check, args }
7
+ * - name: executable name
8
+ * - check: sync function to verify availability (returns boolean)
9
+ * - args: (filePath) => string[] - args to pass to the executable
10
+ */
11
+
12
+ /**
13
+ * Check if an executable exists in PATH.
14
+ * Uses command -v (POSIX) on Unix, where.exe on Windows.
15
+ */
16
+ function findExecutable(name) {
17
+ try {
18
+ if (platform() === 'win32') {
19
+ execFileSync('where.exe', [name], { stdio: 'pipe', windowsHide: true });
20
+ } else {
21
+ execFileSync('sh', ['-c', `command -v ${name}`], { stdio: 'pipe' });
22
+ }
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /** @type {Array<{ name: string; check: () => boolean; args: (f: string) => string[] }>} */
30
+ const PLAYERS = [
31
+ {
32
+ name: 'afplay',
33
+ check: () => platform() === 'darwin' && findExecutable('afplay'),
34
+ args: (f) => [f]
35
+ },
36
+ {
37
+ name: 'ffplay',
38
+ check: () => findExecutable('ffplay'),
39
+ args: (f) => ['-nodisp', '-autoexit', '-loglevel', 'quiet', f]
40
+ },
41
+ {
42
+ name: 'mpv',
43
+ check: () => findExecutable('mpv'),
44
+ args: (f) => ['--no-video', '--really-quiet', f]
45
+ },
46
+ {
47
+ name: 'mpg123',
48
+ check: () => findExecutable('mpg123'),
49
+ args: (f) => ['-q', f]
50
+ },
51
+ {
52
+ name: 'mpg321',
53
+ check: () => findExecutable('mpg321'),
54
+ args: (f) => ['-q', f]
55
+ },
56
+ {
57
+ name: 'mplayer',
58
+ check: () => findExecutable('mplayer'),
59
+ args: (f) => ['-really-quiet', '-vo', 'null', f]
60
+ },
61
+ {
62
+ name: 'aplay',
63
+ check: () => findExecutable('aplay'),
64
+ args: (f) => ['-q', f]
65
+ },
66
+ {
67
+ name: 'paplay',
68
+ check: () => findExecutable('paplay'),
69
+ args: (f) => [f]
70
+ },
71
+ {
72
+ name: 'cvlc',
73
+ check: () => findExecutable('cvlc'),
74
+ args: (f) => ['--play-and-exit', '-q', f]
75
+ },
76
+ {
77
+ name: 'powershell.exe',
78
+ check: () => platform() === 'win32',
79
+ args: (f) => {
80
+ const escaped = f.replace(/'/g, "''");
81
+ return [
82
+ '-NoProfile',
83
+ '-Command',
84
+ `(New-Object Media.SoundPlayer '${escaped}').PlaySync()`
85
+ ];
86
+ }
87
+ }
88
+ ];
89
+
90
+ /** @type {{ name: string; args: (f: string) => string[] } | null} */
91
+ let _resolvedPlayer = null;
92
+
93
+ /**
94
+ * @returns {{ name: string; args: (f: string) => string[] }}
95
+ * @throws {Error}
96
+ */
97
+ function getPlayer() {
98
+ if (_resolvedPlayer) return _resolvedPlayer;
99
+
100
+ for (const p of PLAYERS) {
101
+ if (p.check()) {
102
+ _resolvedPlayer = { name: p.name, args: p.args };
103
+ return _resolvedPlayer;
104
+ }
105
+ }
106
+
107
+ throw new Error(
108
+ 'No audio player found. On Windows/Linux, install ffmpeg (ffplay) or mpv.'
109
+ );
110
+ }
111
+
4
112
  /**
5
113
  * Play a sound and wait for it to finish.
6
114
  * @param {string} soundId
@@ -8,8 +116,9 @@ import { resolveSoundPath } from './sounds.js';
8
116
  */
9
117
  export function playSound(soundId) {
10
118
  const file = resolveSoundPath(soundId);
119
+ const { name, args } = getPlayer();
11
120
  return new Promise((resolve, reject) => {
12
- execFile('afplay', [file], (err) => {
121
+ execFile(name, args(file), { windowsHide: true }, (err) => {
13
122
  if (err) reject(err);
14
123
  else resolve();
15
124
  });
@@ -28,11 +137,17 @@ let _previewProcess = null;
28
137
  export function playSoundPreview(soundId) {
29
138
  stopPreview();
30
139
  const file = resolveSoundPath(soundId);
31
- const proc = spawn('afplay', [file], {
140
+ const { name, args } = getPlayer();
141
+
142
+ const proc = spawn(name, args(file), {
32
143
  detached: true,
33
- stdio: 'ignore'
144
+ stdio: 'ignore',
145
+ windowsHide: true
34
146
  });
35
147
  _previewProcess = proc;
148
+ proc.on('error', () => {
149
+ if (_previewProcess === proc) _previewProcess = null;
150
+ });
36
151
  proc.on('exit', () => {
37
152
  if (_previewProcess === proc) _previewProcess = null;
38
153
  });
@@ -47,7 +162,7 @@ export function stopPreview() {
47
162
  if (_previewProcess) {
48
163
  try {
49
164
  const pid = _previewProcess.pid;
50
- if (pid != null && process.platform !== 'win32') {
165
+ if (pid != null && platform() !== 'win32') {
51
166
  process.kill(-pid, 'SIGKILL');
52
167
  } else {
53
168
  _previewProcess.kill('SIGKILL');
Binary file