vibeaudio 0.25.1 → 0.25.2
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 +10 -2
- package/package.json +1 -1
- package/src/cli.js +29 -9
- package/src/hooks.js +45 -0
package/README.md
CHANGED
|
@@ -160,7 +160,7 @@ vibe npm test
|
|
|
160
160
|
vibe claude -p "explain this repo"
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
**Lost?** `vibe --help` lists every flag, and `vibe --status` reads your live setup back to you — what's installed, what's playing, and which agents it found. If something seems broken, `vibe --doctor` checks it and prints the fix for each problem.
|
|
163
|
+
**Lost?** `vibe --help` lists every flag, and `vibe --status` reads your live setup back to you — what's installed, what's playing, and which agents it found. Its first line says why the music is silent or playing right now (`silent: muted for 42 more min (vibe --unmute)`, `playing: lofi @ 40%, 2 sessions working (api, web)`). If something seems broken, `vibe --doctor` checks it and prints the fix for each problem.
|
|
164
164
|
|
|
165
165
|
> **Which one you want:** the wrapper plays music for as long as the wrapped process lives. That's exactly right for a command that exits when its work is done — and wrong for an interactive REPL like `claude`, where the process stays alive while you read and type, so the music never stops. Hooks know when the agent is actually thinking; the wrapper can only time the process.
|
|
166
166
|
|
|
@@ -429,6 +429,12 @@ The optional [`/vibeaudio` mod](#vibeaudio-instant-controls-and-a-pane-claude-co
|
|
|
429
429
|
|
|
430
430
|
`/vibe` is a prompt, so Claude has to read it, run a command and answer: it costs a turn and a few tokens, and it waits for the current turn to end. The optional **VibeAudio mod** runs inside Claude Code instead, so `/vibeaudio mute` takes effect at once, even while Claude is working, and costs no tokens.
|
|
431
431
|
|
|
432
|
+
<p align="center">
|
|
433
|
+
<a href="https://kiril6.github.io/vibeaudio/#band-video"><img src="docs/video/band-poster.jpg" alt="The VibeAudio band above the Claude Code prompt" width="49%"></a>
|
|
434
|
+
<a href="https://kiril6.github.io/vibeaudio/#band-video"><img src="docs/video/pane.jpg" alt="The VibeAudio pane: now playing, sound, alerts and share" width="49%"></a>
|
|
435
|
+
<br><sub>▶ <a href="https://kiril6.github.io/vibeaudio/#band-video">Watch the 43-second demo</a> of the band and the pane (frames from the demo video, with sound on the site).</sub>
|
|
436
|
+
</p>
|
|
437
|
+
|
|
432
438
|
```text
|
|
433
439
|
claude plugin marketplace add kiril6/vibeaudio
|
|
434
440
|
claude plugin install vibeaudio-mod@vibeaudio
|
|
@@ -864,7 +870,7 @@ The full order, highest first: **a flag** → **an environment variable** → **
|
|
|
864
870
|
| `--no-hud` | Disable terminal window/tab title animation | `false` |
|
|
865
871
|
| `--preview <genre>` | Play one loop of a genre and exit | — |
|
|
866
872
|
| `--render [file]` | Write this project's music to a `.wav` and exit (full scale, ignores `--volume`) | `vibeaudio-<genre>.wav` |
|
|
867
|
-
| `--status` |
|
|
873
|
+
| `--status` | Say why the music is silent or playing, then show what's installed, running and detected, and exit | — |
|
|
868
874
|
| `--doctor` | Check the setup; every problem comes with the command that fixes it. Exits 1 on a failure, so it scripts | — |
|
|
869
875
|
| `--notify` / `--no-notify` | Also show a desktop banner naming the project when a turn finishes, fails or needs you. Saved to `config.json` | off |
|
|
870
876
|
| `--speak` / `--speak-always` / `--no-speak` | Say which project finished, failed or needs you, after the chime: only with two or more sessions going (`--speak`), or every time (`--speak-always`). Saved to `config.json` | off |
|
|
@@ -935,6 +941,8 @@ The expiry is deliberate too. A call is a bounded thing; a mute you forget about
|
|
|
935
941
|
|
|
936
942
|
## 🔧 Troubleshooting
|
|
937
943
|
|
|
944
|
+
**Start with `vibe --status`.** Its first line answers "why is it silent?" or "why is it still playing?" in one sentence, with the command that changes it: a mute and how long it has left, `VIBE_DISABLE` in your shell, no audio player, music turned off (globally, for this project, or by `VIBE_MUSIC`), a session waiting on you, or simply no agent working right now.
|
|
945
|
+
|
|
938
946
|
**No sound at all**
|
|
939
947
|
Check that a player exists for your platform (see [Requirements](#-requirements)). VibeAudio prints a notice to stderr when it can't find one. On Linux: `sudo apt install pulseaudio-utils` (or `ffmpeg` / `alsa-utils`).
|
|
940
948
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeaudio",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "Procedural focus music while your AI coding tools (Claude Code, Codex, Cursor, Grok, Gemini, Copilot) think — a different arrangement per project.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vibeaudio": "bin/vibeaudio.js",
|
package/src/cli.js
CHANGED
|
@@ -41,6 +41,9 @@ const HOOK_ACTIONS = [
|
|
|
41
41
|
];
|
|
42
42
|
|
|
43
43
|
function printHelp() {
|
|
44
|
+
// Both callers process.exit() next. On macOS a piped stdout is async, so Node 18/20
|
|
45
|
+
// dropped everything past the 8 KB pipe buffer (CI on #77). Blocking writes flush first.
|
|
46
|
+
process.stdout._handle?.setBlocking?.(true);
|
|
44
47
|
console.log(`
|
|
45
48
|
\x1b[1m\x1b[36mVibeAudio\x1b[0m v${pkg.version}
|
|
46
49
|
Procedural focus music while your AI coding tools think.
|
|
@@ -80,7 +83,8 @@ Procedural focus music while your AI coding tools think.
|
|
|
80
83
|
--no-hud Disable terminal window/tab title animation
|
|
81
84
|
--preview <genre> Play one loop of a genre and exit
|
|
82
85
|
--render [file] Write this project's music to a .wav and exit (full scale, ignores --volume)
|
|
83
|
-
--status
|
|
86
|
+
--status Say why the music is silent or playing, then show what is
|
|
87
|
+
installed, running and detected, and exit
|
|
84
88
|
--notify | --no-notify Also show a desktop banner naming the project when a turn finishes, fails, needs you or looks stuck (macOS and Linux; off by default)
|
|
85
89
|
--speak | --no-speak Also say which project finished, failed or needs you, after the chime - with two or more sessions going (off by default)
|
|
86
90
|
--speak-always Like --speak, but every time, even with one session
|
|
@@ -869,21 +873,37 @@ function uninstallHookTargets() {
|
|
|
869
873
|
* do". Every line is read from live state rather than assumed, because the
|
|
870
874
|
* usual cause of confusion is a mismatch between config and what is running.
|
|
871
875
|
*/
|
|
876
|
+
// What hooks.explainMusic() needs, read once: the same sources the sections of
|
|
877
|
+
// --status read, so the one-line answer cannot disagree with them.
|
|
878
|
+
function musicFacts() {
|
|
879
|
+
const hooks = require("./hooks");
|
|
880
|
+
const { muteState, playbackDisabled, detectPlayer: detect } = require("./player");
|
|
881
|
+
const mute = muteState();
|
|
882
|
+
const daemon = hooks.daemonPlaying();
|
|
883
|
+
return {
|
|
884
|
+
disabledByEnv: mute === null && playbackDisabled(),
|
|
885
|
+
mute: mute === null ? null : mute.until ? Math.max(1, Math.round((mute.until - Date.now()) / 60000)) : Infinity,
|
|
886
|
+
backend: Boolean(detect()),
|
|
887
|
+
daemon,
|
|
888
|
+
genre: genreNow(),
|
|
889
|
+
volume: daemon ? (daemon.follows ? Math.round(volumeNow() * 100) : daemon.volume) : null,
|
|
890
|
+
music: musicEnabled(),
|
|
891
|
+
musicSource: process.env.VIBE_MUSIC ? "env" : projectSettings(loadConfig()).music !== undefined ? "project" : "",
|
|
892
|
+
sessions: hooks.agentState().sessions
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
872
896
|
function printStatus() {
|
|
873
897
|
const hooks = require("./hooks");
|
|
874
|
-
const {
|
|
898
|
+
const { detectPlayer: detect, CACHE_ROOT } = require("./player");
|
|
875
899
|
const on = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
876
900
|
const off = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
877
901
|
|
|
878
902
|
console.log(`\n\x1b[1m\x1b[36mVibeAudio\x1b[0m v${pkg.version}\n`);
|
|
879
903
|
|
|
880
|
-
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
console.log(`\x1b[33m🔇 Muted ${muteRemainingText(mute)}\x1b[0m \x1b[90m(since ${mute.since})\x1b[0m\n`);
|
|
884
|
-
} else if (playbackDisabled()) {
|
|
885
|
-
console.log(`\x1b[33m⏸ Muted by VIBE_DISABLE=${process.env.VIBE_DISABLE}\x1b[0m — automatic playback is off (--preview still works).\n`);
|
|
886
|
-
}
|
|
904
|
+
// The answer first (#76): everything below is the detail behind it.
|
|
905
|
+
const now = hooks.explainMusic(musicFacts());
|
|
906
|
+
console.log(`\x1b[1mNow\x1b[0m ${now.playing ? on(now.text) : `\x1b[33m${now.text}\x1b[0m`}\n`);
|
|
887
907
|
|
|
888
908
|
// What will actually play, and which of the three possible sources decided
|
|
889
909
|
// it - the usual confusion is a saved default quietly beaten by an export.
|
package/src/hooks.js
CHANGED
|
@@ -805,6 +805,50 @@ function formatStatusline({ status, sessions }) {
|
|
|
805
805
|
return `${status}${project}${active.length > 1 ? ` ×${active.length}` : ""}`;
|
|
806
806
|
}
|
|
807
807
|
|
|
808
|
+
/**
|
|
809
|
+
* "Why is it silent?" / "why is it still playing?" as one line, the first of
|
|
810
|
+
* --status (#76). Pure over what --status gathers (`musicFacts()` in cli.js),
|
|
811
|
+
* so the order is the test: what silences everything (VIBE_DISABLE, a mute)
|
|
812
|
+
* outranks what silences only the music, and a running player outranks every
|
|
813
|
+
* reason it might not be running. Each silent line names the command that
|
|
814
|
+
* changes it. `mute` is the minutes left, Infinity for an open-ended mute,
|
|
815
|
+
* null when not muted; `musicSource` is "env", "project" or "" (saved or default).
|
|
816
|
+
*/
|
|
817
|
+
function explainMusic({ disabledByEnv, mute, backend, daemon, genre, volume, music, musicSource, sessions }) {
|
|
818
|
+
const names = (list) => {
|
|
819
|
+
const all = [...new Set(list.map((s) => (s.project ? projectName(s.project) : null)).filter(Boolean))];
|
|
820
|
+
return all.length > 3 ? `${all.slice(0, 3).join(", ")} +${all.length - 3}` : all.join(", ");
|
|
821
|
+
};
|
|
822
|
+
const counted = (list, word) => {
|
|
823
|
+
const n = names(list);
|
|
824
|
+
return `${list.length} session${list.length === 1 ? "" : "s"} ${word}${n ? ` (${n})` : ""}`;
|
|
825
|
+
};
|
|
826
|
+
const silent = (text) => ({ playing: false, text: `silent: ${text}` });
|
|
827
|
+
const working = sessions.filter((s) => s.state !== "waiting");
|
|
828
|
+
const waiting = sessions.filter((s) => s.state === "waiting");
|
|
829
|
+
|
|
830
|
+
if (disabledByEnv) return silent("VIBE_DISABLE is set in this shell (unset it to hear music)");
|
|
831
|
+
if (mute != null) return silent(`muted ${mute === Infinity ? "until you unmute" : `for ${mute} more min`} (vibe --unmute)`);
|
|
832
|
+
if (!backend) return silent("no audio player found on this machine (vibe --doctor)");
|
|
833
|
+
if (daemon) {
|
|
834
|
+
const stuck = sessions.filter((s) => s.state === "stuck");
|
|
835
|
+
const level = volume === null ? "" : ` @ ${volume}%`;
|
|
836
|
+
return {
|
|
837
|
+
playing: true,
|
|
838
|
+
text: `playing: ${daemon.genre || genre}${level}, ${working.length ? counted(working, "working") : "finishing a turn"}` +
|
|
839
|
+
`${stuck.length ? `; heartbeat on, ${names(stuck) || "a session"} keeps failing` : ""}`
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
if (waiting.length && !working.length) return silent(`${counted(waiting, "waiting on you")}; music resumes when you answer`);
|
|
843
|
+
if (!music) {
|
|
844
|
+
// The fix has to beat whatever turned it off: a saved global does not outrank an export or --here.
|
|
845
|
+
const [where, fix] = { env: [" by VIBE_MUSIC in this shell", "unset VIBE_MUSIC"], project: [" for this project", "vibe --music on --here"] }[musicSource] || ["", "vibe --music on"];
|
|
846
|
+
return silent(`music is off${where}; chimes still play (${fix})`);
|
|
847
|
+
}
|
|
848
|
+
if (working.length) return silent(`${counted(working, "working")} but the player is not running (vibe --doctor)`);
|
|
849
|
+
return silent("no agent is working; music starts with your next prompt");
|
|
850
|
+
}
|
|
851
|
+
|
|
808
852
|
const EVENTS_MAX_BYTES = 256 * 1024;
|
|
809
853
|
|
|
810
854
|
/**
|
|
@@ -1781,6 +1825,7 @@ module.exports = {
|
|
|
1781
1825
|
STUCK_WINDOW,
|
|
1782
1826
|
agentState,
|
|
1783
1827
|
formatStatusline,
|
|
1828
|
+
explainMusic,
|
|
1784
1829
|
followEvents,
|
|
1785
1830
|
EVENTS_FILE,
|
|
1786
1831
|
hookEnd,
|