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 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` | Show what's installed, running and detected, then exit | — |
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.1",
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 Show what is installed, running and detected, then exit
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 { playbackDisabled, detectPlayer: detect, CACHE_ROOT } = require("./player");
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
- const { muteState, muteRemainingText } = require("./player");
881
- const mute = muteState();
882
- if (mute !== null) {
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,