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 +1 -1
- package/src/installer.js +79 -1
- package/src/player.js +6 -3
- package/src/presets.js +11 -1
- package/src/tts.js +1 -0
package/package.json
CHANGED
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(
|
|
407
|
-
.replace(/^\s
|
|
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
|
|
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();
|