klaudio 0.9.0 → 0.9.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klaudio",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Add sound effects to your coding sessions — play sounds when tasks complete, notifications arrive, and more",
5
5
  "type": "module",
6
6
  "bin": {
package/src/installer.js CHANGED
@@ -60,6 +60,12 @@ export async function install({ scope, sounds, tts = false }) {
60
60
  const event = EVENTS[eventId];
61
61
  if (!event) continue;
62
62
 
63
+ // Approval event uses a PreToolUse/PostToolUse timer instead of a direct hook
64
+ if (eventId === "approval") {
65
+ await installApprovalHooks(settings, soundPath, claudeDir);
66
+ continue;
67
+ }
68
+
63
69
  const hookEvent = event.hookEvent;
64
70
  // Enable TTS only for the "stop" event (task complete)
65
71
  const useTts = tts && eventId === "stop";
@@ -101,6 +107,64 @@ export async function install({ scope, sounds, tts = false }) {
101
107
  };
102
108
  }
103
109
 
110
+ /**
111
+ * Install approval notification hooks (PreToolUse/PostToolUse timer).
112
+ * Writes a helper script and hooks that play a sound after 15s if no approval.
113
+ */
114
+ async function installApprovalHooks(settings, soundPath, claudeDir) {
115
+ const normalized = soundPath.replace(/\\/g, "/");
116
+ const scriptPath = join(claudeDir, "approval-notify.sh").replace(/\\/g, "/");
117
+
118
+ // Write the timer script
119
+ const script = `#!/usr/bin/env bash
120
+ # klaudio: approval notification timer
121
+ # Plays a sound if a tool isn't approved within DELAY seconds.
122
+ DELAY=15
123
+ MARKER="/tmp/.claude-approval-pending"
124
+ SOUND="${normalized}"
125
+
126
+ case "$1" in
127
+ start)
128
+ TOKEN="$$-$(date +%s%N)"
129
+ echo "$TOKEN" > "$MARKER"
130
+ (
131
+ sleep "$DELAY"
132
+ if [ -f "$MARKER" ] && [ "$(cat "$MARKER" 2>/dev/null)" = "$TOKEN" ]; then
133
+ rm -f "$MARKER"
134
+ npx klaudio play "$SOUND" 2>/dev/null
135
+ fi
136
+ ) &
137
+ ;;
138
+ cancel)
139
+ rm -f "$MARKER"
140
+ ;;
141
+ esac
142
+ `;
143
+ await writeFile(scriptPath, script, "utf-8");
144
+
145
+ // Add PreToolUse hook
146
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
147
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
148
+ (e) => !e._klaudio && !e._klonk
149
+ );
150
+ settings.hooks.PreToolUse.push({
151
+ _klaudio: true,
152
+ matcher: "",
153
+ hooks: [{ type: "command", command: `bash "${scriptPath}" start` }],
154
+ });
155
+
156
+ // Add PostToolUse hook
157
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
158
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
159
+ (e) => !e._klaudio && !e._klonk
160
+ );
161
+ settings.hooks.PostToolUse.push({
162
+ _klaudio: true,
163
+ matcher: "",
164
+ hooks: [{ type: "command", command: `bash "${scriptPath}" cancel` }],
165
+ });
166
+ }
167
+
104
168
  /**
105
169
  * Install hooks for GitHub Copilot coding agent.
106
170
  * Writes .github/hooks/klaudio.json in the Copilot format.
@@ -168,9 +232,23 @@ export async function getExistingSounds(scope) {
168
232
  if (!settings.hooks) return sounds;
169
233
 
170
234
  for (const [eventId, event] of Object.entries(EVENTS)) {
235
+ // Approval event: read sound from the approval-notify.sh script
236
+ if (eventId === "approval") {
237
+ const scriptPath = join(claudeDir, "approval-notify.sh");
238
+ try {
239
+ const script = await readFile(scriptPath, "utf-8");
240
+ const m = script.match(/SOUND="([^"]+\.(wav|mp3|ogg|flac|aac))"/);
241
+ if (m) {
242
+ sounds[eventId] = m[1].replace(/\//g, join("a", "b").includes("\\") ? "\\" : "/");
243
+ }
244
+ } catch { /* no script */ }
245
+ continue;
246
+ }
247
+
171
248
  const hookEntries = settings.hooks[event.hookEvent];
172
249
  if (!hookEntries) continue;
173
- const entry = hookEntries.find((e) => e._klaudio || e._klonk);
250
+ const entry = hookEntries.find((e) => e._klaudio || e._klonk
251
+ || e.hooks?.[0]?.command?.includes("klaudio"));
174
252
  if (!entry?.hooks?.[0]?.command) continue;
175
253
 
176
254
  // Extract file path from the play command
package/src/player.js CHANGED
@@ -403,8 +403,9 @@ export async function handlePlayCommand(args) {
403
403
  .replace(/_([^_]+)_/g, "$1") // _italic_ -> text
404
404
  .replace(/#{1,6}\s+/g, "") // headings
405
405
  .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // [links](url) -> text
406
- .replace(/^\s*[-*+]\s+/gm, "") // list bullets
407
- .replace(/^\s*\d+\.\s+/gm, "") // numbered lists
406
+ .replace(/\([^)]*\)/g, "") // remove parenthesised content
407
+ .replace(/^\s*[-*+]\s+(.*)/gm, "... $1.") // list bullets -> paused items
408
+ .replace(/^\s*\d+\.\s+(.*)/gm, "... $1.") // numbered lists -> paused items
408
409
  .replace(/\n+/g, " ") // newlines -> spaces
409
410
  .trim();
410
411
  // Build summary: include sentences up to ~25 words max.
@@ -430,12 +431,14 @@ export async function handlePlayCommand(args) {
430
431
  }
431
432
  // Include next sentence if we're still under the word limit
432
433
  if (wordsSoFar + nextWords <= MAX_WORDS) {
433
- summary += " " + next;
434
+ summary += " ... " + next;
434
435
  } else {
435
436
  break;
436
437
  }
437
438
  }
438
439
  }
440
+ // Collapse repeated ellipsis/dots and ensure pauses between sentences
441
+ summary = summary.replace(/\.{2,}/g, "...").replace(/\s{2,}/g, " ");
439
442
  // Prefix with project folder name if available
440
443
  const project = hookData.cwd ? hookData.cwd.replace(/\\/g, "/").split("/").pop() : null;
441
444
  const spoken = project ? `${project}: ${summary}` : summary;
package/src/presets.js CHANGED
@@ -14,7 +14,7 @@ const SOUNDS_DIR = isCompiledBinary
14
14
  export const EVENTS = {
15
15
  notification: {
16
16
  name: "Notification",
17
- description: "Plays when Claude needs your attention",
17
+ description: "Plays when Claude sends a notification (e.g. long task finished in background)",
18
18
  hookEvent: "Notification",
19
19
  copilotHookEvent: null, // Copilot doesn't have this yet
20
20
  },
@@ -24,6 +24,12 @@ export const EVENTS = {
24
24
  hookEvent: "Stop",
25
25
  copilotHookEvent: "sessionEnd",
26
26
  },
27
+ approval: {
28
+ name: "Waiting for Approval",
29
+ description: "Plays when Claude waits for you to approve an action",
30
+ hookEvent: null, // Uses PreToolUse/PostToolUse timer, not a direct hook
31
+ copilotHookEvent: null,
32
+ },
27
33
  };
28
34
 
29
35
  /**
@@ -37,6 +43,7 @@ export const PRESETS = {
37
43
  sounds: {
38
44
  stop: join(SOUNDS_DIR, "retro-8bit", "stop.wav"),
39
45
  notification: join(SOUNDS_DIR, "retro-8bit", "notification.wav"),
46
+ approval: join(SOUNDS_DIR, "retro-8bit", "notification.wav"),
40
47
  },
41
48
  },
42
49
  "minimal-zen": {
@@ -46,6 +53,7 @@ export const PRESETS = {
46
53
  sounds: {
47
54
  stop: join(SOUNDS_DIR, "minimal-zen", "stop.wav"),
48
55
  notification: join(SOUNDS_DIR, "minimal-zen", "notification.wav"),
56
+ approval: join(SOUNDS_DIR, "minimal-zen", "notification.wav"),
49
57
  },
50
58
  },
51
59
  "sci-fi-terminal": {
@@ -55,6 +63,7 @@ export const PRESETS = {
55
63
  sounds: {
56
64
  stop: join(SOUNDS_DIR, "sci-fi-terminal", "stop.wav"),
57
65
  notification: join(SOUNDS_DIR, "sci-fi-terminal", "notification.wav"),
66
+ approval: join(SOUNDS_DIR, "sci-fi-terminal", "notification.wav"),
58
67
  },
59
68
  },
60
69
  "victory-fanfare": {
@@ -64,6 +73,7 @@ export const PRESETS = {
64
73
  sounds: {
65
74
  stop: join(SOUNDS_DIR, "victory-fanfare", "stop.wav"),
66
75
  notification: join(SOUNDS_DIR, "victory-fanfare", "notification.wav"),
76
+ approval: join(SOUNDS_DIR, "victory-fanfare", "notification.wav"),
67
77
  },
68
78
  },
69
79
  };
package/src/tts.js CHANGED
@@ -220,6 +220,7 @@ export async function speak(text, onProgress) {
220
220
  const child = execFile(piperBin, [
221
221
  "--model", modelPath,
222
222
  "--output_file", outPath,
223
+ "--sentence_silence", "0.5",
223
224
  ], { windowsHide: true, timeout: 15000 }, (err) => {
224
225
  if (err) reject(err);
225
226
  else resolve();