klaudio 0.13.0 → 0.13.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/bin/cli.js CHANGED
@@ -52,6 +52,27 @@ if (["help", "--help", "-h"].includes(process.argv[2])) {
52
52
  process.exit(0);
53
53
  }
54
54
 
55
+ // Auto-update: check npm for a newer version and install it before showing UI
56
+ async function autoUpdate() {
57
+ try {
58
+ const { createRequire } = await import("node:module");
59
+ const pkg = createRequire(import.meta.url)("../package.json");
60
+ const res = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, {
61
+ signal: AbortSignal.timeout(3000),
62
+ });
63
+ if (!res.ok) return;
64
+ const { version: latest } = await res.json();
65
+ if (latest === pkg.version) return;
66
+ console.log(`\n Updating klaudio ${pkg.version} → ${latest}...\n`);
67
+ const { spawnSync } = await import("node:child_process");
68
+ spawnSync("npm", ["install", "-g", "klaudio@latest"], { stdio: "inherit" });
69
+ // Re-exec so the UI runs under the new version
70
+ const result = spawnSync("npx", ["--yes", "klaudio"], { stdio: "inherit" });
71
+ process.exit(result.status ?? 0);
72
+ } catch { /* ignore network/registry errors */ }
73
+ }
74
+ await autoUpdate();
75
+
55
76
  // Default: interactive installer UI
56
77
  const { run } = await import("../src/cli.js");
57
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klaudio",
3
- "version": "0.13.0",
3
+ "version": "0.13.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/cli.js CHANGED
@@ -294,7 +294,7 @@ const PresetScreen = ({ existingSounds, outdatedReasons, onNext, onReapply, onBa
294
294
 
295
295
  // ── Screen: Preview ─────────────────────────────────────────────
296
296
  const PreviewScreen = ({ presetId, sounds, onAccept, onBack, onUpdateSound }) => {
297
- const preset = PRESETS[presetId];
297
+ const preset = PRESETS[presetId] ?? PRESETS[Object.keys(PRESETS)[0]];
298
298
  const eventIds = Object.keys(EVENTS);
299
299
  const [currentEvent, setCurrentEvent] = useState(0);
300
300
  const [playing, setPlaying] = useState(false);
@@ -1808,7 +1808,8 @@ const InstallApp = () => {
1808
1808
  onConfirm: () => setScreen(SCREEN.INSTALLING),
1809
1809
  onBack: () => {
1810
1810
  if (selectedGame) setScreen(SCREEN.GAME_SOUNDS);
1811
- else setScreen(SCREEN.PREVIEW);
1811
+ else if (presetId) setScreen(SCREEN.PREVIEW);
1812
+ else setScreen(SCREEN.PRESET);
1812
1813
  },
1813
1814
  });
1814
1815
 
package/src/installer.js CHANGED
@@ -1,13 +1,9 @@
1
1
  import { readFile, writeFile, mkdir, copyFile } from "node:fs/promises";
2
2
  import { join, basename, extname } from "node:path";
3
3
  import { homedir } from "node:os";
4
- import { createRequire } from "node:module";
5
4
  import { getHookPlayCommand, processSound } from "./player.js";
6
5
  import { EVENTS } from "./presets.js";
7
6
 
8
- const _require = createRequire(import.meta.url);
9
- const KLAUDIO_VERSION = _require("../package.json").version;
10
-
11
7
  /**
12
8
  * Get the target directory based on install scope.
13
9
  */
@@ -39,7 +35,10 @@ export async function install({ scope, sounds, tts = false, voice, speed } = {})
39
35
  const installedSounds = {};
40
36
  for (const [eventId, sourcePath] of Object.entries(sounds)) {
41
37
  const processedPath = await processSound(sourcePath);
42
- const srcName = basename(sourcePath, extname(sourcePath));
38
+ // Strip any accumulated eventId prefixes from previous installs
39
+ let srcName = basename(sourcePath, extname(sourcePath));
40
+ const prefixRe = new RegExp(`^(${Object.keys(EVENTS).join("|")})-`, "i");
41
+ while (prefixRe.test(srcName)) srcName = srcName.replace(prefixRe, "");
43
42
  const outExt = extname(processedPath) || ".wav";
44
43
  const fileName = `${eventId}-${srcName}${outExt}`;
45
44
  const destPath = join(soundsDir, fileName);
@@ -88,8 +87,6 @@ export async function install({ scope, sounds, tts = false, voice, speed } = {})
88
87
 
89
88
  // Add our hook
90
89
  settings.hooks[hookEvent].push({
91
- _klaudio: true,
92
- _klaudioVersion: KLAUDIO_VERSION,
93
90
  matcher: "",
94
91
  hooks: [
95
92
  {
@@ -119,37 +116,47 @@ export async function install({ scope, sounds, tts = false, voice, speed } = {})
119
116
  */
120
117
  async function installApprovalHooks(settings, soundPath, claudeDir) {
121
118
  const normalized = soundPath.replace(/\\/g, "/");
122
- const scriptPath = join(claudeDir, "approval-notify.sh").replace(/\\/g, "/");
123
-
124
- // Write the timer script
125
- const script = `#!/usr/bin/env bash
126
- # klaudio: approval notification timer
127
- # Plays a sound + sends a system notification if a tool isn't approved within DELAY seconds.
128
- DELAY=120
129
- MARKER="/tmp/.claude-approval-pending"
130
- SOUND="${normalized}"
131
-
132
- case "$1" in
133
- start)
134
- TOKEN="$$-$(date +%s%N)"
135
- # Store token and CWD so the delayed notification knows the project name
136
- echo "$TOKEN" > "$MARKER"
137
- echo "$PWD" >> "$MARKER"
138
- (
139
- sleep "$DELAY"
140
- if [ -f "$MARKER" ] && [ "$(head -1 "$MARKER" 2>/dev/null)" = "$TOKEN" ]; then
141
- PROJECT=$(tail -1 "$MARKER" 2>/dev/null | sed 's|.*[/\\\\]||')
142
- rm -f "$MARKER"
143
- npx klaudio play "$SOUND" 2>/dev/null
144
- npx klaudio notify "\${PROJECT:-project}" "Waiting for your approval" 2>/dev/null
145
- npx klaudio say "\${PROJECT:-project} needs your attention" 2>/dev/null
146
- fi
147
- ) &
148
- ;;
149
- cancel)
150
- rm -f "$MARKER"
151
- ;;
152
- esac
119
+ const scriptPath = join(claudeDir, "klaudio-approval-notify.js").replace(/\\/g, "/");
120
+
121
+ // Write the timer script (CJS so it works outside any module context)
122
+ const script = `#!/usr/bin/env node
123
+ 'use strict';
124
+ const { writeFileSync, readFileSync, unlinkSync } = require('node:fs');
125
+ const { tmpdir } = require('node:os');
126
+ const { join } = require('node:path');
127
+ const { spawn, spawnSync } = require('node:child_process');
128
+
129
+ const DELAY_MS = 120000;
130
+ const MARKER = join(tmpdir(), '.claude-approval-pending');
131
+ const SOUND = "${normalized}";
132
+
133
+ const action = process.argv[2];
134
+
135
+ if (action === 'start') {
136
+ const token = process.pid + '-' + Date.now();
137
+ writeFileSync(MARKER, token + '\\n' + process.cwd() + '\\n', 'utf-8');
138
+ const child = spawn(process.execPath, [__filename, 'timer', token], {
139
+ detached: true,
140
+ stdio: 'ignore',
141
+ });
142
+ child.unref();
143
+ } else if (action === 'cancel') {
144
+ try { unlinkSync(MARKER); } catch (_) {}
145
+ } else if (action === 'timer') {
146
+ const token = process.argv[3];
147
+ setTimeout(function () {
148
+ try {
149
+ const content = readFileSync(MARKER, 'utf-8');
150
+ const lines = content.trim().split('\\n');
151
+ if (lines[0] !== token) return;
152
+ unlinkSync(MARKER);
153
+ const project = (lines[1] || '').split(/[\\/\\\\]/).pop() || 'project';
154
+ spawnSync('npx', ['--yes', 'klaudio', 'play', SOUND], { stdio: 'ignore' });
155
+ spawnSync('npx', ['--yes', 'klaudio', 'notify', project, 'Waiting for your approval'], { stdio: 'ignore' });
156
+ spawnSync('npx', ['--yes', 'klaudio', 'say', project + ' needs your attention'], { stdio: 'ignore' });
157
+ } catch (_) {}
158
+ }, DELAY_MS);
159
+ }
153
160
  `;
154
161
  await writeFile(scriptPath, script, "utf-8");
155
162
 
@@ -159,9 +166,8 @@ esac
159
166
  (e) => !e._klaudio && !e._klonk
160
167
  );
161
168
  settings.hooks.PreToolUse.push({
162
- _klaudio: true,
163
169
  matcher: "",
164
- hooks: [{ type: "command", command: `bash "${scriptPath}" start` }],
170
+ hooks: [{ type: "command", command: `node "${scriptPath}" start` }],
165
171
  });
166
172
 
167
173
  // Add PostToolUse hook
@@ -170,9 +176,8 @@ esac
170
176
  (e) => !e._klaudio && !e._klonk
171
177
  );
172
178
  settings.hooks.PostToolUse.push({
173
- _klaudio: true,
174
179
  matcher: "",
175
- hooks: [{ type: "command", command: `bash "${scriptPath}" cancel` }],
180
+ hooks: [{ type: "command", command: `node "${scriptPath}" cancel` }],
176
181
  });
177
182
  }
178
183
 
@@ -216,7 +221,6 @@ async function installCopilotHooks(installedSounds, scope) {
216
221
  );
217
222
 
218
223
  config.hooks[event.copilotHookEvent].push({
219
- _klaudio: true,
220
224
  type: "command",
221
225
  bash: bashCmd,
222
226
  powershell: psCmd,
@@ -243,12 +247,12 @@ export async function getExistingSounds(scope) {
243
247
  if (!settings.hooks) return sounds;
244
248
 
245
249
  for (const [eventId, event] of Object.entries(EVENTS)) {
246
- // Approval event: read sound from the approval-notify.sh script
250
+ // Approval event: read sound from the klaudio-approval-notify.js script
247
251
  if (eventId === "approval") {
248
- const scriptPath = join(claudeDir, "approval-notify.sh");
252
+ const scriptPath = join(claudeDir, "klaudio-approval-notify.js");
249
253
  try {
250
254
  const script = await readFile(scriptPath, "utf-8");
251
- const m = script.match(/SOUND="([^"]+\.(wav|mp3|ogg|flac|aac))"/);
255
+ const m = script.match(/SOUND\s*=\s*"([^"]+\.(wav|mp3|ogg|flac|aac))"/);
252
256
  if (m) {
253
257
  sounds[eventId] = m[1].replace(/\//g, join("a", "b").includes("\\") ? "\\" : "/");
254
258
  }
@@ -292,18 +296,6 @@ export async function checkHooksOutdated(scope) {
292
296
 
293
297
  const isOurs = (e) => e._klaudio || e._klonk || e.hooks?.[0]?.command?.includes("klaudio") || e.hooks?.[0]?.command?.includes(".claude/sounds/");
294
298
 
295
- // Check if any klaudio hook was installed with an older version
296
- const allKlaudioHooks = Object.values(settings.hooks).flat().filter(isOurs);
297
- if (allKlaudioHooks.length > 0) {
298
- const hasVersionMismatch = allKlaudioHooks.some((e) => e._klaudioVersion !== KLAUDIO_VERSION);
299
- if (hasVersionMismatch) {
300
- const installedVer = allKlaudioHooks.find((e) => e._klaudioVersion)?._klaudioVersion;
301
- reasons.push(installedVer
302
- ? `Hooks from v${installedVer} → v${KLAUDIO_VERSION}`
303
- : `Hooks need update to v${KLAUDIO_VERSION}`);
304
- }
305
- }
306
-
307
299
  // Check Stop hook for --notify flag
308
300
  const stopEntries = settings.hooks.Stop || [];
309
301
  const stopHook = stopEntries.find(isOurs);
@@ -318,22 +310,25 @@ export async function checkHooksOutdated(scope) {
318
310
  reasons.push("System notifications on background task");
319
311
  }
320
312
 
321
- // Check approval script for notify command and timer delay
322
- const scriptPath = join(claudeDir, "approval-notify.sh");
313
+ // Check approval script exists as the new Node.js version
314
+ const oldScriptPath = join(claudeDir, "approval-notify.sh");
315
+ const newScriptPath = join(claudeDir, "klaudio-approval-notify.js");
323
316
  try {
324
- const script = await readFile(scriptPath, "utf-8");
325
- if (!script.includes("klaudio notify")) {
326
- reasons.push("System notifications on approval wait");
327
- }
328
- const delayMatch = script.match(/DELAY=(\d+)/);
329
- if (delayMatch && parseInt(delayMatch[1]) < 120) {
317
+ await readFile(oldScriptPath, "utf-8");
318
+ // Old bash script still present — needs upgrade
319
+ reasons.push("Approval timer upgraded to cross-platform Node.js");
320
+ } catch { /* old script gone, good */ }
321
+ try {
322
+ const script = await readFile(newScriptPath, "utf-8");
323
+ const delayMatch = script.match(/DELAY_MS\s*=\s*(\d+)/);
324
+ if (delayMatch && parseInt(delayMatch[1]) < 120000) {
330
325
  reasons.push("Approval timer too short (now 120s)");
331
326
  }
332
327
  } catch { /* no script */ }
333
328
 
334
329
  // Check for duplicate hooks (old bug)
335
330
  for (const [event, entries] of Object.entries(settings.hooks)) {
336
- const klaudioEntries = entries.filter((e) => e._klaudio || e._klonk);
331
+ const klaudioEntries = entries.filter(isOurs);
337
332
  if (klaudioEntries.length > 1) {
338
333
  reasons.push(`Duplicate ${event} hooks`);
339
334
  }
package/src/player.js CHANGED
@@ -371,20 +371,22 @@ export async function handlePlayCommand(args) {
371
371
  const soundFile = args.find((a) => !a.startsWith("-"));
372
372
  const tts = args.includes("--tts");
373
373
 
374
- // Read stdin (hook JSON) non-blocking
374
+ // Read stdin (hook JSON) only when TTS or notifications need it
375
375
  let hookData = {};
376
- try {
377
- const chunks = [];
378
- process.stdin.setEncoding("utf-8");
379
- // Read whatever is available with a short timeout
380
- const stdinData = await new Promise((res) => {
381
- const timer = setTimeout(() => { process.stdin.pause(); res(chunks.join("")); }, 500);
382
- process.stdin.on("data", (chunk) => chunks.push(chunk));
383
- process.stdin.on("end", () => { clearTimeout(timer); res(chunks.join("")); });
384
- process.stdin.resume();
385
- });
386
- if (stdinData.trim()) hookData = JSON.parse(stdinData);
387
- } catch { /* no stdin or invalid JSON */ }
376
+ if (tts || args.includes("--notify")) {
377
+ try {
378
+ const chunks = [];
379
+ process.stdin.setEncoding("utf-8");
380
+ // Read whatever is available with a short timeout
381
+ const stdinData = await new Promise((res) => {
382
+ const timer = setTimeout(() => { process.stdin.pause(); res(chunks.join("")); }, 500);
383
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
384
+ process.stdin.on("end", () => { clearTimeout(timer); res(chunks.join("")); });
385
+ process.stdin.resume();
386
+ });
387
+ if (stdinData.trim()) hookData = JSON.parse(stdinData);
388
+ } catch { /* no stdin or invalid JSON */ }
389
+ }
388
390
 
389
391
  const notify = args.includes("--notify");
390
392
 
@@ -497,5 +499,5 @@ export function getHookPlayCommand(soundFilePath, { tts = false, voice, speed, n
497
499
  const voiceFlag = tts && voice ? ` --voice ${voice}` : "";
498
500
  const speedFlag = tts && speed && speed !== 1.0 ? ` --speed ${speed}` : "";
499
501
  const notifyFlag = notify ? " --notify" : "";
500
- return `npx klaudio play "${normalized}"${ttsFlag}${voiceFlag}${speedFlag}${notifyFlag}`;
502
+ return `npx --yes klaudio play "${normalized}"${ttsFlag}${voiceFlag}${speedFlag}${notifyFlag}`;
501
503
  }