vibeaudio 0.14.1 โ 0.15.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 +66 -26
- package/package.json +2 -4
- package/src/cli.js +6 -6
- package/src/hooks.js +106 -32
- package/src/interactive.js +1 -0
- package/src/mcp.js +2 -2
- package/scripts/postinstall.js +0 -50
package/README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
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
|
[](https://github.com/kiril6/vibeaudio/actions/workflows/test.yml) [](https://www.npmjs.com/package/vibeaudio) [](https://npm-stat.com/charts.html?package=vibeaudio) [](https://npm-stat.com/charts.html?package=vibeaudio)
|
|
4
8
|
|
|
5
9
|
> **Procedural focus music while your AI coding tools think.**
|
|
6
10
|
> Every project gets its own arrangement. Zero dependencies, zero audio files.
|
|
7
|
-
> Works with Claude Code, Codex, Cursor, Grok, Gemini CLI, Copilot CLI, Qwen Code, Windsurf, Aider โ and any terminal command.
|
|
11
|
+
> Works with Claude Code, Codex, Cursor, Grok, Gemini CLI, Copilot CLI, Qwen Code, Windsurf, Antigravity, Aider โ and any terminal command.
|
|
8
12
|
|
|
9
13
|
**[Install](#-install)** ยท **[Agent hooks](#-agent-hooks-no-wrapper-needed)** ยท **[Genres](#-music-genres)** ยท **[Flags](#-options--flags)** ยท **[Troubleshooting](#-troubleshooting)** ยท **[Uninstall](#-uninstall)**
|
|
10
14
|
|
|
@@ -31,7 +35,7 @@ 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`.
|
|
35
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.
|
|
36
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.
|
|
37
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.
|
|
@@ -41,7 +45,7 @@ It is built as **calm technology**, in the sense of Mark Weiser and John Seely B
|
|
|
41
45
|
* ๐ **Outcome-aware chimes.** Ascending on success, a soft descending minor chord on failure, and **silence on `Ctrl+C`** โ an abort is never reported as done.
|
|
42
46
|
* ๐ **A heartbeat when an agent looks stuck.** If 4 of a session's last 8 tool calls fail โ the test-edit-test loop that goes nowhere โ a soft pulse on the music's tonic joins the music until things start passing again. It's rare by design: on 4,497 real turns it fired in under 1%.
|
|
43
47
|
* โ **A "your turn" chime.** When Claude Code stops to ask permission (or an MCP server asks for input), the music pauses and a rising two-note chime asks for you; it picks back up once you've answered.
|
|
44
|
-
* ๐ **Universal drop-in.** Hooks for **Claude Code, Codex, Cursor, Grok, Gemini CLI, Copilot CLI, Qwen Code and
|
|
48
|
+
* ๐ **Universal drop-in.** Hooks for **Claude Code, Codex, Cursor, Grok, Gemini CLI, Copilot CLI, Qwen Code, Windsurf and Antigravity**; MCP for **Claude Desktop**; the wrapper (`vibe <command>`) for anything else.
|
|
45
49
|
|
|
46
50
|
<details>
|
|
47
51
|
<summary><b>And the quieter four</b> โ silence on fast commands, several sessions at once, exit codes, the HUD</summary>
|
|
@@ -49,7 +53,7 @@ It is built as **calm technology**, in the sense of Mark Weiser and John Seely B
|
|
|
49
53
|
* ๐ก๏ธ **A grace window.** Fast commands stay 100% silent โ music starts only past 1.5s (`--grace`).
|
|
50
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.
|
|
51
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.
|
|
52
|
-
* ๐ **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.
|
|
53
57
|
|
|
54
58
|
</details>
|
|
55
59
|
|
|
@@ -237,7 +241,7 @@ Adding an entry to the launcher is one line in [`src/interactive.js`](src/intera
|
|
|
237
241
|
|
|
238
242
|
## ๐ช Agent Hooks (no wrapper needed)
|
|
239
243
|
|
|
240
|
-
> **`--install-hooks` supports Claude Code, Codex, Cursor, Grok, Gemini CLI, GitHub Copilot CLI, Qwen Code and
|
|
244
|
+
> **`--install-hooks` supports Claude Code, Codex, Cursor, Grok, Gemini CLI, GitHub Copilot CLI, Qwen Code, Windsurf and Antigravity.** Everything else uses the [wrapper or MCP](#-everything-else-claude-desktop-vs-code-via-mcp) instead.
|
|
241
245
|
|
|
242
246
|
Wrapping (`vibe claude`) infers "the AI is thinking" from how long the process runs. Hooks know for certain โ so music starts the moment you submit a prompt and stops the moment the agent finishes, with no grace-window guessing and no aliases.
|
|
243
247
|
|
|
@@ -250,12 +254,12 @@ vibe --genre jazz --volume 25 --install-hooks # install, and save these as yo
|
|
|
250
254
|
vibe --install-hooks --dry-run # show what would change, write nothing
|
|
251
255
|
```
|
|
252
256
|
|
|
253
|
-
**It auto-detects.** With no `--tools`, VibeAudio wires up each supported agent it finds on your machine โ one counts as present when its config directory exists or its CLI is on your `PATH`. `--tools claude,codex,cursor,grok,gemini,copilot,qwen,windsurf` overrides that. Add `--dry-run` to see, per event, what would be added or changed in each file before anything is written.
|
|
257
|
+
**It auto-detects.** With no `--tools`, VibeAudio wires up each supported agent it finds on your machine โ one counts as present when its config directory exists or its CLI is on your `PATH`. `--tools claude,codex,cursor,grok,gemini,copilot,qwen,windsurf,antigravity` overrides that. Add `--dry-run` to see, per event, what would be added or changed in each file before anything is written.
|
|
254
258
|
|
|
255
259
|
That's it โ run your agent normally, with no `vibe` prefix.
|
|
256
260
|
|
|
257
261
|
<details>
|
|
258
|
-
<summary><b>Which file and which events, per agent</b> โ
|
|
262
|
+
<summary><b>Which file and which events, per agent</b> โ nine dialects, all written for you</summary>
|
|
259
263
|
|
|
260
264
|
Each tool spells its events its own way, and VibeAudio writes whichever dialect the file expects:
|
|
261
265
|
|
|
@@ -269,6 +273,7 @@ Each tool spells its events its own way, and VibeAudio writes whichever dialect
|
|
|
269
273
|
| **Copilot CLI** | `~/.copilot/hooks/vibeaudio.json` | `UserPromptSubmit` | `Stop` | `PreToolUse` |
|
|
270
274
|
| **Qwen Code** | `~/.qwen/settings.json` | `UserPromptSubmit` | `Stop` | `PreToolUse` |
|
|
271
275
|
| **Windsurf** | `~/.codeium/windsurf/hooks.json` | `pre_user_prompt` | `post_cascade_response` | `pre_run_command` |
|
|
276
|
+
| **Antigravity** | `~/.gemini/config/hooks.json`, under its own `vibeaudio` name | `PreInvocation` (the first of each prompt) | `Stop` | โ never ([why](#antigravity-hooks)) |
|
|
272
277
|
|
|
273
278
|
Where an agent reports more than start and stop, VibeAudio listens for that too โ and only where the event was confirmed against the tool itself:
|
|
274
279
|
|
|
@@ -279,12 +284,13 @@ Where an agent reports more than start and stop, VibeAudio listens for that too
|
|
|
279
284
|
| **Gemini CLI** | `Notification` (tool permission) | `AfterTool` | โ | `SessionEnd` |
|
|
280
285
|
| **Copilot CLI** | `Notification` (permission prompt) | `PostToolUse`, `PostToolUseFailure` | โ | `SessionEnd` |
|
|
281
286
|
| **Qwen Code** | `PermissionRequest` | `PostToolUse`, `PostToolUseFailure` | `StopFailure` | `SessionEnd` |
|
|
287
|
+
| **Antigravity** | โ | โ | `Stop` ending in an error or the step limit | โ |
|
|
282
288
|
| **Cursor, Grok, Windsurf** | โ | โ | โ | โ |
|
|
283
289
|
|
|
284
290
|
</details>
|
|
285
291
|
|
|
286
292
|
<details>
|
|
287
|
-
<summary><b>
|
|
293
|
+
<summary><b>Seven things that differ per agent</b> โ only the first one needs anything from you</summary>
|
|
288
294
|
|
|
289
295
|
|
|
290
296
|
| | |
|
|
@@ -292,7 +298,8 @@ Where an agent reports more than start and stop, VibeAudio listens for that too
|
|
|
292
298
|
| **Five agents tell you when they're waiting on you** | When a permission dialog opens the music stops rather than sounding busy while the agent is stuck on you, and it resumes once the thing you answered has run. Claude Code covers the terminal, desktop app and IDEs alike, plus MCP servers asking for input. Copilot CLI's own `PermissionRequest` fires before *every* permission check โ dialog or not โ so VibeAudio listens for its permission-prompt notification instead. Cursor and Grok have no such event that's been verified, so they keep playing through a prompt. |
|
|
293
299
|
| **Claude Code turns that never reach `Stop` still end the music** | An API error or rate limit ends the turn with `StopFailure` instead, which plays the failure chime. Interrupting (Esc, or the stop button in the desktop app) fires no hook at all, so the background player watches the session transcript for Claude Code's interrupt entry and stops silently within half a second โ whether the agent was writing or running a tool. A prompt that another hook blocks (a token-saving proxy, say) never reaches `Stop` either, so the same watcher ends the music on Claude Code's "prompt blocked" entry โ or never starts it, when the blocker was quicker than we were. Closing the session mid-turn stops it too โ but only if that session started the music, so closing an idle terminal never silences another one. |
|
|
294
300
|
| **Codex asks you to trust the hook once** | Codex keeps a per-hook trust hash in `~/.codex/config.toml` and won't run a hook it hasn't been told to trust, so the install isn't live until you approve each one the first time it fires. |
|
|
295
|
-
| **
|
|
301
|
+
| **Cursor and Antigravity say how a turn ended** | Cursor's stop event reports whether the turn completed, aborted or errored, and Antigravity's gives a termination reason, so an error plays the failure chime. Claude Code and Qwen Code send a separate `StopFailure` for an API error. The others send no verdict, so a turn there always ends on the success chime โ VibeAudio won't invent a failure the agent never claimed. |
|
|
302
|
+
| <a id="antigravity-hooks"></a>**Antigravity never gets a pre-tool hook** | Its pre-tool hook has to approve or deny every tool call โ one that answers nothing blocks the tool โ so VibeAudio installs none, and `--reactive` does nothing there. It has no prompt event either: the start event fires before every model call, and VibeAudio starts the music on the first one of each prompt only. Its hooks live in `~/.gemini/config/hooks.json` under a name of their own, so your other named hooks there are never touched. Verified with the `agy` CLI; the desktop app reads the same file by Antigravity's own docs but hasn't been checked live. |
|
|
296
303
|
| **Upgraded, and a new feature isn't there?** | Hooks are written into your agent's config once, at install time, and an upgrade doesn't touch them. When a new version listens for more events (0.13.1 added Codex's `Interrupt` and `SessionEnd`), `vibe --doctor` and `vibe --status` name what's missing, and `vibe --install-hooks` adds it. |
|
|
297
304
|
| **Moved your config folder?** | VibeAudio follows `CLAUDE_CONFIG_DIR` and `CODEX_HOME` the way the agents do, so hooks (and `/vibe`) go where your agent actually reads them. Set the variable in the shell you run `vibe --install-hooks` from. |
|
|
298
305
|
| **Grok and Copilot CLI get a file of their own** | Each reads every `*.json` in its `hooks/` directory, so VibeAudio writes `vibeaudio.json` rather than merging into anyone else's โ which makes uninstalling it a delete, and leaves no backup file behind. Copilot's honours `COPILOT_HOME`. |
|
|
@@ -319,9 +326,9 @@ Sessions with no id in their payload share a single slot, so they behave as one.
|
|
|
319
326
|
|
|
320
327
|
</details>
|
|
321
328
|
|
|
322
|
-
> **No restart needed, even mid-session โ for Claude Code, Codex, Cursor and Grok.** Each re-reads its hook file every time a hook fires, so changes land on your **next prompt**. A daemon already playing keeps its old settings until that prompt replaces it โ at most the tail of one turn. Whether an open Gemini CLI, Copilot CLI, Qwen Code or
|
|
329
|
+
> **No restart needed, even mid-session โ for Claude Code, Codex, Cursor and Grok.** Each re-reads its hook file every time a hook fires, so changes land on your **next prompt**. A daemon already playing keeps its old settings until that prompt replaces it โ at most the tail of one turn. Whether an open Gemini CLI, Copilot CLI, Qwen Code, Windsurf or Antigravity session does the same hasn't been checked, so start a new session there to be sure.
|
|
323
330
|
|
|
324
|
-
> **The Claude Code desktop app is covered too**, not just the terminal โ both read the same `~/.claude/settings.json` (or `$CLAUDE_CONFIG_DIR/settings.json`). (The separate **Claude Desktop** chat app is a different product with no hooks; that one needs [MCP](#-everything-else-claude-desktop-
|
|
331
|
+
> **The Claude Code desktop app is covered too**, not just the terminal โ both read the same `~/.claude/settings.json` (or `$CLAUDE_CONFIG_DIR/settings.json`). (The separate **Claude Desktop** chat app is a different product with no hooks; that one needs [MCP](#-everything-else-claude-desktop-vs-code-via-mcp).)
|
|
325
332
|
|
|
326
333
|
> **One player is shared.** Prompt two agents at once and the last prompt owns the music. One person, one set of speakers โ deliberate, not a limitation being worked around.
|
|
327
334
|
|
|
@@ -352,11 +359,16 @@ Codex asks you to approve the plugin's hooks once, as it does for any hook. Upda
|
|
|
352
359
|
|
|
353
360
|
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).
|
|
354
361
|
|
|
362
|
+
<details>
|
|
363
|
+
<summary><b>Plugin details & coexistence with npm</b> โ settings file, coexistence, and limitations</summary>
|
|
364
|
+
|
|
355
365
|
- **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.
|
|
356
366
|
- **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.
|
|
357
367
|
- **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.
|
|
358
368
|
- **Not reactive.** `--reactive` is an `--install-hooks` option; the plugin doesn't carry it.
|
|
359
369
|
|
|
370
|
+
</details>
|
|
371
|
+
|
|
360
372
|
### `/vibe` inside Claude Code
|
|
361
373
|
|
|
362
374
|
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:
|
|
@@ -419,8 +431,13 @@ Hooks log each finished turn โ which project, how long, how it ended, and how
|
|
|
419
431
|
32s with a chime 1m 50s without (muted or --no-chime)
|
|
420
432
|
```
|
|
421
433
|
|
|
434
|
+
<details>
|
|
435
|
+
<summary><b>How the report metrics & return times are calculated</b></summary>
|
|
436
|
+
|
|
422
437
|
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.
|
|
423
438
|
|
|
439
|
+
</details>
|
|
440
|
+
|
|
424
441
|
### The chime is in the music's key
|
|
425
442
|
|
|
426
443
|
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.
|
|
@@ -429,9 +446,14 @@ The success chime is the tonic chord of whatever key your genre sits in โ C ma
|
|
|
429
446
|
|
|
430
447
|
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.
|
|
431
448
|
|
|
449
|
+
<details>
|
|
450
|
+
<summary><b>How the stuck threshold was calibrated (4,497 turns analyzed)</b></summary>
|
|
451
|
+
|
|
432
452
|
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.
|
|
433
453
|
|
|
434
|
-
It needs the agent to report failed tool calls, which **Claude Code, Copilot CLI and Qwen Code** do (`PostToolUseFailure`). Codex, Cursor, Gemini, Grok and
|
|
454
|
+
It needs the agent to report failed tool calls, which **Claude Code, Copilot CLI and Qwen Code** do (`PostToolUseFailure`). Codex, Cursor, Gemini, Grok, Windsurf and Antigravity don't (Antigravity reports a tool error, but not a failing command), so for them the music never adds the heartbeat.
|
|
455
|
+
|
|
456
|
+
</details>
|
|
435
457
|
|
|
436
458
|
### Reactive mode (opt-in)
|
|
437
459
|
|
|
@@ -468,7 +490,7 @@ Changes land at the next loop boundary, so it shifts musically rather than cutti
|
|
|
468
490
|
|
|
469
491
|
### Build on it: `vibe --state` and `vibe --events`
|
|
470
492
|
|
|
471
|
-
The hard part of VibeAudio isn't the music. It's turning
|
|
493
|
+
The hard part of VibeAudio isn't the music. It's turning nine agents' different hook events into one set of states that mean the same thing everywhere. The music is one consumer of those states, and anything else can be another: a smart light, a menu bar icon, a tmux status line, a Stream Deck key.
|
|
472
494
|
|
|
473
495
|
```bash
|
|
474
496
|
vibe --state
|
|
@@ -483,23 +505,34 @@ Each session is `working`, `stuck` (4 of its last 8 tool calls failed) or `waiti
|
|
|
483
505
|
{"v":1,"at":1791026130000,"event":"waiting","session":"โฆ","project":"/work/api","tool":"Bash","status":"waiting"}
|
|
484
506
|
```
|
|
485
507
|
|
|
486
|
-
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.
|
|
508
|
+
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.
|
|
487
509
|
|
|
510
|
+
#### ๐ Status Bar & Desktop Integrations
|
|
511
|
+
|
|
512
|
+
**tmux status bar** โ show overall agent state (`waiting`, `stuck`, `working`, or `idle`):
|
|
488
513
|
```bash
|
|
489
|
-
# tmux: show the state in the status bar
|
|
490
514
|
set -g status-right '#(vibe --state | jq -r .status)'
|
|
515
|
+
```
|
|
491
516
|
|
|
492
|
-
|
|
517
|
+
**macOS speech alert** โ announce out loud whenever an agent stops to ask for your input:
|
|
518
|
+
```bash
|
|
493
519
|
vibe --events | jq --unbuffered -r 'select(.event=="waiting") | .project' | while read p; do say "$(basename "$p") needs you"; done
|
|
494
520
|
```
|
|
495
521
|
|
|
522
|
+
**Starship prompt** โ custom indicator in `~/.config/starship.toml`:
|
|
523
|
+
```toml
|
|
524
|
+
[custom.vibe]
|
|
525
|
+
command = "vibe --state | jq -r 'if .status != \"idle\" then \"๐ง \" + .status else \"\" end'"
|
|
526
|
+
when = "command -v vibe >/dev/null"
|
|
527
|
+
```
|
|
528
|
+
|
|
496
529
|
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.
|
|
497
530
|
|
|
498
|
-
## ๐ฅ๏ธ Everything Else (Claude Desktop,
|
|
531
|
+
## ๐ฅ๏ธ Everything Else (Claude Desktop, VS Codeโฆ via MCP)
|
|
499
532
|
|
|
500
|
-
**Claude Code, [Codex](https://github.com/openai/codex), [Cursor](https://cursor.com/docs/hooks), [Grok](https://docs.x.ai/build/features/hooks), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Copilot CLI](https://docs.github.com/en/copilot/reference/hooks-configuration), [Qwen Code](https://github.com/QwenLM/qwen-code)
|
|
533
|
+
**Claude Code, [Codex](https://github.com/openai/codex), [Cursor](https://cursor.com/docs/hooks), [Grok](https://docs.x.ai/build/features/hooks), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Copilot CLI](https://docs.github.com/en/copilot/reference/hooks-configuration), [Qwen Code](https://github.com/QwenLM/qwen-code) [Windsurf](https://docs.devin.ai/desktop/cascade/hooks) and [Antigravity](https://antigravity.google/docs/hooks) have hook systems, and `--install-hooks` writes to all nine** โ use [hooks](#-agent-hooks-no-wrapper-needed) there, they're strictly better. This section is for everything else. MCP is the way in: it's plain stdio JSON-RPC, so the setup is identical everywhere and only the config file differs.
|
|
501
534
|
|
|
502
|
-
> **This is weaker than hooks, by nature.** Hooks fire on an event; MCP tools are *model-invoked*, so the assistant has to decide to call `vibe_play` and remember `vibe_stop`. Expect the occasional silent turn โ say "play some focus music while you work on this" if you want it reliably. **On any of the
|
|
535
|
+
> **This is weaker than hooks, by nature.** Hooks fire on an event; MCP tools are *model-invoked*, so the assistant has to decide to call `vibe_play` and remember `vibe_stop`. Expect the occasional silent turn โ say "play some focus music while you work on this" if you want it reliably. **On any of the nine agents above, use [hooks](#-agent-hooks-no-wrapper-needed) instead.**
|
|
503
536
|
|
|
504
537
|
Use an **absolute path**, not the bare `vibe` command: GUI apps launched from Finder don't inherit your shell's `PATH`, and version managers like `fnm` or `nvm` put `vibe` on a per-shell path that won't resolve. Print yours with:
|
|
505
538
|
|
|
@@ -525,7 +558,7 @@ Where the file lives:
|
|
|
525
558
|
| Tool | Config file |
|
|
526
559
|
| :--- | :--- |
|
|
527
560
|
| **Claude Desktop** (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
528
|
-
| **
|
|
561
|
+
| **VS Code, Zed, โฆ** | that app's own MCP settings โ same JSON shape |
|
|
529
562
|
|
|
530
563
|
Restart the app afterwards.
|
|
531
564
|
|
|
@@ -575,7 +608,7 @@ Playback stops automatically if the desktop client disconnects, and caps out aft
|
|
|
575
608
|
|
|
576
609
|
## ๐จ Music Genres
|
|
577
610
|
|
|
578
|
-
VibeAudio includes **
|
|
611
|
+
VibeAudio includes **10 procedural sound styles** synthesized entirely in code:
|
|
579
612
|
|
|
580
613
|
| Genre | Style | Vibe |
|
|
581
614
|
| :--- | :--- | :--- |
|
|
@@ -591,7 +624,8 @@ VibeAudio includes **8 procedural music styles** synthesized entirely in code:
|
|
|
591
624
|
| `ocean` | ๐ **Ocean** | Low surf on a slow swell that rises and drains โ **no melody at all** |
|
|
592
625
|
| `random` | ๐ฒ **Shuffle Mode** | Picks a surprise genre for the run โ **never `drone`, `rain` or `ocean`** |
|
|
593
626
|
|
|
594
|
-
|
|
627
|
+
<details>
|
|
628
|
+
<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>
|
|
595
629
|
|
|
596
630
|
| Canonical | Also accepted |
|
|
597
631
|
| :--- | :--- |
|
|
@@ -608,6 +642,8 @@ VibeAudio includes **8 procedural music styles** synthesized entirely in code:
|
|
|
608
642
|
|
|
609
643
|
Matching is case-insensitive, so `BOSSA` works too.
|
|
610
644
|
|
|
645
|
+
</details>
|
|
646
|
+
|
|
611
647
|
> **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.
|
|
612
648
|
>
|
|
613
649
|
> **`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.
|
|
@@ -681,7 +717,7 @@ The full order, highest first: **a flag** โ **an environment variable** โ **
|
|
|
681
717
|
| `--clear-cache` | Delete all cached audio, then exit | โ |
|
|
682
718
|
| `--mcp` | Run as an MCP stdio server for desktop apps | โ |
|
|
683
719
|
| `--install-hooks` | Wire music into your agent's hooks (no wrapper needed) | โ |
|
|
684
|
-
| `--tools <list>` | With `--install-hooks`: `claude,codex,cursor,grok,gemini,copilot,qwen,windsurf` | auto-detect |
|
|
720
|
+
| `--tools <list>` | With `--install-hooks`: `claude,codex,cursor,grok,gemini,copilot,qwen,windsurf,antigravity` | auto-detect |
|
|
685
721
|
| `--reactive` | With `--install-hooks`: intensity follows the tool in use | off |
|
|
686
722
|
| `--dry-run` | With `--install-hooks`: show what would change in each file, write nothing | off |
|
|
687
723
|
| `--uninstall-hooks` | Remove the hooks again, from every agent | โ |
|
|
@@ -747,7 +783,9 @@ It shouldn't. The cache key includes a hash of the synth sources, so changing a
|
|
|
747
783
|
vibe --clear-cache
|
|
748
784
|
```
|
|
749
785
|
|
|
750
|
-
|
|
786
|
+
<details>
|
|
787
|
+
<summary><b>Silent when the agent runs on another machine over SSH (socket forwarding)</b></summary>
|
|
788
|
+
|
|
751
789
|
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.
|
|
752
790
|
|
|
753
791
|
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.
|
|
@@ -769,6 +807,8 @@ This needs a PulseAudio-compatible sound server on **your local machine**: a Lin
|
|
|
769
807
|
|
|
770
808
|
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.
|
|
771
809
|
|
|
810
|
+
</details>
|
|
811
|
+
|
|
772
812
|
**Volume flag does nothing**
|
|
773
813
|
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.
|
|
774
814
|
|
|
@@ -837,8 +877,8 @@ Step 3 reclaims disk โ the audio cache, pruned to the 3 most recent projects
|
|
|
837
877
|
|
|
838
878
|
| Leftover | Why, and how to remove it |
|
|
839
879
|
| :--- | :--- |
|
|
840
|
-
| `*.vibeaudio.bak` next to each shared hook config | Your config as it was before the first install โ one each for Claude Code, Codex, Cursor, Gemini CLI, Qwen Code and
|
|
841
|
-
| `vibeaudio` entries in other apps' MCP configs | VibeAudio never edits those files, so it can't clean them either. Drop the entry from [whichever config you added it to](#-everything-else-claude-desktop-
|
|
880
|
+
| `*.vibeaudio.bak` next to each shared hook config | Your config as it was before the first install โ one each for Claude Code, Codex, Cursor, Gemini CLI, Qwen Code, Windsurf and Antigravity (if you had a `hooks.json` there). A safety net we won't delete for you; `rm` them once you're happy the real files are correct, and `vibe --uninstall-hooks` prints the path of every one it finds. (Grok and Copilot CLI leave nothing: their files are ours alone, so uninstall deletes them outright.) |
|
|
881
|
+
| `vibeaudio` entries in other apps' MCP configs | VibeAudio never edits those files, so it can't clean them either. Drop the entry from [whichever config you added it to](#-everything-else-claude-desktop-vs-code-via-mcp). |
|
|
842
882
|
|
|
843
883
|
Apart from those two, the three commands above remove everything VibeAudio writes.
|
|
844
884
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeaudio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -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/cli.js
CHANGED
|
@@ -87,7 +87,7 @@ Procedural focus music while your AI coding tools think.
|
|
|
87
87
|
--clear-cache Delete cached audio, then exit
|
|
88
88
|
--mcp Run as Model Context Protocol (MCP) server for Desktop apps
|
|
89
89
|
--install-hooks Wire music into your agent's hooks (no wrapper needed)
|
|
90
|
-
--tools <list> With --install-hooks: claude,codex,cursor,grok,gemini,copilot,qwen,windsurf (auto-detect)
|
|
90
|
+
--tools <list> With --install-hooks: claude,codex,cursor,grok,gemini,copilot,qwen,windsurf,antigravity (auto-detect)
|
|
91
91
|
--reactive With --install-hooks: intensity follows the tool in use
|
|
92
92
|
--dry-run With --install-hooks: show what would change, write nothing
|
|
93
93
|
--uninstall-hooks Remove the hooks again, from every agent
|
|
@@ -604,7 +604,7 @@ function installHookTargets(ids, genre, volume, reactive, dryRun = false, typed
|
|
|
604
604
|
}
|
|
605
605
|
console.log(` ${events.start.padEnd(19)}โ music starts (${genre} @ ${Math.round(volume * 100)}%)`);
|
|
606
606
|
console.log(` ${events.stop.padEnd(19)}โ music stops + success chime`);
|
|
607
|
-
if (reactive) {
|
|
607
|
+
if (result.reactive) { // off for a target with no pre-tool event
|
|
608
608
|
console.log(` ${events.tool.padEnd(19)}โ intensity follows the tool in use (reactive mode)`);
|
|
609
609
|
}
|
|
610
610
|
if (events.wait) console.log(` ${events.wait[0].padEnd(19)}โ music pauses + "your turn" chime`);
|
|
@@ -648,7 +648,7 @@ function printHookPlan({ file, backup, name, before, after, id }, t) {
|
|
|
648
648
|
const ours = (text) => {
|
|
649
649
|
const map = new Map();
|
|
650
650
|
if (!text) return map;
|
|
651
|
-
for (const [event, entries] of hooks.hookEntries(JSON.parse(text)
|
|
651
|
+
for (const [event, entries] of hooks.hookEntries(hooks.hooksOf(JSON.parse(text), t), t)) {
|
|
652
652
|
const mine = entries.filter((e) => hooks.isVibeHook(e, id));
|
|
653
653
|
if (mine.length) map.set(event, JSON.stringify(mine));
|
|
654
654
|
}
|
|
@@ -1118,8 +1118,8 @@ function readVibeHooks(file, t = null) {
|
|
|
1118
1118
|
try {
|
|
1119
1119
|
const settings = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1120
1120
|
const out = [];
|
|
1121
|
-
for (const [event, entries] of Object.entries(settings
|
|
1122
|
-
for (const entry of entries
|
|
1121
|
+
for (const [event, entries] of Object.entries(require("./hooks").hooksOf(settings, t) || {})) {
|
|
1122
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
1123
1123
|
for (const command of commands(entry)) {
|
|
1124
1124
|
if (VIBE_HOOK_FLAG.test(command || "")) out.push({ event, command });
|
|
1125
1125
|
}
|
|
@@ -1430,7 +1430,7 @@ function hooksAlreadyCover(cmdArgs, settingsFile = null) {
|
|
|
1430
1430
|
if (!fs.existsSync(file)) return false;
|
|
1431
1431
|
|
|
1432
1432
|
const settings = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1433
|
-
return Object.values(settings.
|
|
1433
|
+
return Object.values(hooks.hooksOf(settings, hooks.TARGETS[id]) || {}).some((entries) =>
|
|
1434
1434
|
// Not `.some(hooks.isVibeHook)` - Array.some would pass the index as the
|
|
1435
1435
|
// target id and every lookup would throw.
|
|
1436
1436
|
(entries || []).some((entry) => hooks.isVibeHook(entry, id))
|
package/src/hooks.js
CHANGED
|
@@ -106,9 +106,16 @@ function payloadToolName(raw) {
|
|
|
106
106
|
return String(payload.tool_name || payload.toolName || byEvent);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// Windsurf calls the conversation `trajectory_id`.
|
|
109
|
+
// Windsurf calls the conversation `trajectory_id`; Antigravity, `conversationId`.
|
|
110
110
|
function payloadSession(payload) {
|
|
111
|
-
return payload.session_id || payload.trajectory_id;
|
|
111
|
+
return payload.session_id || payload.trajectory_id || payload.conversationId;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The project a hook fired in: `cwd` for most agents, Antigravity's first workspace. */
|
|
115
|
+
function payloadProject(payload) {
|
|
116
|
+
if (typeof payload.cwd === "string" && payload.cwd) return payload.cwd;
|
|
117
|
+
const ws = Array.isArray(payload.workspacePaths) ? payload.workspacePaths[0] : null;
|
|
118
|
+
return typeof ws === "string" && ws ? ws : null;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
/**
|
|
@@ -346,6 +353,36 @@ const TARGETS = {
|
|
|
346
353
|
seed: () => ({}),
|
|
347
354
|
liveReload: false,
|
|
348
355
|
note: "Windsurf's hooks do not load in Restricted Mode, and whether an open session reloads them is not documented โ start a new one to be sure."
|
|
356
|
+
},
|
|
357
|
+
antigravity: {
|
|
358
|
+
name: "Antigravity",
|
|
359
|
+
cmd: "agy",
|
|
360
|
+
// Verified live with the agy CLI 1.1.25 (a logging hook in this file, two
|
|
361
|
+
// turns): ~/.gemini/config/ is Antigravity's global customization root,
|
|
362
|
+
// and its hooks.json holds *named* hooks - {"<name>": {"<Event>": [...]}} -
|
|
363
|
+
// so ours live under a key of their own (hooksKey) and nobody else's are
|
|
364
|
+
// touched. Handlers are flat for these two events; timeout in seconds.
|
|
365
|
+
// - start: there is no prompt event. PreInvocation fires before every model
|
|
366
|
+
// call with invocationNum counting from 0 per turn; newTurn() marks the
|
|
367
|
+
// later ones `continues` and hookStart ignores them.
|
|
368
|
+
// - stop: Stop, with terminationReason (outcomeFromPayload).
|
|
369
|
+
// - no tool slot, deliberately: PreToolUse must answer with a decision,
|
|
370
|
+
// and a hook that printed none was seen to DENY the tool, while "allow"
|
|
371
|
+
// would bypass the user's permission prompts. So --reactive cannot apply.
|
|
372
|
+
// - no wait, failure, resume or end: there is no permission or session-end
|
|
373
|
+
// event, and PostToolUse.error stayed empty for a failing command.
|
|
374
|
+
// Not verified: the desktop app reading this file (2.19.1 was running and
|
|
375
|
+
// fired nothing, but was idle), and what Esc fires.
|
|
376
|
+
file: () => path.join(os.homedir(), ".gemini", "config", "hooks.json"),
|
|
377
|
+
// ~/.gemini is Gemini CLI's too; this directory is Antigravity's alone.
|
|
378
|
+
configDir: () => path.join(os.homedir(), ".gemini", "antigravity"),
|
|
379
|
+
hooksKey: "vibeaudio",
|
|
380
|
+
events: { start: "PreInvocation", stop: "Stop" },
|
|
381
|
+
entry: (command) => ({ type: "command", command, timeout: 5 }),
|
|
382
|
+
commands: (entry) => (entry.hooks ? entry.hooks.map((h) => h.command) : entry.command ? [entry.command] : []),
|
|
383
|
+
seed: () => ({}),
|
|
384
|
+
liveReload: false,
|
|
385
|
+
note: "Not checked whether an open Antigravity session reloads hooks โ start a new one to be sure. Reactive mode does not apply: Antigravity's pre-tool hook must approve or deny every tool, so VibeAudio installs none."
|
|
349
386
|
}
|
|
350
387
|
};
|
|
351
388
|
|
|
@@ -580,7 +617,17 @@ function newTurn(raw) {
|
|
|
580
617
|
const payload = parsePayload(raw);
|
|
581
618
|
const transcript = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
582
619
|
const offset = transcript ? fileSize(transcript) : 0;
|
|
583
|
-
return {
|
|
620
|
+
return {
|
|
621
|
+
session: String(payloadSession(payload) || ""),
|
|
622
|
+
project: payloadProject(payload) || process.cwd(),
|
|
623
|
+
transcript,
|
|
624
|
+
offset,
|
|
625
|
+
blocked: blockedJustBefore(transcript, offset),
|
|
626
|
+
// Antigravity has no prompt event: its start is PreInvocation, which fires
|
|
627
|
+
// before every model call and counts them from 0 within the turn. Only the
|
|
628
|
+
// first one is a new turn; the rest would restart the music mid-turn.
|
|
629
|
+
continues: Number(payload.invocationNum) > 0
|
|
630
|
+
};
|
|
584
631
|
}
|
|
585
632
|
|
|
586
633
|
const BLOCK_LOOKBACK_MS = 3000;
|
|
@@ -928,6 +975,7 @@ function spawnDaemon(genre, volume, reactive, follow = false) {
|
|
|
928
975
|
*/
|
|
929
976
|
function hookStart(genre, volume, { reactive = false, turn = null, follow = false } = {}) {
|
|
930
977
|
if (turn && turn.blocked) return null; // Rejected before it began: nothing to play for.
|
|
978
|
+
if (turn && turn.continues) return null; // A later model call in a turn already playing.
|
|
931
979
|
const id = sessionId(turn && turn.session);
|
|
932
980
|
// Before the spawn: the daemon reads it on startup.
|
|
933
981
|
const project = (turn && turn.project) || process.cwd();
|
|
@@ -952,7 +1000,10 @@ function outcomeFromPayload(raw) {
|
|
|
952
1000
|
const payload = parsePayload(raw);
|
|
953
1001
|
if (payload.hook_event_name === "StopFailure") return "failure";
|
|
954
1002
|
const status = String(payload.status || "").toLowerCase();
|
|
955
|
-
|
|
1003
|
+
if (status === "error" || status === "aborted") return "failure";
|
|
1004
|
+
// Antigravity's Stop: NO_TOOL_CALL is the normal end (seen live; its docs
|
|
1005
|
+
// say model_stop). An error or the step limit is a turn that did not finish.
|
|
1006
|
+
return /error|max_steps/i.test(String(payload.terminationReason || "")) ? "failure" : "success";
|
|
956
1007
|
}
|
|
957
1008
|
|
|
958
1009
|
/**
|
|
@@ -998,9 +1049,9 @@ function notifyEnabled(env = process.env, config = loadConfig()) {
|
|
|
998
1049
|
return ["1", "true", "on", "yes"].includes(raw);
|
|
999
1050
|
}
|
|
1000
1051
|
|
|
1001
|
-
/** The project a hook fired in: the payload's
|
|
1052
|
+
/** The project a hook fired in: the payload's (see payloadProject), else ours. */
|
|
1002
1053
|
function sessionLabel(payload, cwd = process.cwd()) {
|
|
1003
|
-
const dir =
|
|
1054
|
+
const dir = payloadProject(payload) || cwd;
|
|
1004
1055
|
return path.basename(dir.replace(/[\\/]+$/, "")) || "a session";
|
|
1005
1056
|
}
|
|
1006
1057
|
|
|
@@ -1044,14 +1095,14 @@ function hookStop({ outcome = "success", volume = 0.4, chimeVolume = null, noChi
|
|
|
1044
1095
|
const session = readSession(id);
|
|
1045
1096
|
const tracked = session !== null;
|
|
1046
1097
|
fs.rmSync(sessionFile(id), { force: true });
|
|
1047
|
-
if (tracked) emitEvent("finished", id, parsePayload(raw)
|
|
1098
|
+
if (tracked) emitEvent("finished", id, payloadProject(parsePayload(raw)) || session.project, { outcome });
|
|
1048
1099
|
if (session && Number.isFinite(session.started)) {
|
|
1049
1100
|
const now = Date.now();
|
|
1050
1101
|
// A turn that ends while still paused for you has been blocked since then.
|
|
1051
1102
|
const blockedMs = (session.blockedMs || 0) + (session.waiting != null && session.waitStart ? now - session.waitStart : 0);
|
|
1052
1103
|
// ponytail: assumes a playback backend exists; a machine with none hears no music either.
|
|
1053
1104
|
const chimed = !noChime && !playbackDisabled();
|
|
1054
|
-
recordTurn({ project: parsePayload(raw)
|
|
1105
|
+
recordTurn({ project: payloadProject(parsePayload(raw)) || process.cwd(), ms: now - session.started, blockedMs, outcome, session: id, chimed, at: now });
|
|
1055
1106
|
}
|
|
1056
1107
|
|
|
1057
1108
|
// The music is every working session's, so it ends with the last of them.
|
|
@@ -1244,6 +1295,19 @@ function setHook(hooks, event, command, id) {
|
|
|
1244
1295
|
hooks[event] = kept;
|
|
1245
1296
|
}
|
|
1246
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* The object holding a target's events inside its config file: `hooks` for
|
|
1300
|
+
* every agent but Antigravity, whose file is a map of named hooks and keeps
|
|
1301
|
+
* ours under a name of their own (`hooksKey`).
|
|
1302
|
+
*/
|
|
1303
|
+
function hooksKey(t) {
|
|
1304
|
+
return (t && t.hooksKey) || "hooks";
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function hooksOf(settings, t) {
|
|
1308
|
+
return settings ? settings[hooksKey(t)] : undefined;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1247
1311
|
/** [event, entries] for every event in a hooks object, skipping settings keys. */
|
|
1248
1312
|
function hookEntries(hooks, t) {
|
|
1249
1313
|
const configKeys = (t && t.configKeys) || [];
|
|
@@ -1253,7 +1317,7 @@ function hookEntries(hooks, t) {
|
|
|
1253
1317
|
function readVibeEntryCount(file, id) {
|
|
1254
1318
|
try {
|
|
1255
1319
|
const { settings } = loadSettings(file, target(id));
|
|
1256
|
-
return hookEntries(settings
|
|
1320
|
+
return hookEntries(hooksOf(settings, target(id)), target(id)).reduce(
|
|
1257
1321
|
(n, [, entries]) => n + entries.filter((e) => isVibeHook(e, id)).length,
|
|
1258
1322
|
0
|
|
1259
1323
|
);
|
|
@@ -1285,14 +1349,15 @@ function loadSettings(file, t = null) {
|
|
|
1285
1349
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) {
|
|
1286
1350
|
throw new Error(`${file} does not contain a JSON object โ refusing to overwrite it.`);
|
|
1287
1351
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1352
|
+
const key = hooksKey(t);
|
|
1353
|
+
if (settings[key] !== undefined) {
|
|
1354
|
+
if (settings[key] === null || typeof settings[key] !== "object" || Array.isArray(settings[key])) {
|
|
1355
|
+
throw new Error(`${file} has a "${key}" key that is not an object โ refusing to overwrite it.`);
|
|
1291
1356
|
}
|
|
1292
|
-
for (const [event, entries] of hookEntries(settings
|
|
1357
|
+
for (const [event, entries] of hookEntries(settings[key], t)) {
|
|
1293
1358
|
if (!Array.isArray(entries)) {
|
|
1294
1359
|
throw new Error(
|
|
1295
|
-
`${file} has
|
|
1360
|
+
`${file} has ${key}.${event} as ${Array.isArray(entries) ? "an array" : typeof entries}, ` +
|
|
1296
1361
|
`not an array of entries โ refusing to overwrite it.`
|
|
1297
1362
|
);
|
|
1298
1363
|
}
|
|
@@ -1354,33 +1419,39 @@ function installHooks(genre = "lofi", volume = 0.4, file = null, { reactive = fa
|
|
|
1354
1419
|
}
|
|
1355
1420
|
|
|
1356
1421
|
const ev = t.events;
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1422
|
+
// Reactive needs a pre-tool event we can listen on without deciding for the
|
|
1423
|
+
// agent; where there is none (Antigravity) it is off, not carried as a flag
|
|
1424
|
+
// that does nothing and that --status would then report.
|
|
1425
|
+
reactive = reactive && Boolean(ev.tool);
|
|
1426
|
+
const key = hooksKey(t);
|
|
1427
|
+
settings[key] = settings[key] || {};
|
|
1428
|
+
const hooks = settings[key];
|
|
1429
|
+
setHook(hooks, ev.start, hookCommand("--hook-start", genre, volume, reactive), id);
|
|
1430
|
+
setHook(hooks, ev.stop, hookCommand("--hook-stop", genre, volume), id);
|
|
1360
1431
|
|
|
1361
1432
|
// Only reactive mode needs per-tool-call signalling.
|
|
1362
1433
|
if (reactive) {
|
|
1363
|
-
setHook(
|
|
1364
|
-
} else if (
|
|
1365
|
-
const kept =
|
|
1366
|
-
if (kept.length)
|
|
1367
|
-
else delete
|
|
1434
|
+
setHook(hooks, ev.tool, hookCommand("--hook-tool", genre, volume), id);
|
|
1435
|
+
} else if (ev.tool && hooks[ev.tool]) {
|
|
1436
|
+
const kept = hooks[ev.tool].filter((entry) => !isVibeHook(entry, id));
|
|
1437
|
+
if (kept.length) hooks[ev.tool] = kept;
|
|
1438
|
+
else delete hooks[ev.tool];
|
|
1368
1439
|
}
|
|
1369
1440
|
|
|
1370
1441
|
for (const event of ev.wait || []) {
|
|
1371
|
-
setHook(
|
|
1442
|
+
setHook(hooks, event, hookCommand("--hook-wait", genre, volume), id);
|
|
1372
1443
|
}
|
|
1373
1444
|
// The agent awaits a resume before the next tool's permission check, so a
|
|
1374
1445
|
// resume can never land after the next wait.
|
|
1375
1446
|
// ponytail: one ~40ms node start per tool call; a shell-side existence
|
|
1376
1447
|
// check on the waiting file would skip it if that ever shows.
|
|
1377
1448
|
for (const event of ev.resume || []) {
|
|
1378
|
-
setHook(
|
|
1449
|
+
setHook(hooks, event, hookCommand("--hook-resume", genre, volume, reactive), id);
|
|
1379
1450
|
}
|
|
1380
|
-
if (ev.failure) setHook(
|
|
1381
|
-
if (ev.end) setHook(
|
|
1451
|
+
if (ev.failure) setHook(hooks, ev.failure, hookCommand("--hook-stop", genre, volume), id);
|
|
1452
|
+
if (ev.end) setHook(hooks, ev.end, hookCommand("--hook-end", genre, volume), id);
|
|
1382
1453
|
// An interrupt ends the turn the same silent way a closed session does.
|
|
1383
|
-
if (ev.interrupt) setHook(
|
|
1454
|
+
if (ev.interrupt) setHook(hooks, ev.interrupt, hookCommand("--hook-end", genre, volume), id);
|
|
1384
1455
|
|
|
1385
1456
|
const after = `${JSON.stringify(settings, null, 2)}\n`;
|
|
1386
1457
|
if (!dryRun) fs.writeFileSync(file, after);
|
|
@@ -1457,18 +1528,20 @@ function uninstallHooks(file = null, { id = "claude" } = {}) {
|
|
|
1457
1528
|
}
|
|
1458
1529
|
|
|
1459
1530
|
const { settings } = loadSettings(file, t);
|
|
1460
|
-
|
|
1531
|
+
const key = hooksKey(t);
|
|
1532
|
+
const hooks = settings[key];
|
|
1533
|
+
if (!hooks) return { file, removed: 0, id };
|
|
1461
1534
|
|
|
1462
1535
|
let removed = 0;
|
|
1463
|
-
for (const [event, entries] of hookEntries(
|
|
1536
|
+
for (const [event, entries] of hookEntries(hooks, t)) {
|
|
1464
1537
|
const kept = entries.filter((entry) => !isVibeHook(entry, id));
|
|
1465
1538
|
removed += entries.length - kept.length;
|
|
1466
1539
|
|
|
1467
|
-
if (kept.length)
|
|
1468
|
-
else delete
|
|
1540
|
+
if (kept.length) hooks[event] = kept;
|
|
1541
|
+
else delete hooks[event];
|
|
1469
1542
|
}
|
|
1470
1543
|
|
|
1471
|
-
if (Object.keys(
|
|
1544
|
+
if (Object.keys(hooks).length === 0) delete settings[key];
|
|
1472
1545
|
fs.writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
|
|
1473
1546
|
return { file, removed, id };
|
|
1474
1547
|
}
|
|
@@ -1513,6 +1586,7 @@ module.exports = {
|
|
|
1513
1586
|
isVibeHook,
|
|
1514
1587
|
userHooksInstalled,
|
|
1515
1588
|
hookEntries,
|
|
1589
|
+
hooksOf,
|
|
1516
1590
|
installSlashCommand,
|
|
1517
1591
|
uninstallSlashCommand,
|
|
1518
1592
|
VIBE_HOOK_FLAG,
|
package/src/interactive.js
CHANGED
|
@@ -36,6 +36,7 @@ const AI_TOOLS = [
|
|
|
36
36
|
{ name: "GitHub Copilot CLI", cmd: ["copilot"], check: "copilot", hookTarget: "copilot" },
|
|
37
37
|
{ name: "Qwen Code", cmd: ["qwen"], check: "qwen", hookTarget: "qwen" },
|
|
38
38
|
{ name: "Windsurf", cmd: ["windsurf"], check: "windsurf", hookTarget: "windsurf" },
|
|
39
|
+
{ name: "Antigravity CLI", cmd: ["agy"], check: "agy", hookTarget: "antigravity" },
|
|
39
40
|
{ name: "Aider", cmd: ["aider"], check: "aider" },
|
|
40
41
|
{ name: "Ollama (Llama 3)", cmd: ["ollama", "run", "llama3"], check: "ollama" },
|
|
41
42
|
{ name: "Custom command...", cmd: null }
|
package/src/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model Context Protocol (MCP) Stdio Server
|
|
3
|
-
* Connects VibeAudio to clients with no hook system (Claude Desktop,
|
|
4
|
-
* Gemini CLI).
|
|
3
|
+
* Connects VibeAudio to clients with no hook system (Claude Desktop, VS Code,
|
|
4
|
+
* older Gemini CLI releases). Agents with hooks (see TARGETS in hooks.js) should use those.
|
|
5
5
|
* Zero dependencies - Pure Node.js JSON-RPC 2.0 over Stdio
|
|
6
6
|
*/
|
|
7
7
|
|
package/scripts/postinstall.js
DELETED
|
@@ -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
|
-
}
|