vibeaudio 0.25.0 → 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
@@ -445,14 +451,14 @@ To update later, run `claude plugin marketplace update vibeaudio` and then `clau
445
451
  /vibeaudio genre zen here genre, volume and music also take "here": save for this project only
446
452
  /vibeaudio card make your week card; the pane shows it in terminals that draw images
447
453
  /vibeaudio band on an always-visible logo band above the prompt (off by default; band off removes it)
448
- /vibeaudio usage on add context fill and plan limits (ctx ██░░░ 42% 5h █░░░░ 31%, green, amber from 60%, red from 80%) to the band (off by default; usage off removes it)
454
+ /vibeaudio usage on add context fill and plan limits (ctx 42% 5h 31%) to the band (off by default; usage off removes it)
449
455
  /vibeaudio doctor the setup check in a pane, each problem with its fix (vibe --doctor)
450
456
  /vibeaudio help every command on one line, and the "here" option
451
457
  ```
452
458
 
453
- The pane is four cards under a logo header with a status pill (Playing, Ready, Muted or Music off): Now playing (mute for 15 minutes or an hour, stop the music, send a test signal), Sound (genre, volume, **Preview** plays one loop of the chosen genre, and where to save), Alerts (speech, banners, `vibe --announce`) and Share, which holds your last 7 days as a small chart and the week card buttons. Its colours come from the site and adapt to your Claude Code theme, light or dark. While you are muted, a line above the prompt says how many minutes are left, so a forgotten mute does not look like a broken tool. Nothing else is shown unless you turn the strip on.
459
+ The pane is four cards under a logo header with a status pill (Playing, Ready, Muted or Music off): Now playing (mute for 15 minutes or an hour, stop the music, send a test signal), Sound (genre, volume, **Preview** plays one loop of the chosen genre, and where to save), Alerts (speech, banners, `vibe --announce`) and Share, which holds your last 7 days as a small chart and the week card buttons. Its colours come from the site and adapt to your Claude Code theme, light or dark. While you are muted, a line above the prompt says how many minutes are left, so a forgotten mute does not look like a broken tool. Nothing else is shown unless you turn the band on.
454
460
 
455
- The optional **band** (`/vibeaudio band on`, remembered across sessions) puts the VibeAudio logo above the prompt, three level bars that move while music is playing (an SVG on the Desktop app, text bars in the terminal). Next to it: the genre and volume, your last 7 days as the week card counts them (turns and time waited, read from `~/.vibeaudio/history.jsonl`), and what your other agents are doing (`api waiting, web working`, in yellow when one needs you). The buttons are icons to save room: 🔇/🔊 mute and unmute, 🎵 music on or off, 🎼 the next genre, − and + for volume, 📊 the week card (then 🖼 to open it) and ⚙ for the controls pane. In a short window it shrinks to one line. It runs `vibe --state` every 5 seconds and redraws the text bars twice a second only in the terminal, while it is on and music is playing; on the Desktop app the SVG animates itself, so only the level bars inside the headphones move. Tab to it with Ctrl+X then Tab. With `/vibeaudio usage on` it also shows how full the context window is and, on a Claude plan, your 5-hour and 7-day limits, turning the warning colour at 80%; it reads the same figures Claude Code's status line has, and nothing else. While Claude works, the spinner also says what is playing (`· ♪ jazz…`, or `· ♪ muted`).
461
+ The optional **band** (`/vibeaudio band on`, remembered across sessions) puts the VibeAudio logo above the prompt, three level bars that move while music is playing (an SVG on the Desktop app, text bars in the terminal). Next to it: the genre and volume, your last 7 days as the week card counts them (turns and time waited, read from `~/.vibeaudio/history.jsonl`). The bars also move while another agent on the machine is working. The buttons are compact icons that show the current state: 🔊/🔇 sound on or muted (press to flip it), ♪ on/off for music, ♫ with the current genre (press for the next one), − and + for volume, ▤ week for the week card (then ↗ to open it) and ⚙ for the controls pane. In a short window it shrinks to one line. It runs `vibe --state` every 5 seconds and redraws the text bars twice a second only in the terminal, while it is on and music is playing; on the Desktop app the SVG animates itself, so only the level bars inside the headphones move. Tab to it with Ctrl+X then Tab. With `/vibeaudio usage on` it also shows how full the context window is and, on a Claude plan, your 5-hour and 7-day limits, turning the warning colour at 80%; it reads the same figures Claude Code's status line has, and nothing else. While Claude works, the spinner also says what is playing (`· ♪ jazz…`, or `· ♪ muted`).
456
462
 
457
463
  - **Per project.** The pane's **Saving** button switches genre, volume and music between everywhere and this project, and the pane notes when this folder has its own settings (`vibe --genre zen --here`, found from a subfolder the way the CLI does). There is no way to remove a project's settings yet, in the mod or the CLI.
458
464
  - **It changes nothing itself.** Every action runs the `vibe` command with a fixed argument list (no shell), so settings stay in `~/.vibeaudio/config.json` and the pane cannot disagree with the CLI. The one thing it keeps for itself is whether the band is on, in Claude Code's own store for the mod. It needs `vibe` on your `PATH` (`npm i -g vibeaudio`) and **Claude Code 2.1.287 or later** (Desktop app 2.1.286). It draws in the terminal and the Desktop app; in the VS Code panel and `claude -p` the same commands answer in text. It makes no network request.
@@ -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.0",
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,