klaudio 0.5.1 → 0.5.3

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.5.1",
3
+ "version": "0.5.3",
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/cache.js CHANGED
@@ -14,9 +14,9 @@ const CATEGORY_PATTERNS = {
14
14
  "forest", "weather", "background_loop", "room_tone",
15
15
  ],
16
16
  music: [
17
- "music", "mus_", "soundtrack", "bgm", "score", "theme",
17
+ "music", "mus_", "/music/", "soundtrack", "bgm", "score", "theme",
18
18
  "menu_music", "mainmenu", "background_music", "level_music",
19
- "snippet",
19
+ "snippet", "musdisk",
20
20
  ],
21
21
  sfx: [
22
22
  "sfx", "effect", "impact", "explosion", "hit", "slash",
@@ -31,9 +31,9 @@ const CATEGORY_PATTERNS = {
31
31
  "interface", "hud", "tab", "scroll",
32
32
  ],
33
33
  voice: [
34
- "voice", "vocal", "vox", "dialogue", "dialog", "speech",
34
+ "voice", "vocal", "vox", "/voice/", "dialogue", "dialog", "speech",
35
35
  "narrat", "speak", "talk", "grunt", "shout", "scream",
36
- "laugh", "cry", "cheer",
36
+ "laugh", "cry", "cheer", "voxdisk",
37
37
  ],
38
38
  creature: [
39
39
  "creature", "animal", "monster", "enemy", "npc",
@@ -42,6 +42,15 @@ const CATEGORY_PATTERNS = {
42
42
  ],
43
43
  };
44
44
 
45
+ /**
46
+ * SCUMM BUN filename prefix patterns.
47
+ * These are checked before generic patterns for files from BUN extraction.
48
+ */
49
+ const SCUMM_PREFIX_PATTERNS = {
50
+ voice: [/^ad[a-z]/i, /^adgt/i, /^adso/i],
51
+ music: [/^\d{4}-[a-z]_/i],
52
+ };
53
+
45
54
  /**
46
55
  * Infer a category from a filename, its parent folder path, and optional metadata name.
47
56
  */
@@ -50,6 +59,14 @@ export function inferCategory(filePath, metadataName) {
50
59
  const dirPath = dirname(filePath).toLowerCase().replace(/\\/g, "/");
51
60
  const combined = `${dirPath}/${name}`;
52
61
 
62
+ // Check SCUMM prefix patterns first (high-confidence filename-based)
63
+ const baseName = basename(name, extname(name));
64
+ for (const [cat, patterns] of Object.entries(SCUMM_PREFIX_PATTERNS)) {
65
+ for (const re of patterns) {
66
+ if (re.test(baseName)) return cat;
67
+ }
68
+ }
69
+
53
70
  // Check each category's patterns against the combined path+name
54
71
  const scores = {};
55
72
  for (const [cat, patterns] of Object.entries(CATEGORY_PATTERNS)) {
package/src/cli.js CHANGED
@@ -114,8 +114,8 @@ const NavHint = ({ back = true, extra = "" }) =>
114
114
  // ── Screen: Scope ───────────────────────────────────────────────
115
115
  const ScopeScreen = ({ onNext }) => {
116
116
  const items = [
117
- { label: "Global (~/.claude) sounds in all projects", value: "global" },
118
- { label: "This project (.claude/) project-specific", value: "project" },
117
+ { label: "Global — Claude Code + VS Code Copilot (all projects)", value: "global" },
118
+ { label: "This project — Claude Code + Copilot (incl. GitHub agent)", value: "project" },
119
119
  ];
120
120
  return h(Box, { flexDirection: "column" },
121
121
  h(Text, { bold: true }, " Where should sounds be installed?"),
@@ -555,7 +555,7 @@ const GameSoundsScreen = ({ game, sounds, onSelectSound, onDone, onBack }) => {
555
555
  if (prev) stopPlayback();
556
556
  return !prev;
557
557
  });
558
- } else if (activeCategory !== null) {
558
+ } else if (activeCategory !== null || !showCategoryPicker) {
559
559
  if (key.backspace || key.delete) {
560
560
  setFilter((f) => f.slice(0, -1));
561
561
  } else if (input && input !== "p" && !key.ctrl && !key.meta && input.length === 1 && input.charCodeAt(0) >= 32) {
@@ -973,7 +973,7 @@ const ConfirmScreen = ({ scope, sounds, onConfirm, onBack }) => {
973
973
  return h(Box, { flexDirection: "column" },
974
974
  h(Text, { bold: true, marginLeft: 2 }, " Ready to install:"),
975
975
  h(Box, { marginTop: 1, flexDirection: "column" },
976
- h(Text, { marginLeft: 4 }, `Scope: ${scope === "global" ? "Global (~/.claude)" : "This project (.claude/)"}`),
976
+ h(Text, { marginLeft: 4 }, `Scope: ${scope === "global" ? "Global (Claude Code + VS Code Copilot)" : "This project (Claude Code + Copilot + GitHub agent)"}`),
977
977
  ...soundEntries.map(([eid, path]) =>
978
978
  h(Text, { key: eid, marginLeft: 4 },
979
979
  `${EVENTS[eid].name} → ${basename(path)}`
package/src/installer.js CHANGED
@@ -88,6 +88,9 @@ export async function install({ scope, sounds }) {
88
88
  // Write settings
89
89
  await writeFile(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf-8");
90
90
 
91
+ // Also install Copilot coding agent hooks (.github/hooks/klaudio.json)
92
+ await installCopilotHooks(installedSounds, scope);
93
+
91
94
  return {
92
95
  soundsDir,
93
96
  settingsFile,
@@ -95,6 +98,58 @@ export async function install({ scope, sounds }) {
95
98
  };
96
99
  }
97
100
 
101
+ /**
102
+ * Install hooks for GitHub Copilot coding agent.
103
+ * Writes .github/hooks/klaudio.json in the Copilot format.
104
+ */
105
+ async function installCopilotHooks(installedSounds, scope) {
106
+ // Find the repo root (.github lives at repo root)
107
+ const repoRoot = scope === "global" ? null : process.cwd();
108
+ if (!repoRoot) return; // Copilot hooks are project-scoped only
109
+
110
+ const hooksDir = join(repoRoot, ".github", "hooks");
111
+ const hooksFile = join(hooksDir, "klaudio.json");
112
+
113
+ await mkdir(hooksDir, { recursive: true });
114
+
115
+ // Read existing file if present
116
+ let config = { version: 1, hooks: {} };
117
+ try {
118
+ const existing = await readFile(hooksFile, "utf-8");
119
+ config = JSON.parse(existing);
120
+ if (!config.hooks) config.hooks = {};
121
+ } catch { /* start fresh */ }
122
+
123
+ for (const [eventId, soundPath] of Object.entries(installedSounds)) {
124
+ const event = EVENTS[eventId];
125
+ if (!event?.copilotHookEvent) continue;
126
+
127
+ const normalized = soundPath.replace(/\\/g, "/");
128
+ const bashCmd = `afplay "${normalized}" 2>/dev/null & aplay "${normalized}" 2>/dev/null &`;
129
+ const psCmd = `Add-Type -AssemblyName PresentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([System.Uri]::new('${normalized.replace(/\//g, "\\")}')); Start-Sleep -Milliseconds 200; $p.Play(); Start-Sleep -Seconds 2`;
130
+
131
+ if (!config.hooks[event.copilotHookEvent]) {
132
+ config.hooks[event.copilotHookEvent] = [];
133
+ }
134
+
135
+ // Remove existing klaudio entries
136
+ config.hooks[event.copilotHookEvent] = config.hooks[event.copilotHookEvent].filter(
137
+ (entry) => !entry._klaudio
138
+ );
139
+
140
+ config.hooks[event.copilotHookEvent].push({
141
+ _klaudio: true,
142
+ type: "command",
143
+ bash: bashCmd,
144
+ powershell: psCmd,
145
+ timeoutSec: 10,
146
+ comment: `klaudio: ${event.name}`,
147
+ });
148
+ }
149
+
150
+ await writeFile(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf-8");
151
+ }
152
+
98
153
  /**
99
154
  * Read existing klaudio sound selections from settings.
100
155
  * Returns a map of eventId -> soundFilePath (from the sounds/ dir).
@@ -154,8 +209,22 @@ export async function uninstall(scope) {
154
209
  }
155
210
 
156
211
  await writeFile(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf-8");
157
- return true;
158
- } catch {
159
- return false;
160
- }
212
+ } catch { /* no existing config */ }
213
+
214
+ // Also clean up Copilot hooks
215
+ await uninstallCopilotHooks(scope);
216
+
217
+ return true;
218
+ }
219
+
220
+ /**
221
+ * Remove klaudio entries from .github/hooks/klaudio.json.
222
+ */
223
+ async function uninstallCopilotHooks(scope) {
224
+ if (scope === "global") return;
225
+ const hooksFile = join(process.cwd(), ".github", "hooks", "klaudio.json");
226
+ try {
227
+ const { unlink } = await import("node:fs/promises");
228
+ await unlink(hooksFile);
229
+ } catch { /* file doesn't exist */ }
161
230
  }
package/src/presets.js CHANGED
@@ -16,11 +16,13 @@ export const EVENTS = {
16
16
  name: "Notification",
17
17
  description: "Plays when Claude needs your attention",
18
18
  hookEvent: "Notification",
19
+ copilotHookEvent: null, // Copilot doesn't have this yet
19
20
  },
20
21
  stop: {
21
22
  name: "Task Complete",
22
23
  description: "Plays when Claude finishes a response",
23
24
  hookEvent: "Stop",
25
+ copilotHookEvent: "sessionEnd",
24
26
  },
25
27
  };
26
28
 
package/src/scumm.js CHANGED
@@ -517,11 +517,18 @@ export async function extractBunFile(bunPath, outputDir, onProgress) {
517
517
  const { sampleRate, bitsPerSample, channels, pcmData } = parseImusResource(fullBuf);
518
518
  if (!pcmData || pcmData.length === 0) continue;
519
519
 
520
- // Write WAV
520
+ // Write WAV into a subdirectory named after the BUN file
521
521
  const wav = createWav(pcmData, sampleRate, channels, bitsPerSample);
522
+ const bunName = basename(bunPath, extname(bunPath)).toLowerCase();
523
+ const subDir = bunName.includes("vox") ? "voice"
524
+ : bunName.includes("mus") ? "music"
525
+ : bunName.includes("sfx") ? "sfx"
526
+ : "other";
527
+ const bunOutputDir = join(outputDir, subDir);
528
+ await mkdir(bunOutputDir, { recursive: true });
522
529
  const safeName = entry.filename.replace(/[^a-zA-Z0-9._-]/g, "_");
523
530
  const outName = safeName.replace(/\.[^.]+$/, "") + ".wav";
524
- const outPath = join(outputDir, outName);
531
+ const outPath = join(bunOutputDir, outName);
525
532
  await writeFile(outPath, wav);
526
533
  outputs.push(outPath);
527
534
  } catch {