@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/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();
@@ -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 };