vibeaudio 0.23.0 → 0.25.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/README.md CHANGED
@@ -423,7 +423,7 @@ Installing the Claude Code hooks also adds a `/vibe` command (`~/.claude/command
423
423
  /vibe volume 20 change volume, keeping your genre
424
424
  ```
425
425
 
426
- It asks Claude to run the matching `vibe` command, so Claude Code will ask permission for that command the first time. If you already have your own `vibe.md` there, VibeAudio leaves it alone and says so; `--uninstall-hooks` removes only its own.
426
+ The optional [`/vibeaudio` mod](#vibeaudio-instant-controls-and-a-pane-claude-code-mod) does the same instantly with no turn; `/vibe` is the version that works everywhere. It asks Claude to run the matching `vibe` command, so Claude Code will ask permission for that command the first time. If you already have your own `vibe.md` there, VibeAudio leaves it alone and says so; `--uninstall-hooks` removes only its own.
427
427
 
428
428
  ### `/vibeaudio`: instant controls and a pane (Claude Code mod)
429
429
 
@@ -436,24 +436,28 @@ claude plugin install vibeaudio-mod@vibeaudio
436
436
 
437
437
  Then `/reload-plugins` or a new session; `/plugin` shows `1 mod active`.
438
438
 
439
+ To update later, run `claude plugin marketplace update vibeaudio` and then `claude plugin update vibeaudio-mod@vibeaudio`, followed by `/reload-plugins`. The mod does not update when the `vibe` command does.
440
+
439
441
  ```text
440
- /vibeaudio open the pane: mute, music, genre, volume, speech, banners, test signal
442
+ /vibeaudio open the pane: mute, stop, music, genre, preview, volume, speech, banners, announcements, test signal
441
443
  /vibeaudio mute 30 silence for 30 minutes (unmute ends it)
442
- /vibeaudio genre jazz also: volume <n>, music on|off, speak off|auto|always, notify on|off, stop
444
+ /vibeaudio genre jazz also: volume <n>, music on|off, speak off|auto|always, notify on|off, announce on|off, preview, stop
443
445
  /vibeaudio genre zen here genre, volume and music also take "here": save for this project only
444
446
  /vibeaudio card make your week card; the pane shows it in terminals that draw images
445
- /vibeaudio strip on an always-visible logo band above the prompt (off by default; strip off removes it)
447
+ /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)
446
449
  /vibeaudio doctor the setup check in a pane, each problem with its fix (vibe --doctor)
450
+ /vibeaudio help every command on one line, and the "here" option
447
451
  ```
448
452
 
449
- The pane is four cards under a logo header with a status pill (Playing, Ready, Muted or Music off): Now playing, Sound, Alerts 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.
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.
450
454
 
451
- The optional **strip** (`/vibeaudio strip 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 **Mute**/**Unmute**, **Music**, **Vol -**, **Vol +**, **Week card** (makes the share card, then **Open card**) and **Controls**. In a short window it shrinks to one line. It runs `vibe --state` every 5 seconds and redraws the bars twice a second only while it is on and music is playing. Tab to it with Ctrl+X then Tab. While Claude works, the spinner also says what is playing (`· ♪ jazz…`, or `· ♪ muted`).
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`).
452
456
 
453
457
  - **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.
454
- - **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 strip 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.
455
- - **A mod is code, not configuration.** It runs inside Claude Code with your permissions and is not sandboxed, which is why `--install-hooks` never installs it for you. Read it first: it is [`mods/vibe/hooks/register.js`](mods/vibe/hooks/register.js), and `claude plugin validate mods/vibe` lists every call it makes (it reads three files in `~/.vibeaudio/` (`config.json`, `muted`, `history.jsonl`), your Claude Code theme, runs `vibe`, and draws).
456
- - **`/vibe` stays.** It works on every Claude Code version and surface; the mod is the faster path where it loads. Remove the mod with `claude plugin uninstall vibeaudio-mod@vibeaudio`.
458
+ - **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.
459
+ - **A mod is code, not configuration.** It runs inside Claude Code with your permissions and is not sandboxed, which is why `--install-hooks` never installs it for you; it prints the two commands until Claude Code lists the mod as installed. Read it first: it is [`mods/vibe/hooks/register.js`](mods/vibe/hooks/register.js), and `claude plugin validate mods/vibe` lists every call it makes (it reads three files in `~/.vibeaudio/` (`config.json`, `muted`, `history.jsonl`), your Claude Code theme, the session's usage figures (only with `usage on`), runs `vibe`, and draws).
460
+ - **`/vibe` stays, as the fallback.** It is a prompt, so it costs a turn, but it works on every Claude Code version and surface, including where the mod does not load (older than 2.1.287, or a surface that skips mods). Use `/vibeaudio` where the mod loads and `/vibe` everywhere else; both change the same settings in `~/.vibeaudio/config.json`. The `vibe` and `vibeaudio` commands in your shell are the same program. Remove the mod with `claude plugin uninstall vibeaudio-mod@vibeaudio`.
457
461
 
458
462
  ### Changing the sound later
459
463
 
@@ -482,7 +486,7 @@ vibe --reactive --install-hooks # on
482
486
  vibe --install-hooks # off again
483
487
  ```
484
488
 
485
- Music already playing keeps the old genre until the next prompt swaps the daemon — `vibe --stop` cuts it short. A new **volume** is the exception: the music playing now eases to it within a second.
489
+ A new **genre** (saved everywhere, not with `--here`) replaces the music playing right now while a turn is running, and so does `--music off` or `on`. A new **volume** eases in within a second. `vibe --stop` ends the music and the running turns.
486
490
 
487
491
  Removing them is one command:
488
492
 
@@ -874,7 +878,7 @@ The full order, highest first: **a flag** → **an environment variable** → **
874
878
  | `--announce <text>` | Play the chime for something that isn't an agent (CI, a deploy, a script), then exit. Add `--outcome success\|failure\|attention` or `--exit-code $?` | `success` |
875
879
  | `--stop` | Stop the background player, then exit | — |
876
880
  | `--mute [minutes]` | Silence everything for a call, then exit | `60` min (`0` = until unmuted) |
877
- | `--unmute` | Resume normal playback, then exit | — |
881
+ | `--unmute` | Resume normal playback, then exit; a turn still running gets its music back at once | — |
878
882
  | `--clear-cache` | Delete all cached audio, then exit | — |
879
883
  | `--mcp` | Run as an MCP stdio server for desktop apps | — |
880
884
  | `--install-hooks` | Wire music into your agent's hooks (no wrapper needed) | — |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeaudio",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
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
@@ -777,6 +777,7 @@ function installHookTargets(ids, genre, volume, reactive, dryRun = false, typed
777
777
  const slash = hooks.installSlashCommand({ dryRun });
778
778
  if (!slash.installed) console.log(` \x1b[33m/vibe not installed: ${slash.reason} (${slash.file})\x1b[0m`);
779
779
  else console.log(` ${"/vibe".padEnd(19)}→ ${dryRun ? "would write" : "control the music from inside Claude Code"} (${slash.file})`);
780
+ if (!dryRun && !hooks.modInstalled()) console.log(` \x1b[2mOptional: /plugin marketplace add kiril6/vibeaudio, then /plugin install vibeaudio-mod@vibeaudio - instant /vibeaudio controls, a pane and a band above the prompt.\x1b[0m`);
780
781
  }
781
782
  // Codex will not run a hook it has not been told to trust, so saying
782
783
  // "done" without this would be reporting an install that isn't live yet.
@@ -1358,7 +1359,7 @@ function detectAiTools(hooked = new Set()) {
1358
1359
  * natural way to phrase the request did something else entirely, and the
1359
1360
  * README had to warn about it. It saves the default instead.
1360
1361
  */
1361
- function saveDefaults(typed, hereOnly = false) {
1362
+ function saveDefaults(typed, hereOnly = false, live = null) {
1362
1363
  const patch = {};
1363
1364
  if (typed.genre !== undefined) patch.genre = typed.genre;
1364
1365
  if (typed.volume !== undefined) patch.volume = Math.round(typed.volume * 100);
@@ -1385,11 +1386,17 @@ function saveDefaults(typed, hereOnly = false) {
1385
1386
 
1386
1387
  // Music already in the air is the one thing this does not reach, and
1387
1388
  // silence about it reads as the setting having done nothing at all.
1388
- const playing = require("./hooks").daemonPlaying();
1389
- if (playing && patch.genre && playing.genre && playing.genre !== patch.genre) {
1389
+ const hooks = require("./hooks");
1390
+ const playing = hooks.daemonPlaying();
1391
+ // A project's own settings do not say what the music in the air should be, so --here leaves it be.
1392
+ const retuned = live && !hereOnly && (patch.genre !== undefined || patch.music !== undefined)
1393
+ && hooks.applyLiveMusic(live.genre, live.volume, { music: live.music, follow: live.follow });
1394
+ if (retuned) {
1395
+ console.log(` \x1b[90mThe music playing now changed with it.\x1b[0m`);
1396
+ } else if (playing && patch.genre && playing.genre && playing.genre !== patch.genre) {
1390
1397
  console.log(` \x1b[90m${playing.genre} is still playing — vibe --stop cuts it short.\x1b[0m`);
1391
1398
  }
1392
- if (playing && patch.music === false) {
1399
+ if (!retuned && playing && patch.music === false) {
1393
1400
  console.log(` \x1b[90m${playing.genre || "The music"} is still playing — vibe --stop cuts it short.\x1b[0m`);
1394
1401
  }
1395
1402
  if (patch.music === false) {
@@ -1903,7 +1910,9 @@ async function run() {
1903
1910
 
1904
1911
  if (!muteChange) {
1905
1912
  setMuted(false);
1906
- console.log(`\x1b[32m🔊 Unmuted.\x1b[0m Music returns on your next prompt.\n`);
1913
+ // A turn already running gets its music back now; only an idle machine waits for a prompt.
1914
+ const resumed = require("./hooks").resumeAfterUnmute(genre, volume, { music, follow: typed.volume === undefined });
1915
+ console.log(`\x1b[32m🔊 Unmuted.\x1b[0m ${resumed ? "Music is back." : "Music returns on your next prompt."}\n`);
1907
1916
  return;
1908
1917
  }
1909
1918
 
@@ -2028,7 +2037,7 @@ async function run() {
2028
2037
 
2029
2038
  // Settings typed with no command to run: save them, don't open the launcher.
2030
2039
  if (cmdArgs.length === 0 && Object.keys(typed).length > 0) {
2031
- return saveDefaults(typed, hereOnly);
2040
+ return saveDefaults(typed, hereOnly, { genre, volume, music, follow: typed.volume === undefined });
2032
2041
  }
2033
2042
 
2034
2043
  if (hereOnly) {
package/src/hooks.js CHANGED
@@ -1387,6 +1387,33 @@ function hookResume(raw, genre, volume, { reactive = false, follow = false, musi
1387
1387
  return true;
1388
1388
  }
1389
1389
 
1390
+ /**
1391
+ * An unmute while a turn is in flight: a mute stopped the player, and nothing
1392
+ * else would start one until the next prompt. Only for a session that is
1393
+ * working (a waiting one is waiting on the user, and says so with silence), and
1394
+ * never over a daemon already playing. Not reactive: that flag lives in the hook
1395
+ * commands, so a reactive install is plain until its next tool-driven resume.
1396
+ */
1397
+ function resumeAfterUnmute(genre, volume, { music = true, follow = false } = {}) {
1398
+ if (!music || daemonRunning() || working(listSessions()).length === 0) return false;
1399
+ spawnDaemon(genre, volume, false, follow);
1400
+ return true;
1401
+ }
1402
+
1403
+ /**
1404
+ * A saved genre or music switch reaches music already playing now, not at the
1405
+ * next prompt: whoever pressed the button is listening. The player is replaced
1406
+ * (or just stopped for music off) while the sessions stay, so chimes, events and
1407
+ * --report carry on. Returns whether anything changed.
1408
+ */
1409
+ function applyLiveMusic(genre, volume, { music = true, follow = false } = {}) {
1410
+ const was = daemonRunning();
1411
+ if (was) stopDaemon({ keepSessions: true });
1412
+ const start = music && !playbackDisabled() && working(listSessions()).length > 0;
1413
+ if (start) spawnDaemon(genre, volume, false, follow);
1414
+ return was || start;
1415
+ }
1416
+
1390
1417
  /**
1391
1418
  * The agent's session is over (quit, /clear, logout). A turn cut off by it
1392
1419
  * never reaches Stop. Silent: nothing finished.
@@ -1641,6 +1668,11 @@ function slashCommandFile() {
1641
1668
  return path.join(claudeConfigDir(), "commands", "vibe.md");
1642
1669
  }
1643
1670
 
1671
+ // The optional in-app mod (mods/vibe). Claude Code records installed plugins by name in this file.
1672
+ function modInstalled(file = path.join(claudeConfigDir(), "plugins", "installed_plugins.json")) {
1673
+ try { return fs.readFileSync(file, "utf8").includes("vibeaudio-mod"); } catch { return false; }
1674
+ }
1675
+
1644
1676
  function slashCommandText() {
1645
1677
  const cli = `${shellQuote(process.execPath)} ${shellQuote(CLI_ENTRY)}`;
1646
1678
  return `---
@@ -1720,6 +1752,7 @@ function uninstallHooks(file = null, { id = "claude" } = {}) {
1720
1752
  module.exports = {
1721
1753
  shellQuote,
1722
1754
  claudeConfigDir,
1755
+ modInstalled,
1723
1756
  codexHome,
1724
1757
  geminiDir,
1725
1758
  qwenHome,
@@ -1742,6 +1775,8 @@ module.exports = {
1742
1775
  hookTool,
1743
1776
  hookWait,
1744
1777
  hookResume,
1778
+ resumeAfterUnmute,
1779
+ applyLiveMusic,
1745
1780
  isStuck,
1746
1781
  STUCK_WINDOW,
1747
1782
  agentState,