klaudio 0.1.0 → 0.3.0

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/src/installer.js CHANGED
@@ -1,128 +1,161 @@
1
- import { readFile, writeFile, mkdir, copyFile } from "node:fs/promises";
2
- import { join, basename, extname } from "node:path";
3
- import { homedir } from "node:os";
4
- import { getHookPlayCommand, processSound } from "./player.js";
5
- import { EVENTS } from "./presets.js";
6
-
7
- /**
8
- * Get the target directory based on install scope.
9
- */
10
- function getTargetDir(scope) {
11
- if (scope === "global") {
12
- return join(homedir(), ".claude");
13
- }
14
- return join(process.cwd(), ".claude");
15
- }
16
-
17
- /**
18
- * Install sounds and configure hooks.
19
- *
20
- * @param {object} options
21
- * @param {string} options.scope - "global" or "project"
22
- * @param {Record<string, string>} options.sounds - Map of event ID -> source sound file path
23
- */
24
- export async function install({ scope, sounds }) {
25
- const claudeDir = getTargetDir(scope);
26
- const soundsDir = join(claudeDir, "sounds");
27
- const settingsFile = join(claudeDir, "settings.json");
28
-
29
- // Create sounds directory
30
- await mkdir(soundsDir, { recursive: true });
31
-
32
- // Process and copy sound files (clamp to 10s with fadeout via ffmpeg)
33
- const installedSounds = {};
34
- for (const [eventId, sourcePath] of Object.entries(sounds)) {
35
- const processedPath = await processSound(sourcePath);
36
- const srcName = basename(sourcePath, extname(sourcePath));
37
- const outExt = extname(processedPath) || ".wav";
38
- const fileName = `${eventId}-${srcName}${outExt}`;
39
- const destPath = join(soundsDir, fileName);
40
- await copyFile(processedPath, destPath);
41
- installedSounds[eventId] = destPath;
42
- }
43
-
44
- // Read existing settings
45
- let settings = {};
46
- try {
47
- const existing = await readFile(settingsFile, "utf-8");
48
- settings = JSON.parse(existing);
49
- } catch {
50
- // File doesn't exist or is invalid — start fresh
51
- }
52
-
53
- // Build hooks config
54
- if (!settings.hooks) {
55
- settings.hooks = {};
56
- }
57
-
58
- for (const [eventId, soundPath] of Object.entries(installedSounds)) {
59
- const event = EVENTS[eventId];
60
- if (!event) continue;
61
-
62
- const hookEvent = event.hookEvent;
63
- const playCommand = getHookPlayCommand(soundPath);
64
-
65
- // Check if there's already a klonk hook for this event
66
- if (!settings.hooks[hookEvent]) {
67
- settings.hooks[hookEvent] = [];
68
- }
69
-
70
- // Remove any existing klonk entries
71
- settings.hooks[hookEvent] = settings.hooks[hookEvent].filter(
72
- (entry) => !entry._klonk
73
- );
74
-
75
- // Add our hook
76
- settings.hooks[hookEvent].push({
77
- _klonk: true,
78
- matcher: "",
79
- hooks: [
80
- {
81
- type: "command",
82
- command: playCommand,
83
- },
84
- ],
85
- });
86
- }
87
-
88
- // Write settings
89
- await writeFile(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf-8");
90
-
91
- return {
92
- soundsDir,
93
- settingsFile,
94
- installedSounds,
95
- };
96
- }
97
-
98
- /**
99
- * Uninstall klonk hooks from settings.
100
- */
101
- export async function uninstall(scope) {
102
- const claudeDir = getTargetDir(scope);
103
- const settingsFile = join(claudeDir, "settings.json");
104
-
105
- try {
106
- const existing = await readFile(settingsFile, "utf-8");
107
- const settings = JSON.parse(existing);
108
-
109
- if (settings.hooks) {
110
- for (const [event, entries] of Object.entries(settings.hooks)) {
111
- settings.hooks[event] = entries.filter(
112
- (entry) => !entry._klonk
113
- );
114
- if (settings.hooks[event].length === 0) {
115
- delete settings.hooks[event];
116
- }
117
- }
118
- if (Object.keys(settings.hooks).length === 0) {
119
- delete settings.hooks;
120
- }
121
- }
122
-
123
- await writeFile(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf-8");
124
- return true;
125
- } catch {
126
- return false;
127
- }
128
- }
1
+ import { readFile, writeFile, mkdir, copyFile } from "node:fs/promises";
2
+ import { join, basename, extname } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { getHookPlayCommand, processSound } from "./player.js";
5
+ import { EVENTS } from "./presets.js";
6
+
7
+ /**
8
+ * Get the target directory based on install scope.
9
+ */
10
+ function getTargetDir(scope) {
11
+ if (scope === "global") {
12
+ return join(homedir(), ".claude");
13
+ }
14
+ return join(process.cwd(), ".claude");
15
+ }
16
+
17
+ /**
18
+ * Install sounds and configure hooks.
19
+ *
20
+ * @param {object} options
21
+ * @param {string} options.scope - "global" or "project"
22
+ * @param {Record<string, string>} options.sounds - Map of event ID -> source sound file path
23
+ */
24
+ export async function install({ scope, sounds }) {
25
+ const claudeDir = getTargetDir(scope);
26
+ const soundsDir = join(claudeDir, "sounds");
27
+ const settingsFile = join(claudeDir, "settings.json");
28
+
29
+ // Create sounds directory
30
+ await mkdir(soundsDir, { recursive: true });
31
+
32
+ // Process and copy sound files (clamp to 10s with fadeout via ffmpeg)
33
+ const installedSounds = {};
34
+ for (const [eventId, sourcePath] of Object.entries(sounds)) {
35
+ const processedPath = await processSound(sourcePath);
36
+ const srcName = basename(sourcePath, extname(sourcePath));
37
+ const outExt = extname(processedPath) || ".wav";
38
+ const fileName = `${eventId}-${srcName}${outExt}`;
39
+ const destPath = join(soundsDir, fileName);
40
+ await copyFile(processedPath, destPath);
41
+ installedSounds[eventId] = destPath;
42
+ }
43
+
44
+ // Read existing settings
45
+ let settings = {};
46
+ try {
47
+ const existing = await readFile(settingsFile, "utf-8");
48
+ settings = JSON.parse(existing);
49
+ } catch {
50
+ // File doesn't exist or is invalid — start fresh
51
+ }
52
+
53
+ // Build hooks config
54
+ if (!settings.hooks) {
55
+ settings.hooks = {};
56
+ }
57
+
58
+ for (const [eventId, soundPath] of Object.entries(installedSounds)) {
59
+ const event = EVENTS[eventId];
60
+ if (!event) continue;
61
+
62
+ const hookEvent = event.hookEvent;
63
+ const playCommand = getHookPlayCommand(soundPath);
64
+
65
+ // Check if there's already a klonk hook for this event
66
+ if (!settings.hooks[hookEvent]) {
67
+ settings.hooks[hookEvent] = [];
68
+ }
69
+
70
+ // Remove any existing klonk entries
71
+ settings.hooks[hookEvent] = settings.hooks[hookEvent].filter(
72
+ (entry) => !entry._klonk
73
+ );
74
+
75
+ // Add our hook
76
+ settings.hooks[hookEvent].push({
77
+ _klonk: true,
78
+ matcher: "",
79
+ hooks: [
80
+ {
81
+ type: "command",
82
+ command: playCommand,
83
+ },
84
+ ],
85
+ });
86
+ }
87
+
88
+ // Write settings
89
+ await writeFile(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf-8");
90
+
91
+ return {
92
+ soundsDir,
93
+ settingsFile,
94
+ installedSounds,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Read existing klaudio sound selections from settings.
100
+ * Returns a map of eventId -> soundFilePath (from the sounds/ dir).
101
+ */
102
+ export async function getExistingSounds(scope) {
103
+ const claudeDir = getTargetDir(scope);
104
+ const settingsFile = join(claudeDir, "settings.json");
105
+ const sounds = {};
106
+
107
+ try {
108
+ const existing = await readFile(settingsFile, "utf-8");
109
+ const settings = JSON.parse(existing);
110
+ if (!settings.hooks) return sounds;
111
+
112
+ for (const [eventId, event] of Object.entries(EVENTS)) {
113
+ const hookEntries = settings.hooks[event.hookEvent];
114
+ if (!hookEntries) continue;
115
+ const entry = hookEntries.find((e) => e._klonk);
116
+ if (!entry?.hooks?.[0]?.command) continue;
117
+
118
+ // Extract file path from the play command
119
+ // Commands contain the path in quotes: ... "path/to/file" ...
120
+ const match = entry.hooks[0].command.match(/"([^"]+\.(wav|mp3|ogg|flac|aac))"/);
121
+ if (match) {
122
+ const soundPath = match[1].replace(/\//g, join("a", "b").includes("\\") ? "\\" : "/");
123
+ sounds[eventId] = soundPath;
124
+ }
125
+ }
126
+ } catch { /* no existing config */ }
127
+
128
+ return sounds;
129
+ }
130
+
131
+ /**
132
+ * Uninstall klonk hooks from settings.
133
+ */
134
+ export async function uninstall(scope) {
135
+ const claudeDir = getTargetDir(scope);
136
+ const settingsFile = join(claudeDir, "settings.json");
137
+
138
+ try {
139
+ const existing = await readFile(settingsFile, "utf-8");
140
+ const settings = JSON.parse(existing);
141
+
142
+ if (settings.hooks) {
143
+ for (const [event, entries] of Object.entries(settings.hooks)) {
144
+ settings.hooks[event] = entries.filter(
145
+ (entry) => !entry._klonk
146
+ );
147
+ if (settings.hooks[event].length === 0) {
148
+ delete settings.hooks[event];
149
+ }
150
+ }
151
+ if (Object.keys(settings.hooks).length === 0) {
152
+ delete settings.hooks;
153
+ }
154
+ }
155
+
156
+ await writeFile(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf-8");
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }