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/README.md +67 -65
- package/package.json +44 -40
- package/src/cache.js +262 -262
- package/src/cli.js +1205 -999
- package/src/extractor.js +203 -203
- package/src/installer.js +161 -128
- package/src/player.js +359 -359
- package/src/presets.js +5 -5
- package/src/scanner.js +41 -6
- package/src/unity.js +241 -0
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
|
-
*
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|