claude-code-sounds 1.0.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 +273 -0
- package/bin/cli.js +262 -0
- package/hooks/play-sound.sh +23 -0
- package/images/peon-icon.png +0 -0
- package/images/peon.png +0 -0
- package/package.json +32 -0
- package/themes/wc3-peon/download.sh +30 -0
- package/themes/wc3-peon/theme.json +117 -0
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="images/peon.png" width="280" alt="WC3 Orc Peon" />
|
|
4
|
+
|
|
5
|
+
# claude-code-sounds
|
|
6
|
+
|
|
7
|
+
**Sound themes for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) lifecycle hooks.**
|
|
8
|
+
|
|
9
|
+
Plays sound effects when sessions start, prompts are submitted, responses finish, errors occur, and more.
|
|
10
|
+
|
|
11
|
+
Ships with a **WC3 Orc Peon** theme. Bring your own sounds or create new themes.
|
|
12
|
+
|
|
13
|
+
*"Something need doing?"*
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx claude-code-sounds
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That's it. Requires macOS (uses `afplay`) and Node.js 16+.
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary>Alternative: install from source</summary>
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/ryparker/claude-code-sounds.git
|
|
30
|
+
cd claude-code-sounds
|
|
31
|
+
./install.sh
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The bash installer requires `jq` (`brew install jq`).
|
|
35
|
+
|
|
36
|
+
</details>
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx claude-code-sounds # Install default theme (wc3-peon)
|
|
42
|
+
npx claude-code-sounds <theme> # Install a specific theme
|
|
43
|
+
npx claude-code-sounds --list # List available themes
|
|
44
|
+
npx claude-code-sounds --uninstall # Remove all sounds and hooks
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## WC3 Orc Peon Theme
|
|
48
|
+
|
|
49
|
+
55 sounds from Warcraft 3 Orc units mapped across 11 Claude Code lifecycle events.
|
|
50
|
+
|
|
51
|
+
> After installing, preview all sounds with `./preview.sh` or a specific category with `./preview.sh start`
|
|
52
|
+
|
|
53
|
+
<details open>
|
|
54
|
+
<summary><b>start</b> — Session starting, being summoned (5 sounds)</summary>
|
|
55
|
+
|
|
56
|
+
| Sound | Quote | Unit |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `ready-to-work.wav` | *"Ready to work!"* | Peon |
|
|
59
|
+
| `something-need-doing.wav` | *"Something need doing?"* | Peon |
|
|
60
|
+
| `more-work.mp3` | *"More work?"* | Peon (WC2) |
|
|
61
|
+
| `how-can-i-help.wav` | *"How can I help?"* | Shaman |
|
|
62
|
+
| `someone-call-for-the-doctor.wav` | *"Someone call for the doctor?"* | Witch Doctor |
|
|
63
|
+
|
|
64
|
+
</details>
|
|
65
|
+
|
|
66
|
+
<details>
|
|
67
|
+
<summary><b>prompt</b> — User submitted a prompt, acknowledging order (7 sounds)</summary>
|
|
68
|
+
|
|
69
|
+
| Sound | Quote | Unit |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `yes.wav` | *"Yes"* | Peon |
|
|
72
|
+
| `dabu.wav` | *"Dabu"* | Grunt |
|
|
73
|
+
| `zug-zug.wav` | *"Zug zug"* | Grunt |
|
|
74
|
+
| `right-away.wav` | *"Right away"* | Shaman |
|
|
75
|
+
| `immediately.wav` | *"Immediately!"* | Tauren |
|
|
76
|
+
| `anything-you-want.wav` | *"Anything you want"* | Headhunter |
|
|
77
|
+
| `more-work.mp3` | *"More work?"* | Peon (WC2) |
|
|
78
|
+
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary><b>stop</b> — Claude finished responding (8 sounds)</summary>
|
|
83
|
+
|
|
84
|
+
| Sound | Quote | Unit |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `zug-zug.wav` | *"Zug zug"* | Peon |
|
|
87
|
+
| `ok.wav` | *"OK"* | Peon |
|
|
88
|
+
| `i-can-do-that.wav` | *"I can do that"* | Peon |
|
|
89
|
+
| `be-happy-to.wav` | *"Be happy to"* | Peon |
|
|
90
|
+
| `understood.wav` | *"Understood"* | Shaman |
|
|
91
|
+
| `of-course.wav` | *"Of course"* | Far Seer |
|
|
92
|
+
| `it-is-certain.wav` | *"It is certain"* | Far Seer |
|
|
93
|
+
| `whatever-you-say.wav` | *"Whatever you say"* | Grom Hellscream |
|
|
94
|
+
|
|
95
|
+
</details>
|
|
96
|
+
|
|
97
|
+
<details>
|
|
98
|
+
<summary><b>permission</b> — Permission prompt, waiting for approval (5 sounds)</summary>
|
|
99
|
+
|
|
100
|
+
| Sound | Quote | Unit |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `hmmm.wav` | *"Hmmm?"* | Peon |
|
|
103
|
+
| `what-you-want.wav` | *"What you want?"* | Peon |
|
|
104
|
+
| `what-you-want-me-to-do.wav` | *"What you want me to do?"* | Headhunter |
|
|
105
|
+
| `who-you-want-me-kill.wav` | *"Who you want me kill?"* | Headhunter |
|
|
106
|
+
| `you-seek-me-help.wav` | *"You seek me help?"* | Witch Doctor |
|
|
107
|
+
|
|
108
|
+
</details>
|
|
109
|
+
|
|
110
|
+
<details>
|
|
111
|
+
<summary><b>subagent</b> — Spawning a subagent (6 sounds)</summary>
|
|
112
|
+
|
|
113
|
+
| Sound | Quote | Unit |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `work-work.wav` | *"Work, work"* | Peon |
|
|
116
|
+
| `zug-zug.wav` | *"Zug zug"* | Peon |
|
|
117
|
+
| `ill-try.wav` | *"I'll try"* | Peon |
|
|
118
|
+
| `why-not.wav` | *"Why not?"* | Peon |
|
|
119
|
+
| `for-the-horde.wav` | *"For the Horde!"* | Grunt |
|
|
120
|
+
| `taste-the-fury.wav` | *"Taste the fury of the Warsong!"* | Grom Hellscream |
|
|
121
|
+
|
|
122
|
+
</details>
|
|
123
|
+
|
|
124
|
+
<details>
|
|
125
|
+
<summary><b>idle</b> — Waiting for user input (7 sounds)</summary>
|
|
126
|
+
|
|
127
|
+
| Sound | Quote | Unit |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `me-busy-leave-me-alone.wav` | *"Me busy, leave me alone!"* | Peon |
|
|
130
|
+
| `no-time-for-play.wav` | *"No time for play!"* | Peon |
|
|
131
|
+
| `me-not-that-kind-of-orc.wav` | *"Me not that kind of orc!"* | Peon |
|
|
132
|
+
| `why-you-poking-me.wav` | *"Why are you poking me again?"* | Grunt |
|
|
133
|
+
| `not-easy-being-green.wav` | *"It's not easy being green"* | Grunt |
|
|
134
|
+
| `i-can-wait-no-longer.wav` | *"I can wait no longer!"* | Grom Hellscream |
|
|
135
|
+
| `outlook-not-so-good.wav` | *"Outlook... not so good"* | Far Seer |
|
|
136
|
+
|
|
137
|
+
</details>
|
|
138
|
+
|
|
139
|
+
<details>
|
|
140
|
+
<summary><b>error</b> — Tool call failed (4 sounds)</summary>
|
|
141
|
+
|
|
142
|
+
| Sound | Quote | Unit |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `peon-death.wav` | *(death sound)* | Peon |
|
|
145
|
+
| `grunt-death.wav` | *(death sound)* | Grunt |
|
|
146
|
+
| `headhunter-death.wav` | *(death sound)* | Headhunter |
|
|
147
|
+
| `reply-hazy-try-again.wav` | *"Reply hazy, try again"* | Far Seer |
|
|
148
|
+
|
|
149
|
+
</details>
|
|
150
|
+
|
|
151
|
+
<details>
|
|
152
|
+
<summary><b>end</b> — Session ending (3 sounds)</summary>
|
|
153
|
+
|
|
154
|
+
| Sound | Quote | Unit |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `well-done.wav` | *"Well done!"* | Tauren |
|
|
157
|
+
| `finally.wav` | *"Finally!"* | Grom Hellscream |
|
|
158
|
+
| `okie-dokie.wav` | *"Okie dokie"* | Peon |
|
|
159
|
+
|
|
160
|
+
</details>
|
|
161
|
+
|
|
162
|
+
<details>
|
|
163
|
+
<summary><b>task-completed</b> — Task marked done (2 sounds)</summary>
|
|
164
|
+
|
|
165
|
+
| Sound | Quote | Unit |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `well-done.wav` | *"Well done!"* | Tauren |
|
|
168
|
+
| `finally.wav` | *"Finally!"* | Grom Hellscream |
|
|
169
|
+
|
|
170
|
+
</details>
|
|
171
|
+
|
|
172
|
+
<details>
|
|
173
|
+
<summary><b>compact</b> — Context compaction, memory fading (4 sounds)</summary>
|
|
174
|
+
|
|
175
|
+
| Sound | Quote | Unit |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `concentrate-and-ask-again.wav` | *"Concentrate... and ask again"* | Far Seer |
|
|
178
|
+
| `reply-hazy-try-again.wav` | *"Reply hazy, try again"* | Far Seer |
|
|
179
|
+
| `i-can-wait-no-longer.wav` | *"I can wait no longer!"* | Grom Hellscream |
|
|
180
|
+
| `death.wav` | *(death sound)* | Peon |
|
|
181
|
+
|
|
182
|
+
</details>
|
|
183
|
+
|
|
184
|
+
<details>
|
|
185
|
+
<summary><b>teammate-idle</b> — Teammate went idle (4 sounds)</summary>
|
|
186
|
+
|
|
187
|
+
| Sound | Quote | Unit |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `what.wav` | *"What?!"* | Peon |
|
|
190
|
+
| `me-busy-leave-me-alone.wav` | *"Me busy, leave me alone!"* | Peon |
|
|
191
|
+
| `no-time-for-play.wav` | *"No time for play!"* | Peon |
|
|
192
|
+
| `i-can-wait-no-longer.wav` | *"I can wait no longer!"* | Grom Hellscream |
|
|
193
|
+
|
|
194
|
+
</details>
|
|
195
|
+
|
|
196
|
+
## Hook Events
|
|
197
|
+
|
|
198
|
+
| Event | Hook | When |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `start` | `SessionStart` | Session begins |
|
|
201
|
+
| `end` | `SessionEnd` | Session ends |
|
|
202
|
+
| `prompt` | `UserPromptSubmit` | You submit a prompt |
|
|
203
|
+
| `stop` | `Stop` | Claude finishes responding |
|
|
204
|
+
| `permission` | `Notification` | Permission prompt appears |
|
|
205
|
+
| `idle` | `Notification` | Waiting for your input |
|
|
206
|
+
| `subagent` | `SubagentStart` | Subagent spawned |
|
|
207
|
+
| `error` | `PostToolUseFailure` | Tool call failed |
|
|
208
|
+
| `task-completed` | `TaskCompleted` | Task marked done |
|
|
209
|
+
| `compact` | `PreCompact` | Context compaction |
|
|
210
|
+
| `teammate-idle` | `TeammateIdle` | Teammate went idle |
|
|
211
|
+
|
|
212
|
+
## Creating a Theme
|
|
213
|
+
|
|
214
|
+
Themes live in `themes/<name>/` with two files:
|
|
215
|
+
|
|
216
|
+
### `theme.json`
|
|
217
|
+
|
|
218
|
+
Defines metadata and maps source files to hook categories:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"name": "My Theme",
|
|
223
|
+
"description": "A short description",
|
|
224
|
+
"sounds": {
|
|
225
|
+
"start": {
|
|
226
|
+
"description": "Session starting",
|
|
227
|
+
"files": [
|
|
228
|
+
{ "src": "path/to/file.wav", "name": "descriptive-name.wav" }
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `download.sh`
|
|
236
|
+
|
|
237
|
+
Downloads the sound files. Receives two arguments:
|
|
238
|
+
- `$1` — target sounds directory (`~/.claude/sounds`)
|
|
239
|
+
- `$2` — temp directory for downloads
|
|
240
|
+
|
|
241
|
+
The script should download and extract files so they're accessible at `$2/Orc/<src path>` (matching the `src` values in `theme.json`).
|
|
242
|
+
|
|
243
|
+
## How It Works
|
|
244
|
+
|
|
245
|
+
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`.
|
|
246
|
+
|
|
247
|
+
Hooks are configured in `~/.claude/settings.json` — each Claude Code lifecycle event calls the script with the appropriate category.
|
|
248
|
+
|
|
249
|
+
## Customizing
|
|
250
|
+
|
|
251
|
+
Drop any `.wav` or `.mp3` into the sound directories to add your own clips:
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
~/.claude/sounds/
|
|
255
|
+
├── start/ # add files here for session start
|
|
256
|
+
├── stop/ # add files here for response complete
|
|
257
|
+
├── error/ # add files here for failures
|
|
258
|
+
└── ...
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The script picks randomly from whatever files are in each directory.
|
|
262
|
+
|
|
263
|
+
## Uninstalling
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
npx claude-code-sounds --uninstall
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
This removes all sound files, the hook script, and the hooks config from `settings.json`.
|
|
270
|
+
|
|
271
|
+
## Disclaimer
|
|
272
|
+
|
|
273
|
+
Sound files are downloaded from third-party sources at install time and are not included in this repository. All game audio is property of Blizzard Entertainment.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const { execSync } = require("child_process");
|
|
7
|
+
|
|
8
|
+
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const PKG_DIR = path.resolve(__dirname, "..");
|
|
11
|
+
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
12
|
+
const SOUNDS_DIR = path.join(CLAUDE_DIR, "sounds");
|
|
13
|
+
const HOOKS_DIR = path.join(CLAUDE_DIR, "hooks");
|
|
14
|
+
const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
15
|
+
const THEMES_DIR = path.join(PKG_DIR, "themes");
|
|
16
|
+
|
|
17
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function print(msg = "") {
|
|
20
|
+
console.log(msg);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function die(msg) {
|
|
24
|
+
console.error(`Error: ${msg}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mkdirp(dir) {
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function exec(cmd, opts = {}) {
|
|
33
|
+
return execSync(cmd, { encoding: "utf-8", stdio: "pipe", ...opts });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function listThemes() {
|
|
37
|
+
const themes = [];
|
|
38
|
+
for (const name of fs.readdirSync(THEMES_DIR)) {
|
|
39
|
+
const themeJson = path.join(THEMES_DIR, name, "theme.json");
|
|
40
|
+
if (!fs.existsSync(themeJson)) continue;
|
|
41
|
+
const meta = JSON.parse(fs.readFileSync(themeJson, "utf-8"));
|
|
42
|
+
themes.push({ name, description: meta.description || "", display: meta.name || name });
|
|
43
|
+
}
|
|
44
|
+
return themes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readSettings() {
|
|
48
|
+
if (fs.existsSync(SETTINGS_PATH)) {
|
|
49
|
+
return JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeSettings(settings) {
|
|
55
|
+
mkdirp(CLAUDE_DIR);
|
|
56
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Hooks Config ────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
const HOOKS_CONFIG = {
|
|
62
|
+
SessionStart: [{ matcher: "startup", hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" start', timeout: 5 }] }],
|
|
63
|
+
SessionEnd: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" end', timeout: 5 }] }],
|
|
64
|
+
Notification: [
|
|
65
|
+
{ matcher: "permission_prompt", hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" permission', timeout: 5 }] },
|
|
66
|
+
{ matcher: "idle_prompt", hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" idle', timeout: 5 }] },
|
|
67
|
+
],
|
|
68
|
+
Stop: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" stop', timeout: 5 }] }],
|
|
69
|
+
SubagentStart: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" subagent', timeout: 5 }] }],
|
|
70
|
+
PostToolUseFailure: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" error', timeout: 5 }] }],
|
|
71
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" prompt', timeout: 5 }] }],
|
|
72
|
+
TaskCompleted: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" task-completed', timeout: 5 }] }],
|
|
73
|
+
PreCompact: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" compact', timeout: 5 }] }],
|
|
74
|
+
TeammateIdle: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" teammate-idle', timeout: 5 }] }],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function showHelp() {
|
|
80
|
+
print("");
|
|
81
|
+
print(" claude-code-sounds");
|
|
82
|
+
print(" ──────────────────────────────");
|
|
83
|
+
print("");
|
|
84
|
+
print(" Usage:");
|
|
85
|
+
print(" npx claude-code-sounds Install default theme (wc3-peon)");
|
|
86
|
+
print(" npx claude-code-sounds <theme> Install a specific theme");
|
|
87
|
+
print(" npx claude-code-sounds --list List available themes");
|
|
88
|
+
print(" npx claude-code-sounds --uninstall Remove all sounds and hooks");
|
|
89
|
+
print(" npx claude-code-sounds --help Show this help");
|
|
90
|
+
print("");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function showList() {
|
|
94
|
+
print("");
|
|
95
|
+
print(" Available themes:");
|
|
96
|
+
print("");
|
|
97
|
+
for (const t of listThemes()) {
|
|
98
|
+
print(` ${t.name} — ${t.description}`);
|
|
99
|
+
}
|
|
100
|
+
print("");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function uninstall() {
|
|
104
|
+
print("");
|
|
105
|
+
print(" Uninstalling claude-code-sounds...");
|
|
106
|
+
|
|
107
|
+
if (fs.existsSync(SOUNDS_DIR)) {
|
|
108
|
+
fs.rmSync(SOUNDS_DIR, { recursive: true });
|
|
109
|
+
print(" Removed ~/.claude/sounds/");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const hookScript = path.join(HOOKS_DIR, "play-sound.sh");
|
|
113
|
+
if (fs.existsSync(hookScript)) {
|
|
114
|
+
fs.unlinkSync(hookScript);
|
|
115
|
+
print(" Removed ~/.claude/hooks/play-sound.sh");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (fs.existsSync(SETTINGS_PATH)) {
|
|
119
|
+
const settings = readSettings();
|
|
120
|
+
delete settings.hooks;
|
|
121
|
+
writeSettings(settings);
|
|
122
|
+
print(" Removed hooks from settings.json");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
print("");
|
|
126
|
+
print(" Done. All sounds removed.");
|
|
127
|
+
print("");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function install(themeName) {
|
|
131
|
+
const themeDir = path.join(THEMES_DIR, themeName);
|
|
132
|
+
const themeJsonPath = path.join(themeDir, "theme.json");
|
|
133
|
+
|
|
134
|
+
if (!fs.existsSync(themeJsonPath)) {
|
|
135
|
+
die(`Theme '${themeName}' not found.\n\nAvailable themes:\n${listThemes().map((t) => ` ${t.name} — ${t.description}`).join("\n")}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const theme = JSON.parse(fs.readFileSync(themeJsonPath, "utf-8"));
|
|
139
|
+
const categories = Object.keys(theme.sounds);
|
|
140
|
+
|
|
141
|
+
// Preflight
|
|
142
|
+
try {
|
|
143
|
+
exec("which afplay");
|
|
144
|
+
} catch {
|
|
145
|
+
die("afplay not found. This tool requires macOS.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
print("");
|
|
149
|
+
print(" claude-code-sounds");
|
|
150
|
+
print(" ──────────────────────────────");
|
|
151
|
+
print(` Theme: ${theme.name}`);
|
|
152
|
+
print("");
|
|
153
|
+
|
|
154
|
+
// 1. Create directories
|
|
155
|
+
print(" [1/4] Creating directories...");
|
|
156
|
+
for (const cat of categories) {
|
|
157
|
+
mkdirp(path.join(SOUNDS_DIR, cat));
|
|
158
|
+
}
|
|
159
|
+
mkdirp(HOOKS_DIR);
|
|
160
|
+
|
|
161
|
+
// 2. Download sounds
|
|
162
|
+
print(" [2/4] Downloading sounds...");
|
|
163
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-sounds-"));
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const downloadScript = path.join(themeDir, "download.sh");
|
|
167
|
+
if (fs.existsSync(downloadScript)) {
|
|
168
|
+
exec(`bash "${downloadScript}" "${SOUNDS_DIR}" "${tmpDir}"`, { stdio: "inherit" });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 3. Sort sounds
|
|
172
|
+
print(" [3/4] Sorting sounds...");
|
|
173
|
+
|
|
174
|
+
// Clear existing sounds
|
|
175
|
+
for (const cat of categories) {
|
|
176
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
177
|
+
for (const f of fs.readdirSync(catDir)) {
|
|
178
|
+
if (f.endsWith(".wav") || f.endsWith(".mp3")) {
|
|
179
|
+
fs.unlinkSync(path.join(catDir, f));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Copy files based on theme.json
|
|
185
|
+
const srcBase = path.join(tmpDir, "Orc");
|
|
186
|
+
for (const [category, config] of Object.entries(theme.sounds)) {
|
|
187
|
+
for (const file of config.files) {
|
|
188
|
+
let srcFile;
|
|
189
|
+
if (file.src.startsWith("@soundfxcenter/")) {
|
|
190
|
+
srcFile = path.join(srcBase, path.basename(file.src));
|
|
191
|
+
} else {
|
|
192
|
+
srcFile = path.join(srcBase, file.src);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const destFile = path.join(SOUNDS_DIR, category, file.name);
|
|
196
|
+
if (fs.existsSync(srcFile)) {
|
|
197
|
+
fs.copyFileSync(srcFile, destFile);
|
|
198
|
+
} else {
|
|
199
|
+
print(` Warning: ${file.src} not found, skipping`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 4. Install hooks
|
|
205
|
+
print(" [4/4] Installing hooks...");
|
|
206
|
+
|
|
207
|
+
// Copy play-sound.sh
|
|
208
|
+
const hookSrc = path.join(PKG_DIR, "hooks", "play-sound.sh");
|
|
209
|
+
const hookDest = path.join(HOOKS_DIR, "play-sound.sh");
|
|
210
|
+
fs.copyFileSync(hookSrc, hookDest);
|
|
211
|
+
fs.chmodSync(hookDest, 0o755);
|
|
212
|
+
|
|
213
|
+
// Merge hooks into settings.json
|
|
214
|
+
const settings = readSettings();
|
|
215
|
+
settings.hooks = HOOKS_CONFIG;
|
|
216
|
+
writeSettings(settings);
|
|
217
|
+
|
|
218
|
+
// Summary
|
|
219
|
+
print("");
|
|
220
|
+
print(" Installed! Here's what you'll hear:");
|
|
221
|
+
print(" ─────────────────────────────────────");
|
|
222
|
+
|
|
223
|
+
let total = 0;
|
|
224
|
+
for (const [cat, config] of Object.entries(theme.sounds)) {
|
|
225
|
+
const count = config.files.length;
|
|
226
|
+
total += count;
|
|
227
|
+
print(` ${cat} (${count}) — ${config.description}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
print("");
|
|
231
|
+
print(` ${total} sound files across ${categories.length} events.`);
|
|
232
|
+
print(" Start a new Claude Code session to hear it!");
|
|
233
|
+
print("");
|
|
234
|
+
print(" Zug zug.");
|
|
235
|
+
print("");
|
|
236
|
+
} finally {
|
|
237
|
+
// Cleanup
|
|
238
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
const args = process.argv.slice(2);
|
|
245
|
+
const arg = args[0] || "wc3-peon";
|
|
246
|
+
|
|
247
|
+
switch (arg) {
|
|
248
|
+
case "--help":
|
|
249
|
+
case "-h":
|
|
250
|
+
showHelp();
|
|
251
|
+
break;
|
|
252
|
+
case "--list":
|
|
253
|
+
case "-l":
|
|
254
|
+
showList();
|
|
255
|
+
break;
|
|
256
|
+
case "--uninstall":
|
|
257
|
+
case "--remove":
|
|
258
|
+
uninstall();
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
install(arg);
|
|
262
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
SOUNDS_DIR="$HOME/.claude/sounds"
|
|
3
|
+
CATEGORY="${1:-}"
|
|
4
|
+
|
|
5
|
+
# Drain stdin so the hook system doesn't get a broken pipe
|
|
6
|
+
cat > /dev/null 2>&1
|
|
7
|
+
|
|
8
|
+
[[ -z "$CATEGORY" ]] && exit 0
|
|
9
|
+
DIR="$SOUNDS_DIR/$CATEGORY"
|
|
10
|
+
[[ ! -d "$DIR" ]] && exit 0
|
|
11
|
+
|
|
12
|
+
# Collect all .wav and .mp3 files
|
|
13
|
+
FILES=()
|
|
14
|
+
for f in "$DIR"/*.wav "$DIR"/*.mp3; do
|
|
15
|
+
[[ -f "$f" ]] && FILES+=("$f")
|
|
16
|
+
done
|
|
17
|
+
[[ ${#FILES[@]} -eq 0 ]] && exit 0
|
|
18
|
+
|
|
19
|
+
# Pick a random file and play it in the background
|
|
20
|
+
RANDOM_FILE="${FILES[$RANDOM % ${#FILES[@]}]}"
|
|
21
|
+
afplay "$RANDOM_FILE" &
|
|
22
|
+
|
|
23
|
+
exit 0
|
|
Binary file
|
package/images/peon.png
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-code-sounds",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sound themes for Claude Code lifecycle hooks",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-code-sounds": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"claude",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"sounds",
|
|
12
|
+
"hooks",
|
|
13
|
+
"warcraft",
|
|
14
|
+
"peon",
|
|
15
|
+
"soundboard"
|
|
16
|
+
],
|
|
17
|
+
"author": "ryparker",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/ryparker/claude-code-sounds.git"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin/",
|
|
25
|
+
"hooks/",
|
|
26
|
+
"themes/",
|
|
27
|
+
"images/"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=16"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Downloads sound files for the wc3-peon theme.
|
|
4
|
+
# Called by install.sh with $1 = target sounds directory, $2 = temp directory.
|
|
5
|
+
#
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
SOUNDS_DIR="$1"
|
|
9
|
+
TMP_DIR="$2"
|
|
10
|
+
|
|
11
|
+
echo " Downloading WC3 Orc sounds..."
|
|
12
|
+
ZIP="$TMP_DIR/orc-sounds.zip"
|
|
13
|
+
curl -sL -o "$ZIP" "https://sounds.spriters-resource.com/media/assets/422/425494.zip?updated=1755544622"
|
|
14
|
+
|
|
15
|
+
if ! file "$ZIP" | grep -q "Zip"; then
|
|
16
|
+
echo " Error: Failed to download WC3 sound pack."
|
|
17
|
+
return 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
unzip -qo "$ZIP" \
|
|
21
|
+
"Orc/Peon/*" "Orc/Grunt/*" "Orc/HeadHunter/*" "Orc/Shaman/*" \
|
|
22
|
+
"Orc/Hellscream/*" "Orc/HeroFarseer/*" "Orc/Tauren/*" "Orc/WitchDoctor/*" \
|
|
23
|
+
-d "$TMP_DIR"
|
|
24
|
+
|
|
25
|
+
# Download supplemental clips not in the zip
|
|
26
|
+
echo " Downloading supplemental clips..."
|
|
27
|
+
curl -sL -o "$TMP_DIR/Orc/more-work.mp3" \
|
|
28
|
+
"https://soundfxcenter.com/video-games/warcraft-2/8d82b5_Warcraft_2_Peasant_More_Work_Sound_Effect.mp3"
|
|
29
|
+
|
|
30
|
+
echo " Download complete."
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "WC3 Orc Peon",
|
|
3
|
+
"description": "Warcraft 3 Orc unit soundbites — Peons, Grunts, Shamans, and more",
|
|
4
|
+
"author": "ryparker",
|
|
5
|
+
"sounds": {
|
|
6
|
+
"start": {
|
|
7
|
+
"description": "Session starting — being summoned",
|
|
8
|
+
"files": [
|
|
9
|
+
{ "src": "Peon/PeonReady1.wav", "name": "ready-to-work.wav" },
|
|
10
|
+
{ "src": "Peon/PeonWhat4.wav", "name": "something-need-doing.wav" },
|
|
11
|
+
{ "src": "Shaman/ShamanWhat3.wav", "name": "how-can-i-help.wav" },
|
|
12
|
+
{ "src": "WitchDoctor/WitchDoctorReady1.wav", "name": "someone-call-for-the-doctor.wav" },
|
|
13
|
+
{ "src": "@soundfxcenter/more-work.mp3", "name": "more-work.mp3" }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"end": {
|
|
17
|
+
"description": "Session over — farewell",
|
|
18
|
+
"files": [
|
|
19
|
+
{ "src": "Tauren/TaurenYes3.wav", "name": "well-done.wav" },
|
|
20
|
+
{ "src": "Hellscream/GromYes3.wav", "name": "finally.wav" },
|
|
21
|
+
{ "src": "Peon/PeonYes4.wav", "name": "okie-dokie.wav" }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"permission": {
|
|
25
|
+
"description": "Permission prompt — what do you want to approve?",
|
|
26
|
+
"files": [
|
|
27
|
+
{ "src": "Peon/PeonWhat2.wav", "name": "hmmm.wav" },
|
|
28
|
+
{ "src": "Peon/PeonWhat3.wav", "name": "what-you-want.wav" },
|
|
29
|
+
{ "src": "HeadHunter/HeadHunterWhat3.wav", "name": "what-you-want-me-to-do.wav" },
|
|
30
|
+
{ "src": "HeadHunter/HeadHunterWhat1.wav", "name": "who-you-want-me-kill.wav" },
|
|
31
|
+
{ "src": "WitchDoctor/WitchDoctorWhat3.wav", "name": "you-seek-me-help.wav" }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"stop": {
|
|
35
|
+
"description": "Done responding — acknowledgment",
|
|
36
|
+
"files": [
|
|
37
|
+
{ "src": "Peon/PeonYesAttack2.wav", "name": "zug-zug.wav" },
|
|
38
|
+
{ "src": "Peon/PeonYesAttack1.wav", "name": "ok.wav" },
|
|
39
|
+
{ "src": "Peon/PeonYes1.wav", "name": "i-can-do-that.wav" },
|
|
40
|
+
{ "src": "Peon/PeonYes2.wav", "name": "be-happy-to.wav" },
|
|
41
|
+
{ "src": "Shaman/ShamanYes2.wav", "name": "understood.wav" },
|
|
42
|
+
{ "src": "HeroFarseer/HeroFarseerYes2.wav", "name": "of-course.wav" },
|
|
43
|
+
{ "src": "HeroFarseer/HeroFarseerYes4.wav", "name": "it-is-certain.wav" },
|
|
44
|
+
{ "src": "Hellscream/GromYes2.wav", "name": "whatever-you-say.wav" }
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"subagent": {
|
|
48
|
+
"description": "Spawning subagent — dispatching",
|
|
49
|
+
"files": [
|
|
50
|
+
{ "src": "Peon/PeonYes3.wav", "name": "work-work.wav" },
|
|
51
|
+
{ "src": "Grunt/GruntWarcry1.wav", "name": "for-the-horde.wav" },
|
|
52
|
+
{ "src": "Hellscream/GromWarcry1.wav", "name": "taste-the-fury.wav" },
|
|
53
|
+
{ "src": "Peon/PeonYesAttack3.wav", "name": "ill-try.wav" },
|
|
54
|
+
{ "src": "Peon/PeonWarcry1.wav", "name": "why-not.wav" },
|
|
55
|
+
{ "src": "Peon/PeonYesAttack2.wav", "name": "zug-zug.wav" }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"idle": {
|
|
59
|
+
"description": "Waiting for input — bored and annoyed",
|
|
60
|
+
"files": [
|
|
61
|
+
{ "src": "Peon/PeonAngry2.wav", "name": "me-busy-leave-me-alone.wav" },
|
|
62
|
+
{ "src": "Peon/PeonAngry3.wav", "name": "no-time-for-play.wav" },
|
|
63
|
+
{ "src": "Peon/PeonAngry4.wav", "name": "me-not-that-kind-of-orc.wav" },
|
|
64
|
+
{ "src": "Grunt/GruntAngry1.wav", "name": "why-you-poking-me.wav" },
|
|
65
|
+
{ "src": "Grunt/GruntAngry7.wav", "name": "not-easy-being-green.wav" },
|
|
66
|
+
{ "src": "Hellscream/GromWhat3.wav", "name": "i-can-wait-no-longer.wav" },
|
|
67
|
+
{ "src": "HeroFarseer/HeroFarseerAngry4.wav", "name": "outlook-not-so-good.wav" }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"error": {
|
|
71
|
+
"description": "Tool failure — something broke",
|
|
72
|
+
"files": [
|
|
73
|
+
{ "src": "Peon/PeonDeath.wav", "name": "peon-death.wav" },
|
|
74
|
+
{ "src": "Grunt/GruntDeath.wav", "name": "grunt-death.wav" },
|
|
75
|
+
{ "src": "HeadHunter/HeadHunterDeath.wav", "name": "headhunter-death.wav" },
|
|
76
|
+
{ "src": "HeroFarseer/HeroFarseerAngry5.wav", "name": "reply-hazy-try-again.wav" }
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"prompt": {
|
|
80
|
+
"description": "User submitted prompt — acknowledging order",
|
|
81
|
+
"files": [
|
|
82
|
+
{ "src": "Peon/PeonWhat1.wav", "name": "yes.wav" },
|
|
83
|
+
{ "src": "Grunt/GruntYes1.wav", "name": "dabu.wav" },
|
|
84
|
+
{ "src": "Grunt/GruntYes4.wav", "name": "zug-zug.wav" },
|
|
85
|
+
{ "src": "Shaman/ShamanYes4.wav", "name": "right-away.wav" },
|
|
86
|
+
{ "src": "Tauren/TaurenYes2.wav", "name": "immediately.wav" },
|
|
87
|
+
{ "src": "HeadHunter/HeadHunterYes2.wav", "name": "anything-you-want.wav" },
|
|
88
|
+
{ "src": "@soundfxcenter/more-work.mp3", "name": "more-work.mp3" }
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"task-completed": {
|
|
92
|
+
"description": "Task finished — victory",
|
|
93
|
+
"files": [
|
|
94
|
+
{ "src": "Tauren/TaurenYes3.wav", "name": "well-done.wav" },
|
|
95
|
+
{ "src": "Hellscream/GromYes3.wav", "name": "finally.wav" }
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
"compact": {
|
|
99
|
+
"description": "Context compaction — memory fading",
|
|
100
|
+
"files": [
|
|
101
|
+
{ "src": "HeroFarseer/HeroFarseerAngry3.wav", "name": "concentrate-and-ask-again.wav" },
|
|
102
|
+
{ "src": "HeroFarseer/HeroFarseerAngry5.wav", "name": "reply-hazy-try-again.wav" },
|
|
103
|
+
{ "src": "Peon/PeonDeath.wav", "name": "death.wav" },
|
|
104
|
+
{ "src": "Hellscream/GromWhat3.wav", "name": "i-can-wait-no-longer.wav" }
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
"teammate-idle": {
|
|
108
|
+
"description": "Teammate went idle — impatient",
|
|
109
|
+
"files": [
|
|
110
|
+
{ "src": "Peon/PeonAngry1.wav", "name": "what.wav" },
|
|
111
|
+
{ "src": "Peon/PeonAngry2.wav", "name": "me-busy-leave-me-alone.wav" },
|
|
112
|
+
{ "src": "Peon/PeonAngry3.wav", "name": "no-time-for-play.wav" },
|
|
113
|
+
{ "src": "Hellscream/GromWhat3.wav", "name": "i-can-wait-no-longer.wav" }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|