vibeaudio 0.14.0 โ†’ 0.14.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
@@ -1,5 +1,9 @@
1
1
  # ๐ŸŽง VibeAudio
2
2
 
3
+ <p align="center">
4
+ <img src="docs/og.png?v=2" alt="VibeAudio โ€” Focus music while your AI codes" width="720">
5
+ </p>
6
+
3
7
  [![test](https://github.com/kiril6/vibeaudio/actions/workflows/test.yml/badge.svg)](https://github.com/kiril6/vibeaudio/actions/workflows/test.yml) [![npm](https://img.shields.io/npm/v/vibeaudio)](https://www.npmjs.com/package/vibeaudio) [![downloads](https://img.shields.io/npm/d18m/vibeaudio?label=downloads)](https://npm-stat.com/charts.html?package=vibeaudio) [![downloads/month](https://img.shields.io/npm/dm/vibeaudio)](https://npm-stat.com/charts.html?package=vibeaudio)
4
8
 
5
9
  > **Procedural focus music while your AI coding tools think.**
@@ -31,7 +35,8 @@ It is built as **calm technology**, in the sense of Mark Weiser and John Seely B
31
35
 
32
36
  **How it behaves:**
33
37
 
34
- * ๐Ÿงฎ **Pure synthesis, zero MP3s.** Every note, chord and pad is generated in code โ€” no audio assets, no npm dependencies, no `node-gyp`.
38
+ * ๐Ÿงฎ **Pure synthesis, zero MP3s.** Every note, chord and pad is generated in code โ€” no audio assets, no npm dependencies, no install scripts, no `node-gyp`.
39
+ * ๐Ÿƒ **Light on battery.** Each loop is rendered once and cached. While music plays, your OS's own audio player does the work. With hooks, nothing of VibeAudio's keeps running between turns.
35
40
  * ๐ŸŽผ **A different arrangement per project.** Your working directory seeds the progression, bass line and melody, so each repo has its own sound and keeps it.
36
41
  * ๐Ÿ“ˆ **Escalating layers.** Tier 1 (0โ€“15s) gentle intro โ†’ Tier 2 (15โ€“45s) main groove โ†’ Tier 3 (45s+) deep focus. You can hear how deep into the task the agent is.
37
42
  * ๐Ÿ” **A phrase, not a loop.** Each piece is three bars that rotate through your project's own progressions, so a long turn moves through a ~21-second phrase instead of replaying one 7-second bar for half an hour.
@@ -48,7 +53,7 @@ It is built as **calm technology**, in the sense of Mark Weiser and John Seely B
48
53
  * ๐Ÿ›ก๏ธ **A grace window.** Fast commands stay 100% silent โ€” music starts only past 1.5s (`--grace`).
49
54
  * ๐ŸชŸ **Several sessions, one soundtrack.** Run as many terminals of the same agent as you like: the music plays while *any* of them is working, and each finishes with its own chime. One session ending, pausing for a permission dialog or being interrupted never cuts off another that's still going.
50
55
  * ๐Ÿงฎ **Honest exit codes.** Your command's status passes straight through (`130` on `Ctrl+C`), so `vibe claude && next-step` behaves exactly as it would without the wrapper.
51
- * ๐ŸŒŠ **Terminal title HUD.** A live ASCII wave and elapsed timer in the window title, where it can't corrupt a full-screen TUI.
56
+ * ๐ŸŒŠ **Terminal title HUD.** A live ASCII wave and elapsed timer in the window title (`[ โ™ซ โ–ƒโ–…โ–†โ–‡โ–ˆโ–‡โ–†โ–ƒ lofi (0:24) ]`), where it can't corrupt a full-screen TUI.
52
57
 
53
58
  </details>
54
59
 
@@ -351,11 +356,16 @@ Codex asks you to approve the plugin's hooks once, as it does for any hook. Upda
351
356
 
352
357
  The plugin needs Node 18+ on your `PATH` (the hooks run `node`). In Claude Code it also adds `/vibeaudio:vibe`, the plugin's spelling of [`/vibe`](#vibe-inside-claude-code). Update with `claude plugin update vibeaudio@vibeaudio`; remove with `claude plugin uninstall vibeaudio@vibeaudio` (music already playing stops on its own within 15 minutes, or run `/vibeaudio:vibe stop` first).
353
358
 
359
+ <details>
360
+ <summary><b>Plugin details & coexistence with npm</b> โ€” settings file, coexistence, and limitations</summary>
361
+
354
362
  - **Settings are the same file.** `vibe --genre jazz` and the rest work as always, but the `vibe` command comes from `npm i -g vibeaudio`; the plugin alone puts nothing on your `PATH`. Use `/vibeaudio:vibe genre jazz` instead.
355
363
  - **Both at once is safe.** If `--install-hooks` has also been run, the plugin's hooks step aside and the installed ones play, so there's no double chime. Remove the installed hooks first if you want the plugin to own it.
356
364
  - **Claude Code and Codex.** Cursor, Gemini and the others still use `vibe --install-hooks`. Copilot CLI and Qwen Code can load the same plugin and it carries their events, but neither has been run with it yet.
357
365
  - **Not reactive.** `--reactive` is an `--install-hooks` option; the plugin doesn't carry it.
358
366
 
367
+ </details>
368
+
359
369
  ### `/vibe` inside Claude Code
360
370
 
361
371
  Installing the Claude Code hooks also adds a `/vibe` command (`~/.claude/commands/vibe.md`, or under `$CLAUDE_CONFIG_DIR`), so you can control the music without leaving the session:
@@ -418,8 +428,13 @@ Hooks log each finished turn โ€” which project, how long, how it ended, and how
418
428
  32s with a chime 1m 50s without (muted or --no-chime)
419
429
  ```
420
430
 
431
+ <details>
432
+ <summary><b>How the report metrics & return times are calculated</b></summary>
433
+
421
434
  plus where the time went by project and, for windows up to two weeks, by day. "You waited" is the agent's own working time; the time it spent blocked on you is shown separately, because they are different problems. "Back to it" is the other direction: how long a finished turn sat before your next prompt in the same session โ€” gaps over 30 minutes count as breaks and are left out. Once both sides have five turns, it is split by whether a chime announced the turn, so you can see what the chime is worth to you. The log stays on your machine, never leaves it, is capped at about 1 MB, and `VIBE_NO_HISTORY=1` turns it off. Only hook-driven turns are logged โ€” a command wrapped as `vibe <command>` is not, since its lifetime isn't the same thing as an agent's working time.
422
435
 
436
+ </details>
437
+
423
438
  ### The chime is in the music's key
424
439
 
425
440
  The success chime is the tonic chord of whatever key your genre sits in โ€” C major for lofi, 8bit, jazz and piano, D minor for synthwave and electronic, D major for zen, A minor for drone โ€” so it lands as the resolution of the piece rather than a bell over it. `rain`, `ocean` and `random` have no single key to match and keep the original C major chime.
@@ -428,10 +443,15 @@ The success chime is the tonic chord of whatever key your genre sits in โ€” C ma
428
443
 
429
444
  The worst stretch of agent work is twenty minutes of the same thing failing while the music says all is well. So when **4 of a session's last 8 tool calls fail**, a soft heartbeat โ€” two low beats about once a second, on the tonic of your genre โ€” joins the music at the next loop boundary, and leaves once enough calls succeed. With `--notify` on, you also get one banner when the session crosses the line ("api: looks stuck"), not one per failure.
430
445
 
446
+ <details>
447
+ <summary><b>How the stuck threshold was calibrated (4,497 turns analyzed)</b></summary>
448
+
431
449
  The threshold was picked against 4,497 real Claude Code turns (46,032 tool calls). It fires in about 1% of turns, at around minute 3, and those turns typically ran for another 3 minutes โ€” time you could have spent stepping in. "3 in a row" was rejected because the usual loop has a successful edit between every failing test run. Each new prompt starts with a clean slate, and an interrupt (Esc) never counts as a failure.
432
450
 
433
451
  It needs the agent to report failed tool calls, which **Claude Code, Copilot CLI and Qwen Code** do (`PostToolUseFailure`). Codex, Cursor, Gemini, Grok and Windsurf don't, so for them the music never adds the heartbeat.
434
452
 
453
+ </details>
454
+
435
455
  ### Reactive mode (opt-in)
436
456
 
437
457
  ```bash
@@ -482,16 +502,27 @@ Each session is `working`, `stuck` (4 of its last 8 tool calls failed) or `waiti
482
502
  {"v":1,"at":1791026130000,"event":"waiting","session":"โ€ฆ","project":"/work/api","tool":"Bash","status":"waiting"}
483
503
  ```
484
504
 
485
- Events: `started`, `waiting`, `resumed`, `stuck`, `recovered`, `finished` (with `outcome`: `success` or `failure`), `interrupted`, `ended` (with `reason` when `vibe --stop` or `--uninstall-hooks` ended it). Every line also carries `status`, the machine's state after the event, so a consumer that only cares about the overall state can read that one field. Some examples:
505
+ Events: `started`, `waiting`, `resumed`, `stuck`, `recovered`, `finished` (with `outcome`: `success` or `failure`), `interrupted`, `ended` (with `reason` when `vibe --stop` or `--uninstall-hooks` ended it). Every line also carries `status`, the machine's state after the event, so a consumer that only cares about the overall state can read that one field.
486
506
 
507
+ #### ๐Ÿ“Ÿ Status Bar & Desktop Integrations
508
+
509
+ **tmux status bar** โ€” show overall agent state (`waiting`, `stuck`, `working`, or `idle`):
487
510
  ```bash
488
- # tmux: show the state in the status bar
489
511
  set -g status-right '#(vibe --state | jq -r .status)'
512
+ ```
490
513
 
491
- # macOS: say it out loud when any agent needs you
514
+ **macOS speech alert** โ€” announce out loud whenever an agent stops to ask for your input:
515
+ ```bash
492
516
  vibe --events | jq --unbuffered -r 'select(.event=="waiting") | .project' | while read p; do say "$(basename "$p") needs you"; done
493
517
  ```
494
518
 
519
+ **Starship prompt** โ€” custom indicator in `~/.config/starship.toml`:
520
+ ```toml
521
+ [custom.vibe]
522
+ command = "vibe --state | jq -r 'if .status != \"idle\" then \"๐ŸŽง \" + .status else \"\" end'"
523
+ when = "command -v vibe >/dev/null"
524
+ ```
525
+
495
526
  The stream is a local file (`~/.vibeaudio/events.jsonl`, rotated at 256 KB), so it never leaves your machine. It keeps updating while you're muted, because a mute silences sound and a light isn't sound. The `v` field is the format version, and any breaking change will increment it.
496
527
 
497
528
  ## ๐Ÿ–ฅ๏ธ Everything Else (Claude Desktop, Antigravityโ€ฆ via MCP)
@@ -574,7 +605,7 @@ Playback stops automatically if the desktop client disconnects, and caps out aft
574
605
 
575
606
  ## ๐ŸŽจ Music Genres
576
607
 
577
- VibeAudio includes **8 procedural music styles** synthesized entirely in code:
608
+ VibeAudio includes **10 procedural sound styles** synthesized entirely in code:
578
609
 
579
610
  | Genre | Style | Vibe |
580
611
  | :--- | :--- | :--- |
@@ -590,7 +621,8 @@ VibeAudio includes **8 procedural music styles** synthesized entirely in code:
590
621
  | `ocean` | ๐ŸŒŠ **Ocean** | Low surf on a slow swell that rises and drains โ€” **no melody at all** |
591
622
  | `random` | ๐ŸŽฒ **Shuffle Mode** | Picks a surprise genre for the run โ€” **never `drone`, `rain` or `ocean`** |
592
623
 
593
- **Aliases also work**, so you can ask for a genre the way you'd say it โ€” `vibe --preview chill` is `lofi`:
624
+ <details>
625
+ <summary><b>Genre aliases</b> โ€” you can ask for a genre the way you'd say it (<code>chill</code>, <code>retrowave</code>, <code>bossa</code>, <code>ambient</code>โ€ฆ)</summary>
594
626
 
595
627
  | Canonical | Also accepted |
596
628
  | :--- | :--- |
@@ -607,6 +639,8 @@ VibeAudio includes **8 procedural music styles** synthesized entirely in code:
607
639
 
608
640
  Matching is case-insensitive, so `BOSSA` works too.
609
641
 
642
+ </details>
643
+
610
644
  > **If any melody distracts you, use `drone`.** Every other genre plays something โ€” notes, a progression, a bass line โ€” and some people can't read while that happens. `drone` holds one low tone under a slow-breathing noise bed and never moves: closer to a fan or rainfall than to music. Tiers add weight rather than movement. If you want the fan-and-rainfall idea literally, `rain` and `ocean` are synthesized the same way (seeded noise, no recorded files): rain adds more droplets as the turn runs longer, ocean adds a second swell and then foam.
611
645
  >
612
646
  > **`piano` is the gentler version of that idea.** It still plays notes โ€” two in eight seconds at tier 1 โ€” but they're single struck tones with silence between them and nothing running underneath. Higher tiers fill the gaps rather than adding a groove. Try it before `drone` if you want *something* there.
@@ -746,7 +780,9 @@ It shouldn't. The cache key includes a hash of the synth sources, so changing a
746
780
  vibe --clear-cache
747
781
  ```
748
782
 
749
- **Silent when the agent runs on another machine over SSH**
783
+ <details>
784
+ <summary><b>Silent when the agent runs on another machine over SSH (socket forwarding)</b></summary>
785
+
750
786
  Sound plays on the machine where the agent runs, and a remote server usually has no sound card โ€” so VibeAudio finds no player and stays quiet. You can hear it locally by forwarding a PulseAudio socket through the SSH connection: the remote `paplay` sends the audio back over SSH to your own speakers.
751
787
 
752
788
  This needs a PulseAudio-compatible sound server on **your local machine**: a Linux desktop (PipeWire and PulseAudio both provide one) or Windows with WSLg. macOS has none built in, so this route doesn't apply there.
@@ -768,6 +804,8 @@ This needs a PulseAudio-compatible sound server on **your local machine**: a Lin
768
804
 
769
805
  If the second connection fails with the socket "already in use", an earlier session left it behind: `rm /tmp/vibe-pulse.sock` on the server, or set `StreamLocalBindUnlink yes` in the server's `sshd_config`. If `paplay` says access denied, your local PulseAudio requires its cookie โ€” copy `~/.config/pulse/cookie` to the same path on the server.
770
806
 
807
+ </details>
808
+
771
809
  **Volume flag does nothing**
772
810
  Shouldn't happen any more โ€” where the player can't attenuate (`aplay`, PowerShell), the gain is baked into the audio instead. A `--volume` change reaches music already playing within a second on macOS hooks (it eases there), at the next loop boundary elsewhere, and in the wrapper or MCP on their next run; `vibe --status` shows the volume in effect and what set it.
773
811
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeaudio",
3
- "version": "0.14.0",
3
+ "version": "0.14.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",
@@ -10,8 +10,7 @@
10
10
  "scripts": {
11
11
  "test": "node test/test-synth.js",
12
12
  "start": "node bin/vibeaudio.js",
13
- "version": "node -e \"const fs=require('fs'),v=require('./package.json').version;for(const f of ['.claude-plugin/plugin.json','.codex-plugin/plugin.json']){const j=JSON.parse(fs.readFileSync(f,'utf8'));j.version=v;fs.writeFileSync(f,JSON.stringify(j,null,2)+'\\n')}\" && git add .claude-plugin/plugin.json .codex-plugin/plugin.json",
14
- "postinstall": "node scripts/postinstall.js"
13
+ "version": "node -e \"const fs=require('fs'),v=require('./package.json').version;for(const f of ['.claude-plugin/plugin.json','.codex-plugin/plugin.json']){const j=JSON.parse(fs.readFileSync(f,'utf8'));j.version=v;fs.writeFileSync(f,JSON.stringify(j,null,2)+'\\n')}\" && git add .claude-plugin/plugin.json .codex-plugin/plugin.json"
15
14
  },
16
15
  "publishConfig": {
17
16
  "registry": "https://registry.npmjs.org/",
@@ -23,7 +22,6 @@
23
22
  "files": [
24
23
  "bin/",
25
24
  "src/",
26
- "scripts/postinstall.js",
27
25
  "README.md",
28
26
  "LICENSE"
29
27
  ],
package/src/hooks.js CHANGED
@@ -889,6 +889,12 @@ function runDaemon(genre, volume, { reactive = false, volumeSource = null } = {}
889
889
  setTimeout(shutdown, MAX_DAEMON_MS);
890
890
 
891
891
  setInterval(() => {
892
+ // The pid file names the one daemon that owns the speakers. Two prompts
893
+ // at once can each find none running and spawn one; the last to write the
894
+ // file wins, and the other must not play over it (#42).
895
+ // ponytail: up to one poll of doubled audio; claim the pid file with an
896
+ // exclusive create before spawning if that ever matters.
897
+ if (readPid() !== process.pid) return shutdown();
892
898
  // A volume saved while this plays reaches it now, not at the next prompt.
893
899
  if (volumeSource) player.setVolume(volumeSource());
894
900
  if (sweep()) return;
@@ -1,50 +0,0 @@
1
- /**
2
- * The one line an install owes the user.
3
- *
4
- * `npm i -g vibeaudio` used to end in silence: nothing plays until hooks are
5
- * installed or a command is wrapped, so someone who installed it and then went
6
- * back to work heard nothing and concluded it was broken. This says what to
7
- * run next, once, at the only moment the user is definitely looking.
8
- *
9
- * It must never fail an install. Anything thrown here is npm's problem to
10
- * report and the user's to wonder about, so everything is wrapped and the exit
11
- * code is always 0 - a greeting is not worth a failed install.
12
- */
13
- // npm 7+ hides lifecycle-script stdout unless the script fails - measured on
14
- // npm 10, a global install printed only "added 1 package". The controlling
15
- // terminal is outside npm's pipe, so write there; stdout remains for when
16
- // there is no terminal (CI, Windows), where npm may or may not show it.
17
- function say(text) {
18
- const fs = require("fs");
19
- try {
20
- const fd = fs.openSync("/dev/tty", "w");
21
- fs.writeSync(fd, text + "\n");
22
- fs.closeSync(fd);
23
- } catch (e) {
24
- console.log(text);
25
- }
26
- }
27
-
28
- try {
29
- // Local installs are a dependency of something else; their user is not here
30
- // and not the one who would run `vibe`. CI and Docker set this too.
31
- // No isTTY check: npm pipes lifecycle output often enough that gating on it
32
- // would mean the hint never lands for the people who need it. A global
33
- // install is already the narrow case - a dependency install sets this false.
34
- if (process.env.npm_config_global === "true") {
35
- const c = (code, text) => `\x1b[${code}m${text}\x1b[0m`;
36
- say(`
37
- ${c("1;36", "๐ŸŽง VibeAudio installed.")} Start here:
38
-
39
- ${c("1", "vibe")} ${c("90", "guided setup: pick your agent, genre and volume")}
40
-
41
- Or skip the menu:
42
-
43
- ${c("1", "vibe --install-hooks")} ${c("90", "music follows your agent's thinking โ€” Claude Code, Codex, Cursorโ€ฆ")}
44
- ${c("1", "vibe npm test")} ${c("90", "wrap any command that exits when it's done")}
45
- ${c("1", "vibe --preview jazz")} ${c("90", "hear a genre first")}
46
- `);
47
- }
48
- } catch (e) {
49
- // Deliberately silent: see above.
50
- }