klaudio 0.13.1 → 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.1",
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,39 +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
- const cliPath = new URL("../bin/cli.js", import.meta.url).pathname;
124
-
125
- // Write the timer script
126
- const script = `#!/usr/bin/env bash
127
- # klaudio: approval notification timer
128
- # Plays a sound + sends a system notification if a tool isn't approved within DELAY seconds.
129
- DELAY=120
130
- MARKER="/tmp/.claude-approval-pending"
131
- SOUND="${normalized}"
132
- CLI="${cliPath}"
133
-
134
- case "$1" in
135
- start)
136
- TOKEN="$$-$(date +%s%N)"
137
- # Store token and CWD so the delayed notification knows the project name
138
- echo "$TOKEN" > "$MARKER"
139
- echo "$PWD" >> "$MARKER"
140
- (
141
- sleep "$DELAY"
142
- if [ -f "$MARKER" ] && [ "$(head -1 "$MARKER" 2>/dev/null)" = "$TOKEN" ]; then
143
- PROJECT=$(tail -1 "$MARKER" 2>/dev/null | sed 's|.*[/\\\\]||')
144
- rm -f "$MARKER"
145
- node "$CLI" play "$SOUND" 2>/dev/null
146
- node "$CLI" notify "\${PROJECT:-project}" "Waiting for your approval" 2>/dev/null
147
- node "$CLI" say "\${PROJECT:-project} needs your attention" 2>/dev/null
148
- fi
149
- ) &
150
- ;;
151
- cancel)
152
- rm -f "$MARKER"
153
- ;;
154
- 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
+ }
155
160
  `;
156
161
  await writeFile(scriptPath, script, "utf-8");
157
162
 
@@ -161,9 +166,8 @@ esac
161
166
  (e) => !e._klaudio && !e._klonk
162
167
  );
163
168
  settings.hooks.PreToolUse.push({
164
- _klaudio: true,
165
169
  matcher: "",
166
- hooks: [{ type: "command", command: `bash "${scriptPath}" start` }],
170
+ hooks: [{ type: "command", command: `node "${scriptPath}" start` }],
167
171
  });
168
172
 
169
173
  // Add PostToolUse hook
@@ -172,9 +176,8 @@ esac
172
176
  (e) => !e._klaudio && !e._klonk
173
177
  );
174
178
  settings.hooks.PostToolUse.push({
175
- _klaudio: true,
176
179
  matcher: "",
177
- hooks: [{ type: "command", command: `bash "${scriptPath}" cancel` }],
180
+ hooks: [{ type: "command", command: `node "${scriptPath}" cancel` }],
178
181
  });
179
182
  }
180
183
 
@@ -218,7 +221,6 @@ async function installCopilotHooks(installedSounds, scope) {
218
221
  );
219
222
 
220
223
  config.hooks[event.copilotHookEvent].push({
221
- _klaudio: true,
222
224
  type: "command",
223
225
  bash: bashCmd,
224
226
  powershell: psCmd,
@@ -245,12 +247,12 @@ export async function getExistingSounds(scope) {
245
247
  if (!settings.hooks) return sounds;
246
248
 
247
249
  for (const [eventId, event] of Object.entries(EVENTS)) {
248
- // Approval event: read sound from the approval-notify.sh script
250
+ // Approval event: read sound from the klaudio-approval-notify.js script
249
251
  if (eventId === "approval") {
250
- const scriptPath = join(claudeDir, "approval-notify.sh");
252
+ const scriptPath = join(claudeDir, "klaudio-approval-notify.js");
251
253
  try {
252
254
  const script = await readFile(scriptPath, "utf-8");
253
- const m = script.match(/SOUND="([^"]+\.(wav|mp3|ogg|flac|aac))"/);
255
+ const m = script.match(/SOUND\s*=\s*"([^"]+\.(wav|mp3|ogg|flac|aac))"/);
254
256
  if (m) {
255
257
  sounds[eventId] = m[1].replace(/\//g, join("a", "b").includes("\\") ? "\\" : "/");
256
258
  }
@@ -294,18 +296,6 @@ export async function checkHooksOutdated(scope) {
294
296
 
295
297
  const isOurs = (e) => e._klaudio || e._klonk || e.hooks?.[0]?.command?.includes("klaudio") || e.hooks?.[0]?.command?.includes(".claude/sounds/");
296
298
 
297
- // Check if any klaudio hook was installed with an older version
298
- const allKlaudioHooks = Object.values(settings.hooks).flat().filter(isOurs);
299
- if (allKlaudioHooks.length > 0) {
300
- const hasVersionMismatch = allKlaudioHooks.some((e) => e._klaudioVersion !== KLAUDIO_VERSION);
301
- if (hasVersionMismatch) {
302
- const installedVer = allKlaudioHooks.find((e) => e._klaudioVersion)?._klaudioVersion;
303
- reasons.push(installedVer
304
- ? `Hooks from v${installedVer} → v${KLAUDIO_VERSION}`
305
- : `Hooks need update to v${KLAUDIO_VERSION}`);
306
- }
307
- }
308
-
309
299
  // Check Stop hook for --notify flag
310
300
  const stopEntries = settings.hooks.Stop || [];
311
301
  const stopHook = stopEntries.find(isOurs);
@@ -320,22 +310,25 @@ export async function checkHooksOutdated(scope) {
320
310
  reasons.push("System notifications on background task");
321
311
  }
322
312
 
323
- // Check approval script for notify command and timer delay
324
- 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");
325
316
  try {
326
- const script = await readFile(scriptPath, "utf-8");
327
- if (!script.includes("klaudio notify")) {
328
- reasons.push("System notifications on approval wait");
329
- }
330
- const delayMatch = script.match(/DELAY=(\d+)/);
331
- 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) {
332
325
  reasons.push("Approval timer too short (now 120s)");
333
326
  }
334
327
  } catch { /* no script */ }
335
328
 
336
329
  // Check for duplicate hooks (old bug)
337
330
  for (const [event, entries] of Object.entries(settings.hooks)) {
338
- const klaudioEntries = entries.filter((e) => e._klaudio || e._klonk);
331
+ const klaudioEntries = entries.filter(isOurs);
339
332
  if (klaudioEntries.length > 1) {
340
333
  reasons.push(`Duplicate ${event} hooks`);
341
334
  }
package/src/player.js CHANGED
@@ -499,8 +499,5 @@ export function getHookPlayCommand(soundFilePath, { tts = false, voice, speed, n
499
499
  const voiceFlag = tts && voice ? ` --voice ${voice}` : "";
500
500
  const speedFlag = tts && speed && speed !== 1.0 ? ` --speed ${speed}` : "";
501
501
  const notifyFlag = notify ? " --notify" : "";
502
- // Resolve the CLI path at install time to avoid npx overhead on every hook call.
503
- // When klaudio is updated and re-installed, hooks are rewritten with the new path.
504
- const cliPath = new URL("../bin/cli.js", import.meta.url).pathname;
505
- return `node "${cliPath}" play "${normalized}"${ttsFlag}${voiceFlag}${speedFlag}${notifyFlag}`;
502
+ return `npx --yes klaudio play "${normalized}"${ttsFlag}${voiceFlag}${speedFlag}${notifyFlag}`;
506
503
  }