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 +21 -0
- package/package.json +1 -1
- package/src/cli.js +3 -2
- package/src/installer.js +62 -67
- package/src/player.js +16 -14
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
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
|
-
|
|
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.
|
|
123
|
-
|
|
124
|
-
// Write the timer script
|
|
125
|
-
const script = `#!/usr/bin/env
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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: `
|
|
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: `
|
|
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.
|
|
250
|
+
// Approval event: read sound from the klaudio-approval-notify.js script
|
|
247
251
|
if (eventId === "approval") {
|
|
248
|
-
const scriptPath = join(claudeDir, "approval-notify.
|
|
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
|
|
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
|
|
322
|
-
const
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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(
|
|
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)
|
|
374
|
+
// Read stdin (hook JSON) only when TTS or notifications need it
|
|
375
375
|
let hookData = {};
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
}
|