@mcptoolshop/claude-sfx 0.1.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/LICENSE +21 -0
- package/README.es.md +188 -0
- package/README.fr.md +188 -0
- package/README.hi.md +188 -0
- package/README.it.md +188 -0
- package/README.ja.md +188 -0
- package/README.md +188 -0
- package/README.pt-BR.md +188 -0
- package/README.zh.md +188 -0
- package/assets/logo.jpg +0 -0
- package/dist/ambient.d.ts +28 -0
- package/dist/ambient.js +179 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +594 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +104 -0
- package/dist/guard.d.ts +19 -0
- package/dist/guard.js +87 -0
- package/dist/hook-handler.d.ts +34 -0
- package/dist/hook-handler.js +200 -0
- package/dist/hooks.d.ts +35 -0
- package/dist/hooks.js +192 -0
- package/dist/player.d.ts +16 -0
- package/dist/player.js +109 -0
- package/dist/profiles.d.ts +103 -0
- package/dist/profiles.js +297 -0
- package/dist/synth.d.ts +72 -0
- package/dist/synth.js +254 -0
- package/dist/verbs.d.ts +25 -0
- package/dist/verbs.js +251 -0
- package/package.json +54 -0
- package/profiles/minimal.json +202 -0
- package/profiles/retro.json +200 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* claude-sfx CLI
|
|
4
|
+
* Procedural audio feedback for Claude Code.
|
|
5
|
+
*/
|
|
6
|
+
import { ALL_VERBS, VERB_LABELS, VERB_DESCRIPTIONS, generateVerb, generateSessionStart, generateSessionEnd, generateAmbientResolve, } from "./verbs.js";
|
|
7
|
+
import { concatBuffers, applyVolume } from "./synth.js";
|
|
8
|
+
import { playSync, saveWav } from "./player.js";
|
|
9
|
+
import { resolveProfile, listBuiltinProfiles, } from "./profiles.js";
|
|
10
|
+
import { loadConfig, saveConfig, setMuted, setVolume, setProfile, setQuietHours, clearQuietHours, resolveProfileName, volumeToGain, CONFIG_FILE, } from "./config.js";
|
|
11
|
+
import { guardPlay, resetLedger } from "./guard.js";
|
|
12
|
+
import { startAmbient, resolveAmbient, stopAmbient, isAmbientRunning, } from "./ambient.js";
|
|
13
|
+
import { installHooks, uninstallHooks } from "./hooks.js";
|
|
14
|
+
import { handleHook } from "./hook-handler.js";
|
|
15
|
+
import { mkdirSync, existsSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
// --- Argument parsing (zero-dep) ---
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const args = argv.slice(2);
|
|
20
|
+
const command = args[0] ?? "help";
|
|
21
|
+
const positional = [];
|
|
22
|
+
const flags = {};
|
|
23
|
+
for (let i = 1; i < args.length; i++) {
|
|
24
|
+
const arg = args[i];
|
|
25
|
+
if (arg.startsWith("--") && i + 1 < args.length) {
|
|
26
|
+
flags[arg.slice(2)] = args[++i];
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
positional.push(arg);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { command, positional, flags };
|
|
33
|
+
}
|
|
34
|
+
/** Resolve the active profile, respecting config + CLI override + repo overrides. */
|
|
35
|
+
function getProfile(flags) {
|
|
36
|
+
const config = loadConfig();
|
|
37
|
+
const name = flags.profile ?? resolveProfileName(config, process.cwd());
|
|
38
|
+
try {
|
|
39
|
+
return resolveProfile(name);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
console.error(` Error: ${e.message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// --- Commands ---
|
|
47
|
+
function cmdDemo(flags) {
|
|
48
|
+
const profile = getProfile(flags);
|
|
49
|
+
console.log(` claude-sfx demo [${profile.name}]\n`);
|
|
50
|
+
console.log(` ${profile.description}\n`);
|
|
51
|
+
for (const verb of ALL_VERBS) {
|
|
52
|
+
const label = VERB_LABELS[verb].padEnd(10);
|
|
53
|
+
const desc = VERB_DESCRIPTIONS[verb];
|
|
54
|
+
process.stdout.write(` ${label} ${desc}`);
|
|
55
|
+
const buffer = generateVerb(profile, verb);
|
|
56
|
+
const result = playSync(buffer);
|
|
57
|
+
if (result.played) {
|
|
58
|
+
console.log(` (${result.durationMs}ms)`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(" [no audio output available]");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
console.log("\n Session sounds:\n");
|
|
65
|
+
process.stdout.write(" Start boot chime");
|
|
66
|
+
const startResult = playSync(generateSessionStart(profile));
|
|
67
|
+
console.log(startResult.played ? ` (${startResult.durationMs}ms)` : " [no audio]");
|
|
68
|
+
process.stdout.write(" End closure");
|
|
69
|
+
const endResult = playSync(generateSessionEnd(profile));
|
|
70
|
+
console.log(endResult.played ? ` (${endResult.durationMs}ms)` : " [no audio]");
|
|
71
|
+
process.stdout.write(" Resolve stinger");
|
|
72
|
+
const resolveResult = playSync(generateAmbientResolve(profile));
|
|
73
|
+
console.log(resolveResult.played ? ` (${resolveResult.durationMs}ms)` : " [no audio]");
|
|
74
|
+
console.log("\n Done.\n");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Play a verb sound — the main path called by hooks.
|
|
78
|
+
* Goes through the full guard system (debounce, rate limit, mute, quiet hours).
|
|
79
|
+
*/
|
|
80
|
+
function cmdPlay(positional, flags) {
|
|
81
|
+
const verbName = positional[0];
|
|
82
|
+
if (!verbName) {
|
|
83
|
+
console.error(" Error: missing verb. Usage: claude-sfx play <verb>");
|
|
84
|
+
console.error(` Verbs: ${ALL_VERBS.join(", ")}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
if (!ALL_VERBS.includes(verbName)) {
|
|
88
|
+
console.error(` Error: unknown verb "${verbName}".`);
|
|
89
|
+
console.error(` Verbs: ${ALL_VERBS.join(", ")}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const verb = verbName;
|
|
93
|
+
// --- Guard checks (skip with --force) ---
|
|
94
|
+
if (!flags.force) {
|
|
95
|
+
const config = loadConfig();
|
|
96
|
+
const guard = guardPlay(verb, config);
|
|
97
|
+
if (!guard.allowed) {
|
|
98
|
+
// Silent exit — this is intentional, not an error
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const profile = getProfile(flags);
|
|
103
|
+
const config = loadConfig();
|
|
104
|
+
const options = {};
|
|
105
|
+
if (flags.status) {
|
|
106
|
+
if (!["ok", "err", "warn"].includes(flags.status)) {
|
|
107
|
+
console.error(` Error: invalid status "${flags.status}". Use: ok, err, warn`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
options.status = flags.status;
|
|
111
|
+
}
|
|
112
|
+
if (flags.scope) {
|
|
113
|
+
if (!["local", "remote"].includes(flags.scope)) {
|
|
114
|
+
console.error(` Error: invalid scope "${flags.scope}". Use: local, remote`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
options.scope = flags.scope;
|
|
118
|
+
}
|
|
119
|
+
if (flags.direction) {
|
|
120
|
+
if (!["up", "down"].includes(flags.direction)) {
|
|
121
|
+
console.error(` Error: invalid direction "${flags.direction}". Use: up, down`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
options.direction = flags.direction;
|
|
125
|
+
}
|
|
126
|
+
let buffer = generateVerb(profile, verb, options);
|
|
127
|
+
// Apply volume
|
|
128
|
+
const gain = volumeToGain(config.volume);
|
|
129
|
+
if (gain < 1) {
|
|
130
|
+
buffer = applyVolume(buffer, gain);
|
|
131
|
+
}
|
|
132
|
+
playSync(buffer);
|
|
133
|
+
}
|
|
134
|
+
function cmdPreview(positional) {
|
|
135
|
+
const profileName = positional[0];
|
|
136
|
+
if (!profileName) {
|
|
137
|
+
console.log(" Available profiles:\n");
|
|
138
|
+
for (const name of listBuiltinProfiles()) {
|
|
139
|
+
const p = resolveProfile(name);
|
|
140
|
+
console.log(` ${name.padEnd(12)} ${p.description}`);
|
|
141
|
+
}
|
|
142
|
+
console.log("\n Usage: claude-sfx preview <profile>\n");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let profile;
|
|
146
|
+
try {
|
|
147
|
+
profile = resolveProfile(profileName);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
console.error(` Error: ${e.message}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
console.log(`\n Profile: ${profile.name}`);
|
|
154
|
+
console.log(` ${profile.description}\n`);
|
|
155
|
+
const statuses = [undefined, "ok", "err", "warn"];
|
|
156
|
+
for (const verb of ALL_VERBS) {
|
|
157
|
+
console.log(` ${VERB_LABELS[verb]} (${VERB_DESCRIPTIONS[verb]}):`);
|
|
158
|
+
for (const status of statuses) {
|
|
159
|
+
const label = status ? ` --status ${status}` : " (base)";
|
|
160
|
+
process.stdout.write(` ${label.padEnd(18)}`);
|
|
161
|
+
const buffer = generateVerb(profile, verb, { status });
|
|
162
|
+
const result = playSync(buffer);
|
|
163
|
+
console.log(result.played ? `(${result.durationMs}ms)` : "[no audio]");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
console.log("\n Session sounds:");
|
|
167
|
+
process.stdout.write(" Start ");
|
|
168
|
+
playSync(generateSessionStart(profile));
|
|
169
|
+
console.log("");
|
|
170
|
+
process.stdout.write(" End ");
|
|
171
|
+
playSync(generateSessionEnd(profile));
|
|
172
|
+
console.log("");
|
|
173
|
+
process.stdout.write(" Resolve ");
|
|
174
|
+
playSync(generateAmbientResolve(profile));
|
|
175
|
+
console.log("");
|
|
176
|
+
console.log("\n Done.\n");
|
|
177
|
+
}
|
|
178
|
+
function cmdSessionStart(flags) {
|
|
179
|
+
const config = loadConfig();
|
|
180
|
+
if (config.muted)
|
|
181
|
+
return;
|
|
182
|
+
let buffer = generateSessionStart(getProfile(flags));
|
|
183
|
+
const gain = volumeToGain(config.volume);
|
|
184
|
+
if (gain < 1)
|
|
185
|
+
buffer = applyVolume(buffer, gain);
|
|
186
|
+
playSync(buffer);
|
|
187
|
+
}
|
|
188
|
+
function cmdSessionEnd(flags) {
|
|
189
|
+
const config = loadConfig();
|
|
190
|
+
if (config.muted)
|
|
191
|
+
return;
|
|
192
|
+
let buffer = generateSessionEnd(getProfile(flags));
|
|
193
|
+
const gain = volumeToGain(config.volume);
|
|
194
|
+
if (gain < 1)
|
|
195
|
+
buffer = applyVolume(buffer, gain);
|
|
196
|
+
playSync(buffer);
|
|
197
|
+
}
|
|
198
|
+
function cmdAmbientStart(flags) {
|
|
199
|
+
const config = loadConfig();
|
|
200
|
+
if (config.muted) {
|
|
201
|
+
console.log(" Muted — ambient drone suppressed.");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (isAmbientRunning()) {
|
|
205
|
+
console.log(" Ambient drone is already running.");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const profile = getProfile(flags);
|
|
209
|
+
const result = startAmbient(profile);
|
|
210
|
+
if (result.started) {
|
|
211
|
+
console.log(` Ambient drone started (pid: ${result.pid})`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.error(" Failed to start ambient drone.");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function cmdAmbientResolve(flags) {
|
|
218
|
+
const profile = getProfile(flags);
|
|
219
|
+
const result = resolveAmbient(profile);
|
|
220
|
+
if (!result.resolved) {
|
|
221
|
+
console.log(" No ambient drone was running.");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function cmdAmbientStop() {
|
|
225
|
+
const result = stopAmbient();
|
|
226
|
+
if (result.stopped) {
|
|
227
|
+
console.log(" Ambient drone stopped.");
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(" No ambient drone was running.");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// --- Config commands ---
|
|
234
|
+
function cmdMute() {
|
|
235
|
+
setMuted(true);
|
|
236
|
+
resetLedger();
|
|
237
|
+
console.log(" Muted.");
|
|
238
|
+
}
|
|
239
|
+
function cmdUnmute() {
|
|
240
|
+
setMuted(false);
|
|
241
|
+
resetLedger();
|
|
242
|
+
console.log(" Unmuted.");
|
|
243
|
+
}
|
|
244
|
+
function cmdVolume(positional) {
|
|
245
|
+
const val = positional[0];
|
|
246
|
+
if (val === undefined) {
|
|
247
|
+
const config = loadConfig();
|
|
248
|
+
console.log(` Volume: ${config.volume}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const num = parseInt(val, 10);
|
|
252
|
+
if (isNaN(num) || num < 0 || num > 100) {
|
|
253
|
+
console.error(" Error: volume must be 0–100.");
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
const cfg = setVolume(num);
|
|
257
|
+
console.log(` Volume: ${cfg.volume}`);
|
|
258
|
+
}
|
|
259
|
+
function cmdConfig(positional) {
|
|
260
|
+
const subcommand = positional[0];
|
|
261
|
+
if (!subcommand) {
|
|
262
|
+
const config = loadConfig();
|
|
263
|
+
console.log(`\n Config: ${CONFIG_FILE}\n`);
|
|
264
|
+
console.log(` profile: ${config.profile}`);
|
|
265
|
+
console.log(` volume: ${config.volume}`);
|
|
266
|
+
console.log(` muted: ${config.muted}`);
|
|
267
|
+
if (config.quietHours) {
|
|
268
|
+
console.log(` quiet hours: ${config.quietHours.start} – ${config.quietHours.end}`);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
console.log(` quiet hours: off`);
|
|
272
|
+
}
|
|
273
|
+
if (config.disabledVerbs.length > 0) {
|
|
274
|
+
console.log(` disabled verbs: ${config.disabledVerbs.join(", ")}`);
|
|
275
|
+
}
|
|
276
|
+
const repoCount = Object.keys(config.repoProfiles).length;
|
|
277
|
+
if (repoCount > 0) {
|
|
278
|
+
console.log(` repo overrides: ${repoCount}`);
|
|
279
|
+
for (const [path, prof] of Object.entries(config.repoProfiles)) {
|
|
280
|
+
console.log(` ${path} → ${prof}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
console.log("");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (subcommand === "set") {
|
|
287
|
+
const key = positional[1];
|
|
288
|
+
const value = positional[2];
|
|
289
|
+
if (!key || value === undefined) {
|
|
290
|
+
console.error(" Usage: claude-sfx config set <key> <value>");
|
|
291
|
+
console.error(" Keys: profile, volume, quiet-start, quiet-end, quiet-off");
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
switch (key) {
|
|
295
|
+
case "profile":
|
|
296
|
+
setProfile(value);
|
|
297
|
+
console.log(` Profile: ${value}`);
|
|
298
|
+
break;
|
|
299
|
+
case "volume": {
|
|
300
|
+
const v = parseInt(value, 10);
|
|
301
|
+
if (isNaN(v)) {
|
|
302
|
+
console.error(" Volume must be a number.");
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
setVolume(v);
|
|
306
|
+
console.log(` Volume: ${v}`);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
case "quiet-start": {
|
|
310
|
+
const config = loadConfig();
|
|
311
|
+
const end = config.quietHours?.end ?? "07:00";
|
|
312
|
+
setQuietHours(value, end);
|
|
313
|
+
console.log(` Quiet hours: ${value} – ${end}`);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "quiet-end": {
|
|
317
|
+
const config = loadConfig();
|
|
318
|
+
const start = config.quietHours?.start ?? "22:00";
|
|
319
|
+
setQuietHours(start, value);
|
|
320
|
+
console.log(` Quiet hours: ${start} – ${value}`);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case "quiet-off":
|
|
324
|
+
clearQuietHours();
|
|
325
|
+
console.log(" Quiet hours: off");
|
|
326
|
+
break;
|
|
327
|
+
default:
|
|
328
|
+
console.error(` Unknown config key: ${key}`);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (subcommand === "reset") {
|
|
334
|
+
saveConfig({
|
|
335
|
+
profile: "minimal",
|
|
336
|
+
volume: 80,
|
|
337
|
+
muted: false,
|
|
338
|
+
quietHours: null,
|
|
339
|
+
disabledVerbs: [],
|
|
340
|
+
repoProfiles: {},
|
|
341
|
+
});
|
|
342
|
+
console.log(" Config reset to defaults.");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (subcommand === "repo") {
|
|
346
|
+
const profileName = positional[1];
|
|
347
|
+
if (!profileName) {
|
|
348
|
+
console.error(" Usage: claude-sfx config repo <profile> (sets override for cwd)");
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
const config = loadConfig();
|
|
352
|
+
if (profileName === "clear") {
|
|
353
|
+
delete config.repoProfiles[process.cwd()];
|
|
354
|
+
saveConfig(config);
|
|
355
|
+
console.log(` Cleared repo override for ${process.cwd()}`);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
config.repoProfiles[process.cwd()] = profileName;
|
|
359
|
+
saveConfig(config);
|
|
360
|
+
console.log(` ${process.cwd()} → ${profileName}`);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
console.error(` Unknown config subcommand: ${subcommand}`);
|
|
365
|
+
console.error(" Subcommands: (none) | set | reset | repo");
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
function cmdDisable(positional) {
|
|
369
|
+
const verb = positional[0];
|
|
370
|
+
if (!verb) {
|
|
371
|
+
console.error(" Usage: claude-sfx disable <verb>");
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
if (!ALL_VERBS.includes(verb)) {
|
|
375
|
+
console.error(` Unknown verb: ${verb}`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
const config = loadConfig();
|
|
379
|
+
if (!config.disabledVerbs.includes(verb)) {
|
|
380
|
+
config.disabledVerbs.push(verb);
|
|
381
|
+
saveConfig(config);
|
|
382
|
+
}
|
|
383
|
+
console.log(` Disabled: ${verb}`);
|
|
384
|
+
}
|
|
385
|
+
function cmdEnable(positional) {
|
|
386
|
+
const verb = positional[0];
|
|
387
|
+
if (!verb) {
|
|
388
|
+
console.error(" Usage: claude-sfx enable <verb>");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
const config = loadConfig();
|
|
392
|
+
config.disabledVerbs = config.disabledVerbs.filter((v) => v !== verb);
|
|
393
|
+
saveConfig(config);
|
|
394
|
+
console.log(` Enabled: ${verb}`);
|
|
395
|
+
}
|
|
396
|
+
// --- Export ---
|
|
397
|
+
function cmdExport(positional, flags) {
|
|
398
|
+
const profile = getProfile(flags);
|
|
399
|
+
const outputDir = positional[0] ?? "./sounds";
|
|
400
|
+
if (!existsSync(outputDir)) {
|
|
401
|
+
mkdirSync(outputDir, { recursive: true });
|
|
402
|
+
}
|
|
403
|
+
console.log(` Exporting [${profile.name}] sounds to ${outputDir}/\n`);
|
|
404
|
+
const statuses = [undefined, "ok", "err", "warn"];
|
|
405
|
+
for (const verb of ALL_VERBS) {
|
|
406
|
+
for (const status of statuses) {
|
|
407
|
+
const suffix = status ? `-${status}` : "";
|
|
408
|
+
const filename = `${verb}${suffix}.wav`;
|
|
409
|
+
const buffer = generateVerb(profile, verb, { status });
|
|
410
|
+
saveWav(buffer, join(outputDir, filename));
|
|
411
|
+
console.log(` ${filename}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const verb of ["move", "sync"]) {
|
|
415
|
+
for (const direction of ["up", "down"]) {
|
|
416
|
+
const filename = `${verb}-${direction}.wav`;
|
|
417
|
+
const buffer = generateVerb(profile, verb, { direction });
|
|
418
|
+
saveWav(buffer, join(outputDir, filename));
|
|
419
|
+
console.log(` ${filename}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
saveWav(generateSessionStart(profile), join(outputDir, "session-start.wav"));
|
|
423
|
+
console.log(" session-start.wav");
|
|
424
|
+
saveWav(generateSessionEnd(profile), join(outputDir, "session-end.wav"));
|
|
425
|
+
console.log(" session-end.wav");
|
|
426
|
+
saveWav(generateAmbientResolve(profile), join(outputDir, "ambient-resolve.wav"));
|
|
427
|
+
console.log(" ambient-resolve.wav");
|
|
428
|
+
const demoBuffers = ALL_VERBS.map((v) => generateVerb(profile, v));
|
|
429
|
+
const demoSequence = concatBuffers(demoBuffers, 0.3);
|
|
430
|
+
saveWav(demoSequence, join(outputDir, "demo-sequence.wav"));
|
|
431
|
+
console.log(" demo-sequence.wav");
|
|
432
|
+
const fileCount = ALL_VERBS.length * statuses.length + 4 + 3 + 1;
|
|
433
|
+
console.log(`\n Exported ${fileCount} files.\n`);
|
|
434
|
+
}
|
|
435
|
+
// --- Init / Uninstall ---
|
|
436
|
+
function cmdInit() {
|
|
437
|
+
const cwd = process.cwd();
|
|
438
|
+
const result = installHooks(cwd);
|
|
439
|
+
console.log(`\n claude-sfx hooks installed.\n`);
|
|
440
|
+
console.log(` Settings: ${result.settingsPath}`);
|
|
441
|
+
console.log(` Events: ${result.eventsAdded.join(", ")}\n`);
|
|
442
|
+
console.log(" Hook mapping:");
|
|
443
|
+
console.log(" Read, WebFetch, WebSearch → intake");
|
|
444
|
+
console.log(" Edit → transform");
|
|
445
|
+
console.log(" Write, NotebookEdit, TodoWrite → commit");
|
|
446
|
+
console.log(" Grep, Glob → navigate");
|
|
447
|
+
console.log(" Bash → execute (git → sync)");
|
|
448
|
+
console.log(" Task, MCP tools → intake/commit (remote)");
|
|
449
|
+
console.log(" Subagent start/stop → move/commit (remote)");
|
|
450
|
+
console.log(" Session start/stop → chimes\n");
|
|
451
|
+
console.log(" Run `claude-sfx uninstall` to remove.\n");
|
|
452
|
+
}
|
|
453
|
+
function cmdUninstall() {
|
|
454
|
+
const cwd = process.cwd();
|
|
455
|
+
const result = uninstallHooks(cwd);
|
|
456
|
+
if (result.removed) {
|
|
457
|
+
console.log(` claude-sfx hooks removed from ${result.settingsPath}`);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
console.log(" No claude-sfx hooks found to remove.");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async function cmdHookHandler(positional) {
|
|
464
|
+
const eventName = positional[0];
|
|
465
|
+
if (!eventName) {
|
|
466
|
+
console.error(" Error: hook-handler requires an event name.");
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
await handleHook(eventName);
|
|
470
|
+
}
|
|
471
|
+
// --- Help ---
|
|
472
|
+
function cmdHelp() {
|
|
473
|
+
const profiles = listBuiltinProfiles().join(", ");
|
|
474
|
+
console.log(`
|
|
475
|
+
claude-sfx — Procedural audio feedback for Claude Code
|
|
476
|
+
|
|
477
|
+
Setup:
|
|
478
|
+
init Install hooks into .claude/settings.json
|
|
479
|
+
uninstall Remove hooks from .claude/settings.json
|
|
480
|
+
|
|
481
|
+
Playback:
|
|
482
|
+
play <verb> [options] Play a verb sound (goes through guard)
|
|
483
|
+
demo [--profile <name>] Play all 7 verbs in sequence
|
|
484
|
+
preview [profile] Audition all sounds in a profile
|
|
485
|
+
session-start [--profile] Boot chime
|
|
486
|
+
session-end [--profile] Closure chime
|
|
487
|
+
|
|
488
|
+
Ambient (long-running):
|
|
489
|
+
ambient-start [--profile] Start the thinking drone
|
|
490
|
+
ambient-resolve [--profile] Stop drone + play resolution stinger
|
|
491
|
+
ambient-stop Stop drone silently
|
|
492
|
+
|
|
493
|
+
Config:
|
|
494
|
+
mute Mute all sounds
|
|
495
|
+
unmute Unmute
|
|
496
|
+
volume [0-100] Get or set volume
|
|
497
|
+
config Print current config
|
|
498
|
+
config set <key> <value> Set a config value
|
|
499
|
+
config reset Reset to defaults
|
|
500
|
+
config repo <profile|clear> Set profile override for cwd
|
|
501
|
+
disable <verb> Disable a specific verb
|
|
502
|
+
enable <verb> Re-enable a verb
|
|
503
|
+
|
|
504
|
+
Play options:
|
|
505
|
+
--status <ok|err|warn> Status modifier
|
|
506
|
+
--scope <local|remote> Scope modifier
|
|
507
|
+
--direction <up|down> Direction (move/sync verbs)
|
|
508
|
+
--profile <name|path> Sound profile (default: minimal)
|
|
509
|
+
--force Bypass guard (debounce/rate/mute)
|
|
510
|
+
|
|
511
|
+
Verbs:
|
|
512
|
+
intake read / open / fetch
|
|
513
|
+
transform edit / format / refactor
|
|
514
|
+
commit write / save / apply
|
|
515
|
+
navigate search / jump / list
|
|
516
|
+
execute run / test / build
|
|
517
|
+
move move / rename / relocate
|
|
518
|
+
sync git push / pull / deploy
|
|
519
|
+
|
|
520
|
+
Profiles: ${profiles}
|
|
521
|
+
Config: ~/.claude-sfx/config.json
|
|
522
|
+
`);
|
|
523
|
+
}
|
|
524
|
+
// --- Main ---
|
|
525
|
+
const { command, positional, flags } = parseArgs(process.argv);
|
|
526
|
+
// hook-handler is async (reads stdin), so wrap in an IIFE
|
|
527
|
+
const run = async () => {
|
|
528
|
+
switch (command) {
|
|
529
|
+
case "init":
|
|
530
|
+
cmdInit();
|
|
531
|
+
break;
|
|
532
|
+
case "uninstall":
|
|
533
|
+
cmdUninstall();
|
|
534
|
+
break;
|
|
535
|
+
case "hook-handler":
|
|
536
|
+
await cmdHookHandler(positional);
|
|
537
|
+
break;
|
|
538
|
+
case "demo":
|
|
539
|
+
cmdDemo(flags);
|
|
540
|
+
break;
|
|
541
|
+
case "play":
|
|
542
|
+
cmdPlay(positional, flags);
|
|
543
|
+
break;
|
|
544
|
+
case "preview":
|
|
545
|
+
cmdPreview(positional);
|
|
546
|
+
break;
|
|
547
|
+
case "session-start":
|
|
548
|
+
cmdSessionStart(flags);
|
|
549
|
+
break;
|
|
550
|
+
case "session-end":
|
|
551
|
+
cmdSessionEnd(flags);
|
|
552
|
+
break;
|
|
553
|
+
case "ambient-start":
|
|
554
|
+
cmdAmbientStart(flags);
|
|
555
|
+
break;
|
|
556
|
+
case "ambient-resolve":
|
|
557
|
+
cmdAmbientResolve(flags);
|
|
558
|
+
break;
|
|
559
|
+
case "ambient-stop":
|
|
560
|
+
cmdAmbientStop();
|
|
561
|
+
break;
|
|
562
|
+
case "mute":
|
|
563
|
+
cmdMute();
|
|
564
|
+
break;
|
|
565
|
+
case "unmute":
|
|
566
|
+
cmdUnmute();
|
|
567
|
+
break;
|
|
568
|
+
case "volume":
|
|
569
|
+
cmdVolume(positional);
|
|
570
|
+
break;
|
|
571
|
+
case "config":
|
|
572
|
+
cmdConfig(positional);
|
|
573
|
+
break;
|
|
574
|
+
case "disable":
|
|
575
|
+
cmdDisable(positional);
|
|
576
|
+
break;
|
|
577
|
+
case "enable":
|
|
578
|
+
cmdEnable(positional);
|
|
579
|
+
break;
|
|
580
|
+
case "export":
|
|
581
|
+
cmdExport(positional, flags);
|
|
582
|
+
break;
|
|
583
|
+
case "help":
|
|
584
|
+
case "--help":
|
|
585
|
+
case "-h":
|
|
586
|
+
cmdHelp();
|
|
587
|
+
break;
|
|
588
|
+
default:
|
|
589
|
+
console.error(` Unknown command: ${command}`);
|
|
590
|
+
cmdHelp();
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
run();
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global configuration — persisted to ~/.claude-sfx/config.json.
|
|
3
|
+
* Controls profile, volume, quiet hours, mute state, per-verb toggles,
|
|
4
|
+
* and per-repo profile overrides.
|
|
5
|
+
*/
|
|
6
|
+
declare const CONFIG_DIR: string;
|
|
7
|
+
declare const CONFIG_FILE: string;
|
|
8
|
+
export interface QuietHours {
|
|
9
|
+
/** Start time in HH:MM format (24h). */
|
|
10
|
+
start: string;
|
|
11
|
+
/** End time in HH:MM format (24h). */
|
|
12
|
+
end: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SfxConfig {
|
|
15
|
+
/** Active profile name or path. */
|
|
16
|
+
profile: string;
|
|
17
|
+
/** Master volume 0–100 (maps to gain 0.0–1.0). */
|
|
18
|
+
volume: number;
|
|
19
|
+
/** Global mute toggle. */
|
|
20
|
+
muted: boolean;
|
|
21
|
+
/** Quiet hours (sounds suppressed). */
|
|
22
|
+
quietHours: QuietHours | null;
|
|
23
|
+
/** Per-verb enable/disable. Missing = enabled. */
|
|
24
|
+
disabledVerbs: string[];
|
|
25
|
+
/** Per-repo profile overrides. Key = absolute repo path, value = profile name. */
|
|
26
|
+
repoProfiles: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
export declare function loadConfig(): SfxConfig;
|
|
29
|
+
export declare function saveConfig(config: SfxConfig): void;
|
|
30
|
+
export declare function setMuted(muted: boolean): SfxConfig;
|
|
31
|
+
export declare function setVolume(volume: number): SfxConfig;
|
|
32
|
+
export declare function setProfile(profile: string): SfxConfig;
|
|
33
|
+
export declare function setQuietHours(start: string, end: string): SfxConfig;
|
|
34
|
+
export declare function clearQuietHours(): SfxConfig;
|
|
35
|
+
export declare function isQuietTime(config: SfxConfig): boolean;
|
|
36
|
+
/** Resolve the effective profile name for the current working directory. */
|
|
37
|
+
export declare function resolveProfileName(config: SfxConfig, cwd?: string): string;
|
|
38
|
+
/** Convert volume (0–100) to gain (0.0–1.0). */
|
|
39
|
+
export declare function volumeToGain(volume: number): number;
|
|
40
|
+
export { CONFIG_DIR, CONFIG_FILE };
|