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 +10 -3
- package/assets/images/cli-installation.png +0 -0
- package/package.json +2 -2
- package/src/cli.js +1 -1
- package/src/play.js +120 -5
- package/assets/images/captured.png +0 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# claude-sound
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cross-platform CLI (macOS, Windows, Linux) that configures **Claude Code Hooks** to play **bundled sounds**.
|
|
4
4
|
|
|
5
|
-

|
|
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.
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-sound",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Configure Claude Code hooks to play bundled sounds
|
|
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
|
|
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(
|
|
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
|
|
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 &&
|
|
165
|
+
if (pid != null && platform() !== 'win32') {
|
|
51
166
|
process.kill(-pid, 'SIGKILL');
|
|
52
167
|
} else {
|
|
53
168
|
_previewProcess.kill('SIGKILL');
|
|
Binary file
|