vibeaudio 0.4.0 → 0.6.1
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 +140 -49
- package/package.json +7 -5
- package/scripts/postinstall.js +33 -0
- package/src/cli.js +286 -19
- package/src/hooks.js +217 -93
- package/src/interactive.js +56 -4
- package/src/mcp.js +9 -3
- package/src/player.js +102 -2
- package/src/synth/electronic.js +7 -2
- package/src/synth/generator.js +27 -0
- package/src/synth/synthwave.js +3 -2
package/src/cli.js
CHANGED
|
@@ -14,6 +14,10 @@ const {
|
|
|
14
14
|
resolveGenre,
|
|
15
15
|
isKnownGenre,
|
|
16
16
|
normalizeVolume,
|
|
17
|
+
loadConfig,
|
|
18
|
+
saveConfig,
|
|
19
|
+
projectSettings,
|
|
20
|
+
saveProjectConfig,
|
|
17
21
|
wavDurationMs,
|
|
18
22
|
AVAILABLE_GENRES
|
|
19
23
|
} = require("./player");
|
|
@@ -44,6 +48,7 @@ Procedural focus music while your AI coding tools think.
|
|
|
44
48
|
|
|
45
49
|
\x1b[1mEXAMPLES:\x1b[0m
|
|
46
50
|
vibe --install-hooks \x1b[90m# best for interactive agents — music follows thinking\x1b[0m
|
|
51
|
+
vibe --genre jazz --volume 25 \x1b[90m# save your default — reaches hooks, wrapper and MCP\x1b[0m
|
|
47
52
|
vibe --install-hooks --tools codex \x1b[90m# wire up one agent instead of every one found\x1b[0m
|
|
48
53
|
vibe npm test \x1b[90m# wrap any command that exits when it's done\x1b[0m
|
|
49
54
|
vibe claude -p "explain this"
|
|
@@ -51,12 +56,14 @@ Procedural focus music while your AI coding tools think.
|
|
|
51
56
|
vibe --genre 8bit sleep 5
|
|
52
57
|
vibe --volume 30 npm test
|
|
53
58
|
vibe --preview jazz
|
|
59
|
+
vibe \x1b[90m# menu: pick a tool and a sound (p auditions a genre)\x1b[0m
|
|
54
60
|
|
|
55
61
|
\x1b[1mOPTIONS:\x1b[0m
|
|
56
62
|
-g, --genre <name> Select genre: lofi (default), synthwave, 8bit, electronic, jazz, zen, piano, drone, random
|
|
57
63
|
-v, --volume <5-100> Set playback volume (default: 40)
|
|
58
64
|
-cv, --chime-volume <5-100> Set independent completion chime volume
|
|
59
65
|
--grace <ms> Silence window before music starts, in ms (default: ${DEFAULT_GRACE_PERIOD_MS})
|
|
66
|
+
--here With a saved setting: this directory only, not everywhere
|
|
60
67
|
--seed <n> Force a specific arrangement (default: derived from the project directory)
|
|
61
68
|
--whisper Preset: 15% volume (late night / headphones)
|
|
62
69
|
--quiet Preset: 25% volume (focus / open office)
|
|
@@ -78,11 +85,22 @@ Procedural focus music while your AI coding tools think.
|
|
|
78
85
|
-h, --help Show this help message
|
|
79
86
|
--version Show version
|
|
80
87
|
|
|
88
|
+
\x1b[1mSETTINGS:\x1b[0m
|
|
89
|
+
A genre or volume with no command after it is saved as your default:
|
|
90
|
+
|
|
91
|
+
vibe --genre zen \x1b[90m# applies on the next prompt, nothing to reinstall\x1b[0m
|
|
92
|
+
vibe --genre zen --here \x1b[90m# this project only — the default stays as it was\x1b[0m
|
|
93
|
+
|
|
94
|
+
Saved to ~/.vibeaudio/config.json and read by the wrapper, your agent hooks
|
|
95
|
+
and the MCP server alike. A flag beats an environment variable beats a
|
|
96
|
+
\x1b[1m--here\x1b[0m setting beats the global one, so a one-off
|
|
97
|
+
\x1b[1mvibe --genre 8bit npm test\x1b[0m stays a one-off.
|
|
98
|
+
|
|
81
99
|
\x1b[1mENVIRONMENT:\x1b[0m
|
|
82
|
-
VIBE_GENRE=<name>
|
|
83
|
-
VIBE_VOLUME=<5-100>
|
|
84
|
-
VIBE_CHIME_VOLUME=<5-100>
|
|
85
|
-
VIBE_GRACE_MS=<ms>
|
|
100
|
+
VIBE_GENRE=<name> Override the saved genre for this shell
|
|
101
|
+
VIBE_VOLUME=<5-100> Override the saved volume for this shell
|
|
102
|
+
VIBE_CHIME_VOLUME=<5-100> Override the saved chime volume for this shell
|
|
103
|
+
VIBE_GRACE_MS=<ms> Override the saved grace window, in ms
|
|
86
104
|
VIBE_SEED=<n> Pin the arrangement instead of deriving it from the directory
|
|
87
105
|
VIBE_DISABLE=1 Mute automatic playback without uninstalling anything
|
|
88
106
|
`);
|
|
@@ -105,13 +123,33 @@ const VALUE_FLAGS = new Set([
|
|
|
105
123
|
|
|
106
124
|
function parseArgs(argv) {
|
|
107
125
|
const args = argv.slice(2);
|
|
108
|
-
let genre = (process.env.VIBE_GENRE || "lofi").toLowerCase();
|
|
109
126
|
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
// Flag beats env var beats saved config beats the built-in default. The
|
|
128
|
+
// saved config is what makes "set my genre" have one answer instead of three
|
|
129
|
+
// - see loadConfig() in player.js - and it sits below the env var so a shell
|
|
130
|
+
// that exports one keeps winning, as it always did.
|
|
131
|
+
const saved = loadConfig();
|
|
132
|
+
// A setting saved for this directory sits between the environment and the
|
|
133
|
+
// global default: `--here` is a deliberate choice about one project, so it
|
|
134
|
+
// outranks the machine-wide one, and an export still outranks both.
|
|
135
|
+
const here = projectSettings(saved);
|
|
136
|
+
const setting = (key) => (here[key] !== undefined ? here[key] : saved[key]);
|
|
137
|
+
|
|
138
|
+
let genre = (process.env.VIBE_GENRE || setting("genre") || "lofi").toLowerCase();
|
|
139
|
+
|
|
140
|
+
let volume = normalizeVolume(process.env.VIBE_VOLUME, normalizeVolume(setting("volume"), 0.40));
|
|
141
|
+
let chimeVolume = normalizeVolume(process.env.VIBE_CHIME_VOLUME, normalizeVolume(setting("chimeVolume"), null));
|
|
112
142
|
|
|
113
143
|
const envGrace = process.env.VIBE_GRACE_MS ? parseInt(process.env.VIBE_GRACE_MS, 10) : NaN;
|
|
114
|
-
let grace = !isNaN(envGrace) ? Math.max(0, envGrace)
|
|
144
|
+
let grace = !isNaN(envGrace) ? Math.max(0, envGrace)
|
|
145
|
+
: Number.isFinite(setting("grace")) ? Math.max(0, setting("grace"))
|
|
146
|
+
: DEFAULT_GRACE_PERIOD_MS;
|
|
147
|
+
|
|
148
|
+
// What the user typed on THIS command line, as opposed to what was inherited
|
|
149
|
+
// from the environment or the saved file. `vibe --genre jazz` with nothing
|
|
150
|
+
// after it is a request to change the default, and this is how run() tells
|
|
151
|
+
// that apart from a genre that merely happens to be in effect.
|
|
152
|
+
const typed = {};
|
|
115
153
|
|
|
116
154
|
let noChime = false;
|
|
117
155
|
let noHud = false;
|
|
@@ -124,6 +162,7 @@ function parseArgs(argv) {
|
|
|
124
162
|
let hookAction = null;
|
|
125
163
|
let mcp = false;
|
|
126
164
|
let reactive = false;
|
|
165
|
+
let here_flag = false;
|
|
127
166
|
let dryRun = false;
|
|
128
167
|
let tools = null;
|
|
129
168
|
let cmdArgs = [];
|
|
@@ -150,6 +189,7 @@ function parseArgs(argv) {
|
|
|
150
189
|
if (arg === "-g" || arg === "--genre") {
|
|
151
190
|
if (i + 1 < args.length) {
|
|
152
191
|
genre = args[i + 1].toLowerCase();
|
|
192
|
+
typed.genre = genre;
|
|
153
193
|
i += 2;
|
|
154
194
|
continue;
|
|
155
195
|
}
|
|
@@ -158,6 +198,7 @@ function parseArgs(argv) {
|
|
|
158
198
|
if (arg === "-v" || arg === "--volume") {
|
|
159
199
|
if (i + 1 < args.length) {
|
|
160
200
|
volume = normalizeVolume(args[i + 1], volume);
|
|
201
|
+
typed.volume = volume;
|
|
161
202
|
i += 2;
|
|
162
203
|
continue;
|
|
163
204
|
}
|
|
@@ -166,6 +207,7 @@ function parseArgs(argv) {
|
|
|
166
207
|
if (arg === "-cv" || arg === "--chime-volume") {
|
|
167
208
|
if (i + 1 < args.length) {
|
|
168
209
|
chimeVolume = normalizeVolume(args[i + 1], chimeVolume);
|
|
210
|
+
typed.chimeVolume = chimeVolume;
|
|
169
211
|
i += 2;
|
|
170
212
|
continue;
|
|
171
213
|
}
|
|
@@ -176,6 +218,7 @@ function parseArgs(argv) {
|
|
|
176
218
|
const parsed = parseInt(args[i + 1], 10);
|
|
177
219
|
if (!isNaN(parsed)) {
|
|
178
220
|
grace = Math.max(0, parsed);
|
|
221
|
+
typed.grace = grace;
|
|
179
222
|
}
|
|
180
223
|
i += 2;
|
|
181
224
|
continue;
|
|
@@ -239,6 +282,12 @@ function parseArgs(argv) {
|
|
|
239
282
|
continue;
|
|
240
283
|
}
|
|
241
284
|
|
|
285
|
+
if (arg === "--here") {
|
|
286
|
+
here_flag = true;
|
|
287
|
+
i += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
242
291
|
if (arg === "--reactive") {
|
|
243
292
|
reactive = true;
|
|
244
293
|
i += 1;
|
|
@@ -259,18 +308,21 @@ function parseArgs(argv) {
|
|
|
259
308
|
|
|
260
309
|
if (arg === "--whisper") {
|
|
261
310
|
volume = 0.15;
|
|
311
|
+
typed.volume = volume;
|
|
262
312
|
i += 1;
|
|
263
313
|
continue;
|
|
264
314
|
}
|
|
265
315
|
|
|
266
316
|
if (arg === "--quiet") {
|
|
267
317
|
volume = 0.25;
|
|
318
|
+
typed.volume = volume;
|
|
268
319
|
i += 1;
|
|
269
320
|
continue;
|
|
270
321
|
}
|
|
271
322
|
|
|
272
323
|
if (arg === "--loud") {
|
|
273
324
|
volume = 0.75;
|
|
325
|
+
typed.volume = volume;
|
|
274
326
|
i += 1;
|
|
275
327
|
continue;
|
|
276
328
|
}
|
|
@@ -313,11 +365,22 @@ function parseArgs(argv) {
|
|
|
313
365
|
}
|
|
314
366
|
|
|
315
367
|
if (!isKnownGenre(genre)) {
|
|
368
|
+
// A typo that would be SAVED is a different thing from a typo on a run:
|
|
369
|
+
// falling back to lofi here would quietly overwrite the default the user
|
|
370
|
+
// already had, on the strength of a misspelling. Refuse and change nothing.
|
|
371
|
+
if (typed.genre !== undefined && cmdArgs.length === 0) {
|
|
372
|
+
console.error(
|
|
373
|
+
`\x1b[31m[vibeaudio] Unknown genre '${genre}' — nothing saved.\x1b[0m\n` +
|
|
374
|
+
` Available: ${AVAILABLE_GENRES.join(", ")}, random`
|
|
375
|
+
);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
316
378
|
console.error(
|
|
317
379
|
`\x1b[33m[vibeaudio] Unknown genre '${genre}' — falling back to lofi.\x1b[0m\n` +
|
|
318
380
|
` Available: ${AVAILABLE_GENRES.join(", ")}, random`
|
|
319
381
|
);
|
|
320
382
|
genre = "lofi";
|
|
383
|
+
if (typed.genre) typed.genre = "lofi";
|
|
321
384
|
}
|
|
322
385
|
|
|
323
386
|
return {
|
|
@@ -338,6 +401,8 @@ function parseArgs(argv) {
|
|
|
338
401
|
reactive,
|
|
339
402
|
dryRun,
|
|
340
403
|
tools,
|
|
404
|
+
typed,
|
|
405
|
+
hereOnly: here_flag,
|
|
341
406
|
cmdArgs
|
|
342
407
|
};
|
|
343
408
|
}
|
|
@@ -382,9 +447,44 @@ function resolveTargets(explicit) {
|
|
|
382
447
|
* The one install path. Both --install-hooks and the menu's hooks branch call
|
|
383
448
|
* this, so the two entry points can't drift in what they write or report.
|
|
384
449
|
*/
|
|
385
|
-
function installHookTargets(ids, genre, volume, reactive, dryRun = false) {
|
|
450
|
+
function installHookTargets(ids, genre, volume, reactive, dryRun = false, typed = {}) {
|
|
386
451
|
const hooks = require("./hooks");
|
|
452
|
+
|
|
453
|
+
// Migrating off a pinned install must not change what is playing: those
|
|
454
|
+
// entries carry the settings the user has actually been hearing, while the
|
|
455
|
+
// config file may not exist yet - so an untyped reinstall inherits from the
|
|
456
|
+
// entries it is about to replace.
|
|
457
|
+
//
|
|
458
|
+
// A pin fills a gap and never overrides. Once a setting is saved, somebody
|
|
459
|
+
// chose it, and a legacy pin left in some other agent's config is the older
|
|
460
|
+
// fact of the two - without this, installing for every detected agent lets
|
|
461
|
+
// four untouched installs outvote the genre the user picked a minute ago.
|
|
462
|
+
const saved = loadConfig();
|
|
463
|
+
const inherited = [];
|
|
464
|
+
const gap = (key) => typed[key] === undefined && saved[key] === undefined;
|
|
465
|
+
|
|
466
|
+
if (gap("genre") || gap("volume")) {
|
|
467
|
+
const pinned = pinnedSettingsFor(ids);
|
|
468
|
+
if (gap("genre") && pinned.genre && pinned.genre !== genre) {
|
|
469
|
+
genre = pinned.genre;
|
|
470
|
+
inherited.push(`genre ${genre}`);
|
|
471
|
+
}
|
|
472
|
+
if (gap("volume") && pinned.volume !== null && pinned.volume !== volume) {
|
|
473
|
+
volume = pinned.volume;
|
|
474
|
+
inherited.push(`volume ${Math.round(volume * 100)}%`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
387
478
|
if (dryRun) console.log(`\x1b[1mDry run\x1b[0m — nothing below is written.\n`);
|
|
479
|
+
if (inherited.length) {
|
|
480
|
+
console.log(`\x1b[90mKeeping your current ${inherited.join(" and ")} — pass --genre/--volume to change it.\x1b[0m`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// The hook entries no longer carry the genre and volume, so this is where
|
|
484
|
+
// they live. Saved here rather than inside installHooks(), which stays a
|
|
485
|
+
// pure config edit that tests can point at a throwaway file - the same
|
|
486
|
+
// reason the slash command and stopDaemon() are driven from this layer.
|
|
487
|
+
if (!dryRun) saveConfig({ genre, volume: Math.round(volume * 100) });
|
|
388
488
|
|
|
389
489
|
for (const id of ids) {
|
|
390
490
|
const result = hooks.installHooks(genre, volume, null, { reactive, id, dryRun });
|
|
@@ -424,6 +524,7 @@ function installHookTargets(ids, genre, volume, reactive, dryRun = false) {
|
|
|
424
524
|
// verified for each, so an open session picks this up on its next prompt.
|
|
425
525
|
const live = ids.filter((id) => hooks.TARGETS[id].liveReload !== false).map((id) => hooks.TARGETS[id].name);
|
|
426
526
|
if (live.length) console.log(` ${live.join(", ")}: takes effect on your next prompt - no restart needed.`);
|
|
527
|
+
console.log(` \x1b[90mChange the sound any time with: vibe --genre <name> --volume <n> — no reinstall.\x1b[0m`);
|
|
427
528
|
if (ids.length > 1) {
|
|
428
529
|
console.log(` \x1b[90mOne player is shared: whichever agent you prompt last owns the music.\x1b[0m`);
|
|
429
530
|
}
|
|
@@ -515,9 +616,24 @@ function printStatus() {
|
|
|
515
616
|
console.log(`\x1b[33m⏸ Muted by VIBE_DISABLE=${process.env.VIBE_DISABLE}\x1b[0m — automatic playback is off (--preview still works).\n`);
|
|
516
617
|
}
|
|
517
618
|
|
|
619
|
+
// What will actually play, and which of the three possible sources decided
|
|
620
|
+
// it - the usual confusion is a saved default quietly beaten by an export.
|
|
621
|
+
const saved = loadConfig();
|
|
622
|
+
const here = projectSettings(saved);
|
|
623
|
+
const source = (env, key) =>
|
|
624
|
+
process.env[env] ? off(` from ${env}`)
|
|
625
|
+
: here[key] !== undefined ? off(" saved for this project")
|
|
626
|
+
: saved[key] !== undefined ? off(" saved")
|
|
627
|
+
: off(" default");
|
|
628
|
+
|
|
629
|
+
console.log(`\x1b[1mSound\x1b[0m`);
|
|
630
|
+
console.log(` genre ${on(genreNow())}${source("VIBE_GENRE", "genre")}`);
|
|
631
|
+
console.log(` volume ${on(`${Math.round(volumeNow() * 100)}%`)}${source("VIBE_VOLUME", "volume")}`);
|
|
632
|
+
console.log(` ${off("change either with: vibe --genre <name> --volume <n>")}`);
|
|
633
|
+
|
|
518
634
|
// Audio backend
|
|
519
635
|
const backend = detect();
|
|
520
|
-
console.log(`\x1b[1mAudio\x1b[0m`);
|
|
636
|
+
console.log(`\n\x1b[1mAudio\x1b[0m`);
|
|
521
637
|
console.log(backend
|
|
522
638
|
? ` player ${on(backend.cmd)}${backend.volume ? "" : off(" (no volume support — gain is baked into the file)")}`
|
|
523
639
|
: ` player \x1b[31mnone found — VibeAudio runs silently\x1b[0m`);
|
|
@@ -539,28 +655,47 @@ function printStatus() {
|
|
|
539
655
|
const why = detected.includes(id)
|
|
540
656
|
? `${off("not installed")} — run: vibe --install-hooks`
|
|
541
657
|
: `${off("not installed")} ${off("(not found on this machine)")}`;
|
|
542
|
-
console.log(` ${t.name.padEnd(
|
|
658
|
+
console.log(` ${t.name.padEnd(20)}${why}`);
|
|
543
659
|
continue;
|
|
544
660
|
}
|
|
545
661
|
|
|
546
662
|
console.log(` ${t.name}`);
|
|
547
663
|
for (const { event, command } of installed) {
|
|
664
|
+
// A baked genre means an install from before settings moved into the
|
|
665
|
+
// config file: worth showing, because it still overrides that file.
|
|
548
666
|
const g = /--genre (\S+)/.exec(command);
|
|
549
667
|
const vol = /--volume (\d+)/.exec(command);
|
|
550
|
-
const
|
|
668
|
+
const parts = [];
|
|
669
|
+
if (g) parts.push(`pinned ${g[1]} @ ${vol ? vol[1] : "?"}%`);
|
|
670
|
+
if (/--reactive/.test(command)) parts.push("reactive");
|
|
671
|
+
const extra = parts.length ? ` ${parts.join(", ")}` : "";
|
|
551
672
|
console.log(` ${on("✔")} ${extra ? event.padEnd(19) : event}${extra ? off(extra) : ""}`);
|
|
552
673
|
}
|
|
674
|
+
if (readVibeHooks(t.file(), t).some(({ command }) => /--genre /.test(command))) {
|
|
675
|
+
console.log(` \x1b[33m↑ pinned by an older install — vibe --install-hooks makes it follow Sound above\x1b[0m`);
|
|
676
|
+
}
|
|
553
677
|
}
|
|
554
678
|
|
|
555
679
|
// Background player
|
|
556
680
|
console.log(`\n\x1b[1mBackground player\x1b[0m`);
|
|
557
681
|
const pid = readDaemonPid(hooks.PID_FILE);
|
|
682
|
+
const playing = hooks.daemonPlaying();
|
|
558
683
|
if (pid === null) {
|
|
559
684
|
console.log(` ${off("not running")}`);
|
|
560
|
-
} else if (!
|
|
685
|
+
} else if (!playing) {
|
|
561
686
|
console.log(` ${off(`stale pid file (${pid} is not ours) — cleared on the next prompt`)}`);
|
|
562
687
|
} else {
|
|
563
|
-
|
|
688
|
+
// What it was started with, which is not necessarily what is saved: a
|
|
689
|
+
// daemon keeps its settings until the next prompt replaces it, and
|
|
690
|
+
// "I changed the genre and nothing happened" is that gap.
|
|
691
|
+
const now = playing.genre
|
|
692
|
+
? ` ${on(playing.genre)}${playing.volume === null ? "" : on(` @ ${playing.volume}%`)}${playing.reactive ? off(", reactive") : ""}`
|
|
693
|
+
: "";
|
|
694
|
+
console.log(` ${on("playing")}${now}${off(` pid ${pid}`)}`);
|
|
695
|
+
if (playing.genre && playing.genre !== genreNow()) {
|
|
696
|
+
console.log(` ${off(`your saved default is ${genreNow()} — this one keeps playing until the next prompt`)}`);
|
|
697
|
+
}
|
|
698
|
+
console.log(` ${off("stop it with: vibe --stop")}`);
|
|
564
699
|
}
|
|
565
700
|
|
|
566
701
|
// Which tools are actually here, and what each one can use
|
|
@@ -576,6 +711,24 @@ function printStatus() {
|
|
|
576
711
|
console.log();
|
|
577
712
|
}
|
|
578
713
|
|
|
714
|
+
/**
|
|
715
|
+
* The effective genre and volume, resolved the same way parseArgs does it, so
|
|
716
|
+
* --status reports what would actually play rather than a second opinion.
|
|
717
|
+
*/
|
|
718
|
+
function settingNow(key) {
|
|
719
|
+
const config = loadConfig();
|
|
720
|
+
const here = projectSettings(config);
|
|
721
|
+
return here[key] !== undefined ? here[key] : config[key];
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function genreNow() {
|
|
725
|
+
return (process.env.VIBE_GENRE || settingNow("genre") || "lofi").toLowerCase();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function volumeNow() {
|
|
729
|
+
return normalizeVolume(process.env.VIBE_VOLUME, normalizeVolume(settingNow("volume"), 0.40));
|
|
730
|
+
}
|
|
731
|
+
|
|
579
732
|
function dirSize(dir) {
|
|
580
733
|
try {
|
|
581
734
|
let total = 0;
|
|
@@ -646,6 +799,107 @@ function detectAiTools(hooked = new Set()) {
|
|
|
646
799
|
return CANDIDATES.filter((c) => isInstalled(c.cmd));
|
|
647
800
|
}
|
|
648
801
|
|
|
802
|
+
/**
|
|
803
|
+
* `vibe --genre jazz` with no command after it. That used to open the launcher
|
|
804
|
+
* menu, which then asked for a genre and used its own answer - the most
|
|
805
|
+
* natural way to phrase the request did something else entirely, and the
|
|
806
|
+
* README had to warn about it. It saves the default instead.
|
|
807
|
+
*/
|
|
808
|
+
function saveDefaults(typed, hereOnly = false) {
|
|
809
|
+
const patch = {};
|
|
810
|
+
if (typed.genre !== undefined) patch.genre = typed.genre;
|
|
811
|
+
if (typed.volume !== undefined) patch.volume = Math.round(typed.volume * 100);
|
|
812
|
+
if (typed.chimeVolume !== undefined) patch.chimeVolume = Math.round(typed.chimeVolume * 100);
|
|
813
|
+
if (typed.grace !== undefined) patch.grace = typed.grace;
|
|
814
|
+
|
|
815
|
+
const { CONFIG_FILE } = require("./player");
|
|
816
|
+
if (hereOnly) saveProjectConfig(patch);
|
|
817
|
+
else saveConfig(patch);
|
|
818
|
+
|
|
819
|
+
const label = {
|
|
820
|
+
genre: (v) => `genre ${v}`,
|
|
821
|
+
volume: (v) => `volume ${v}%`,
|
|
822
|
+
chimeVolume: (v) => `chime volume ${v}%`,
|
|
823
|
+
grace: (v) => `grace ${v}ms`
|
|
824
|
+
};
|
|
825
|
+
const changed = Object.keys(patch).map((k) => label[k](patch[k])).join(", ");
|
|
826
|
+
const where = hereOnly ? ` for ${process.cwd()}` : "";
|
|
827
|
+
console.log(`\x1b[32m✔ Saved: ${changed}${where}\x1b[0m \x1b[90m(${CONFIG_FILE})\x1b[0m`);
|
|
828
|
+
console.log(` Applies to the wrapper, your agent hooks and MCP — on the next prompt.`);
|
|
829
|
+
if (hereOnly) console.log(` \x1b[90mThis directory and everything under it. Drop --here to change the default everywhere.\x1b[0m`);
|
|
830
|
+
|
|
831
|
+
// Music already in the air is the one thing this does not reach, and
|
|
832
|
+
// silence about it reads as the setting having done nothing at all.
|
|
833
|
+
const playing = require("./hooks").daemonPlaying();
|
|
834
|
+
if (playing && patch.genre && playing.genre && playing.genre !== patch.genre) {
|
|
835
|
+
console.log(` \x1b[90m${playing.genre} is still playing — vibe --stop cuts it short.\x1b[0m`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// An install from before the settings moved out of the hook command line
|
|
839
|
+
// still carries its own genre, and a flag beats this file. Saying nothing
|
|
840
|
+
// would leave the user with a saved default that audibly does nothing.
|
|
841
|
+
const pinned = pinnedHookSettings();
|
|
842
|
+
if (pinned.length) {
|
|
843
|
+
console.log(
|
|
844
|
+
`\n\x1b[33m ${pinned.join(", ")}: hooks installed by an older version pin their own settings.\x1b[0m\n` +
|
|
845
|
+
` Run \x1b[1mvibe --install-hooks\x1b[0m once to have them follow this file instead.`
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
if (typed.genre !== undefined) console.log(`\n Audition it: vibe --preview ${typed.genre}`);
|
|
849
|
+
console.log();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Hook targets whose installed entries still carry a baked --genre/--volume.
|
|
854
|
+
*/
|
|
855
|
+
/**
|
|
856
|
+
* What an install from before the config file pinned, for the first of `ids`
|
|
857
|
+
* that pinned anything.
|
|
858
|
+
*
|
|
859
|
+
* A reinstall is how a user moves off that older scheme, and it must not be
|
|
860
|
+
* the moment their music changes: the settings frozen into those entries are
|
|
861
|
+
* the ones they have actually been listening to, and the config file may hold
|
|
862
|
+
* nothing but an untouched default. Adopted only for what the user did not
|
|
863
|
+
* type on this command line - naming a genre still means that genre.
|
|
864
|
+
*/
|
|
865
|
+
function pinnedSettingsFor(ids) {
|
|
866
|
+
const hooks = require("./hooks");
|
|
867
|
+
const genres = new Set();
|
|
868
|
+
const volumes = new Set();
|
|
869
|
+
|
|
870
|
+
for (const id of ids) {
|
|
871
|
+
const t = hooks.TARGETS[id];
|
|
872
|
+
if (!t) continue;
|
|
873
|
+
for (const { command } of readVibeHooks(t.file(), t)) {
|
|
874
|
+
const genre = /--genre (\S+)/.exec(command);
|
|
875
|
+
const volume = /--volume (\d+)/.exec(command);
|
|
876
|
+
if (genre && isKnownGenre(genre[1])) genres.add(genre[1]);
|
|
877
|
+
if (volume) volumes.add(normalizeVolume(volume[1], null));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Only when they agree. The saved config is one value for the machine, so
|
|
882
|
+
// two agents pinned to different genres have no single answer to inherit -
|
|
883
|
+
// and taking whichever was found first would let an untouched install for
|
|
884
|
+
// some other tool silently overwrite the genre the user actually chose.
|
|
885
|
+
return {
|
|
886
|
+
genre: genres.size === 1 ? [...genres][0] : null,
|
|
887
|
+
volume: volumes.size === 1 ? [...volumes][0] : null
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function pinnedHookSettings() {
|
|
892
|
+
const hooks = require("./hooks");
|
|
893
|
+
const names = [];
|
|
894
|
+
for (const id of Object.keys(hooks.TARGETS)) {
|
|
895
|
+
const t = hooks.TARGETS[id];
|
|
896
|
+
if (readVibeHooks(t.file(), t).some(({ command }) => /--genre |--volume /.test(command))) {
|
|
897
|
+
names.push(t.name);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return names;
|
|
901
|
+
}
|
|
902
|
+
|
|
649
903
|
function signalExitCode(signal) {
|
|
650
904
|
return 128 + (os.constants.signals[signal] || 0);
|
|
651
905
|
}
|
|
@@ -670,7 +924,7 @@ function previewGenre(genre, volume) {
|
|
|
670
924
|
spawnSync(backend.cmd, backend.args(audioFile, volume), { stdio: "ignore" });
|
|
671
925
|
}
|
|
672
926
|
|
|
673
|
-
function runHookAction(action, { genre, volume, chimeVolume, noChime, reactive, tools, dryRun }) {
|
|
927
|
+
function runHookAction(action, { genre, volume, chimeVolume, noChime, reactive, tools, dryRun, typed = {} }) {
|
|
674
928
|
const hooks = require("./hooks");
|
|
675
929
|
|
|
676
930
|
switch (action) {
|
|
@@ -693,7 +947,8 @@ function runHookAction(action, { genre, volume, chimeVolume, noChime, reactive,
|
|
|
693
947
|
outcome: hooks.outcomeFromPayload(raw),
|
|
694
948
|
volume,
|
|
695
949
|
chimeVolume,
|
|
696
|
-
noChime
|
|
950
|
+
noChime,
|
|
951
|
+
raw
|
|
697
952
|
});
|
|
698
953
|
});
|
|
699
954
|
return;
|
|
@@ -714,7 +969,7 @@ function runHookAction(action, { genre, volume, chimeVolume, noChime, reactive,
|
|
|
714
969
|
return;
|
|
715
970
|
|
|
716
971
|
case "install-hooks":
|
|
717
|
-
return installHookTargets(resolveTargets(tools), genre, volume, reactive, dryRun);
|
|
972
|
+
return installHookTargets(resolveTargets(tools), genre, volume, reactive, dryRun, typed);
|
|
718
973
|
|
|
719
974
|
case "uninstall-hooks":
|
|
720
975
|
return uninstallHookTargets();
|
|
@@ -876,6 +1131,8 @@ async function run() {
|
|
|
876
1131
|
reactive,
|
|
877
1132
|
dryRun,
|
|
878
1133
|
tools,
|
|
1134
|
+
typed,
|
|
1135
|
+
hereOnly,
|
|
879
1136
|
cmdArgs
|
|
880
1137
|
} = parseArgs(process.argv);
|
|
881
1138
|
|
|
@@ -889,7 +1146,7 @@ async function run() {
|
|
|
889
1146
|
|
|
890
1147
|
if (hookAction) {
|
|
891
1148
|
try {
|
|
892
|
-
return runHookAction(hookAction, { genre, volume, chimeVolume, noChime, reactive, tools, dryRun });
|
|
1149
|
+
return runHookAction(hookAction, { genre, volume, chimeVolume, noChime, reactive, tools, dryRun, typed });
|
|
893
1150
|
} catch (e) {
|
|
894
1151
|
// Settings problems are the user's to fix — report them, don't stack-trace.
|
|
895
1152
|
console.error(`\x1b[31m[vibeaudio] ${e.message}\x1b[0m`);
|
|
@@ -950,6 +1207,15 @@ async function run() {
|
|
|
950
1207
|
return previewGenre(preview, volume);
|
|
951
1208
|
}
|
|
952
1209
|
|
|
1210
|
+
// Settings typed with no command to run: save them, don't open the launcher.
|
|
1211
|
+
if (cmdArgs.length === 0 && Object.keys(typed).length > 0) {
|
|
1212
|
+
return saveDefaults(typed, hereOnly);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (hereOnly) {
|
|
1216
|
+
console.error("\x1b[33m[vibeaudio] --here only applies when saving a setting; ignoring it.\x1b[0m");
|
|
1217
|
+
}
|
|
1218
|
+
|
|
953
1219
|
if (cmdArgs.length === 0) {
|
|
954
1220
|
if (!process.stdin.isTTY) {
|
|
955
1221
|
printHelp();
|
|
@@ -978,7 +1244,8 @@ async function run() {
|
|
|
978
1244
|
// Either way, say so and still launch the tool the user asked for -
|
|
979
1245
|
// they came here to start an agent, not to configure one.
|
|
980
1246
|
try {
|
|
981
|
-
installHookTargets([selection.hookTarget], selection.genre, chosenVol, selection.reactive
|
|
1247
|
+
installHookTargets([selection.hookTarget], selection.genre, chosenVol, selection.reactive, false,
|
|
1248
|
+
{ genre: selection.genre, volume: chosenVol });
|
|
982
1249
|
} catch (err) {
|
|
983
1250
|
console.error(`\x1b[31m[vibeaudio] ${err.message}\x1b[0m`);
|
|
984
1251
|
}
|