vibeaudio 0.13.1 → 0.14.1
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 +23 -6
- package/package.json +2 -2
- package/src/cli.js +33 -10
- package/src/hooks.js +109 -9
- package/src/player.js +15 -10
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
> **In a hurry?** `npm i -g vibeaudio`, then `vibe --install-hooks`. Your next prompt has music.
|
|
14
14
|
>
|
|
15
|
-
> **Claude Code
|
|
15
|
+
> **Only Claude Code or Codex?** Skip npm. Inside a Claude Code session: `/plugin marketplace add kiril6/vibeaudio`, then `/plugin install vibeaudio@vibeaudio`. For Codex: `codex plugin marketplace add kiril6/vibeaudio`, then `codex plugin add vibeaudio@vibeaudio` ([details](#as-a-plugin-claude-code-and-codex)).
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
@@ -32,6 +32,7 @@ It is built as **calm technology**, in the sense of Mark Weiser and John Seely B
|
|
|
32
32
|
**How it behaves:**
|
|
33
33
|
|
|
34
34
|
* 🧮 **Pure synthesis, zero MP3s.** Every note, chord and pad is generated in code — no audio assets, no npm dependencies, no `node-gyp`.
|
|
35
|
+
* 🍃 **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
36
|
* 🎼 **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
37
|
* 📈 **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
38
|
* 🔁 **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.
|
|
@@ -324,20 +325,36 @@ Sessions with no id in their payload share a single slot, so they behave as one.
|
|
|
324
325
|
|
|
325
326
|
> **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.
|
|
326
327
|
|
|
327
|
-
### As a Claude Code
|
|
328
|
+
### As a plugin (Claude Code and Codex)
|
|
328
329
|
|
|
329
|
-
If Claude Code is the
|
|
330
|
+
If Claude Code or Codex is the agent you use, the plugin installs the same hooks with no npm step. Inside a Claude Code session, type:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
/plugin marketplace add kiril6/vibeaudio
|
|
334
|
+
/plugin install vibeaudio@vibeaudio
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Or the same from a terminal:
|
|
330
338
|
|
|
331
339
|
```bash
|
|
332
340
|
claude plugin marketplace add kiril6/vibeaudio
|
|
333
341
|
claude plugin install vibeaudio@vibeaudio
|
|
334
342
|
```
|
|
335
343
|
|
|
336
|
-
|
|
344
|
+
For Codex:
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
codex plugin marketplace add kiril6/vibeaudio
|
|
348
|
+
codex plugin add vibeaudio@vibeaudio
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Codex asks you to approve the plugin's hooks once, as it does for any hook. Update with `codex plugin marketplace upgrade vibeaudio`; remove with `codex plugin remove vibeaudio@vibeaudio`.
|
|
352
|
+
|
|
353
|
+
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).
|
|
337
354
|
|
|
338
355
|
- **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.
|
|
339
356
|
- **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.
|
|
340
|
-
- **Claude Code
|
|
357
|
+
- **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.
|
|
341
358
|
- **Not reactive.** `--reactive` is an `--install-hooks` option; the plugin doesn't carry it.
|
|
342
359
|
|
|
343
360
|
### `/vibe` inside Claude Code
|
|
@@ -466,7 +483,7 @@ Each session is `working`, `stuck` (4 of its last 8 tool calls failed) or `waiti
|
|
|
466
483
|
{"v":1,"at":1791026130000,"event":"waiting","session":"…","project":"/work/api","tool":"Bash","status":"waiting"}
|
|
467
484
|
```
|
|
468
485
|
|
|
469
|
-
Events: `started`, `waiting`, `resumed`, `stuck`, `recovered`, `finished` (with `outcome`: `success` or `failure`), `interrupted`, `ended
|
|
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. Some examples:
|
|
470
487
|
|
|
471
488
|
```bash
|
|
472
489
|
# tmux: show the state in the status bar
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeaudio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
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,7 +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
|
|
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
14
|
"postinstall": "node scripts/postinstall.js"
|
|
15
15
|
},
|
|
16
16
|
"publishConfig": {
|
package/src/cli.js
CHANGED
|
@@ -137,7 +137,8 @@ const VALUE_FLAGS = new Set([
|
|
|
137
137
|
"-cv", "--chime-volume",
|
|
138
138
|
"--grace",
|
|
139
139
|
"--seed",
|
|
140
|
-
"--tools"
|
|
140
|
+
"--tools",
|
|
141
|
+
"--event"
|
|
141
142
|
]);
|
|
142
143
|
|
|
143
144
|
function parseArgs(argv) {
|
|
@@ -189,6 +190,7 @@ function parseArgs(argv) {
|
|
|
189
190
|
let reactive = false;
|
|
190
191
|
let followVolume = false;
|
|
191
192
|
let plugin = false;
|
|
193
|
+
let hookEvent = null;
|
|
192
194
|
let here_flag = false;
|
|
193
195
|
let dryRun = false;
|
|
194
196
|
let tools = null;
|
|
@@ -381,6 +383,14 @@ function parseArgs(argv) {
|
|
|
381
383
|
continue;
|
|
382
384
|
}
|
|
383
385
|
|
|
386
|
+
// Internal, also the plugin's: which event this hook entry is for, so a
|
|
387
|
+
// hook can ignore events the agent running it does not use that way.
|
|
388
|
+
if (arg === "--event") {
|
|
389
|
+
hookEvent = args[i + 1] || null;
|
|
390
|
+
i += 2;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
384
394
|
if (arg === "--dry-run") {
|
|
385
395
|
dryRun = true;
|
|
386
396
|
i += 1;
|
|
@@ -494,6 +504,7 @@ function parseArgs(argv) {
|
|
|
494
504
|
reactive,
|
|
495
505
|
followVolume,
|
|
496
506
|
plugin,
|
|
507
|
+
hookEvent,
|
|
497
508
|
dryRun,
|
|
498
509
|
tools,
|
|
499
510
|
typed,
|
|
@@ -688,7 +699,7 @@ function uninstallHookTargets() {
|
|
|
688
699
|
// `npm rm -g` right after this would take away the only thing that could
|
|
689
700
|
// stop it. Done here rather than in uninstallHooks() so that function stays
|
|
690
701
|
// a pure config edit for tests.
|
|
691
|
-
if (hooks.stopDaemon()) console.log(` Stopped the background player that was still running.`);
|
|
702
|
+
if (hooks.stopDaemon({ reason: "uninstall" })) console.log(` Stopped the background player that was still running.`);
|
|
692
703
|
}
|
|
693
704
|
|
|
694
705
|
/**
|
|
@@ -1324,13 +1335,23 @@ function renderToFile(target, genre) {
|
|
|
1324
1335
|
console.log(` \x1b[90mAnother project's sound: run it there, or vibe --seed <n> --render.\x1b[0m\n`);
|
|
1325
1336
|
}
|
|
1326
1337
|
|
|
1327
|
-
function runHookAction(action, { genre, volume, chimeVolume, noChime, reactive, followVolume, plugin, tools, dryRun, typed = {} }) {
|
|
1338
|
+
function runHookAction(action, { genre, volume, chimeVolume, noChime, reactive, followVolume, plugin, hookEvent = null, tools, dryRun, typed = {} }) {
|
|
1328
1339
|
const hooks = require("./hooks");
|
|
1329
1340
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1341
|
+
if (plugin && action.startsWith("hook-")) {
|
|
1342
|
+
// One plugin, four agents (PLUGIN_AGENTS). Each check is against the
|
|
1343
|
+
// agent actually running this hook, never Claude Code's by default.
|
|
1344
|
+
const agent = hooks.pluginAgent();
|
|
1345
|
+
// The plugin's hooks and --install-hooks' hooks are the same events: with
|
|
1346
|
+
// both present every prompt would restart the music and every turn chime
|
|
1347
|
+
// twice. The installed ones win because they carry the user's --reactive.
|
|
1348
|
+
if (hooks.userHooksInstalled(null, agent)) return;
|
|
1349
|
+
// The plugin's file carries every agent's events; this one may not use
|
|
1350
|
+
// this event this way. Copilot fires PermissionRequest before every
|
|
1351
|
+
// permission check, dialog or not, so taking it as a wait there would
|
|
1352
|
+
// pause the music on every tool call.
|
|
1353
|
+
if (hookEvent && !hooks.expectedEvents(agent).includes(hookEvent)) return;
|
|
1354
|
+
}
|
|
1334
1355
|
|
|
1335
1356
|
switch (action) {
|
|
1336
1357
|
case "daemon":
|
|
@@ -1557,6 +1578,7 @@ async function run() {
|
|
|
1557
1578
|
reactive,
|
|
1558
1579
|
followVolume,
|
|
1559
1580
|
plugin,
|
|
1581
|
+
hookEvent,
|
|
1560
1582
|
dryRun,
|
|
1561
1583
|
tools,
|
|
1562
1584
|
typed,
|
|
@@ -1574,7 +1596,7 @@ async function run() {
|
|
|
1574
1596
|
|
|
1575
1597
|
if (hookAction) {
|
|
1576
1598
|
try {
|
|
1577
|
-
return runHookAction(hookAction, { genre, volume, chimeVolume, noChime, reactive, followVolume, plugin, tools, dryRun, typed });
|
|
1599
|
+
return runHookAction(hookAction, { genre, volume, chimeVolume, noChime, reactive, followVolume, plugin, hookEvent, tools, dryRun, typed });
|
|
1578
1600
|
} catch (e) {
|
|
1579
1601
|
// Settings problems are the user's to fix — report them, don't stack-trace.
|
|
1580
1602
|
console.error(`\x1b[31m[vibeaudio] ${e.message}\x1b[0m`);
|
|
@@ -1600,8 +1622,9 @@ async function run() {
|
|
|
1600
1622
|
const minutes = muteMinutes === null ? DEFAULT_MUTE_MINUTES : muteMinutes;
|
|
1601
1623
|
const state = setMuted(true, minutes);
|
|
1602
1624
|
// Muting has to silence what is playing right now, not just the next
|
|
1603
|
-
// prompt - the whole point is that a call is already ringing.
|
|
1604
|
-
|
|
1625
|
+
// prompt - the whole point is that a call is already ringing. The turns
|
|
1626
|
+
// in flight are kept: they still finish, log and show in --state.
|
|
1627
|
+
const stopped = require("./hooks").stopDaemon({ keepSessions: true });
|
|
1605
1628
|
|
|
1606
1629
|
console.log(`\x1b[33m🔇 Muted ${muteRemainingText(state)}.\x1b[0m`);
|
|
1607
1630
|
if (stopped) console.log(` Stopped the player that was running.`);
|
package/src/hooks.js
CHANGED
|
@@ -220,7 +220,9 @@ const TARGETS = {
|
|
|
220
220
|
interrupt: "Interrupt",
|
|
221
221
|
end: "SessionEnd"
|
|
222
222
|
},
|
|
223
|
-
|
|
223
|
+
// Codex caps SessionEnd and Interrupt hooks at 3s (SESSION_END_MAX_TIMEOUT_SEC,
|
|
224
|
+
// discovery.rs) and warns on every run when one asks for more.
|
|
225
|
+
entry: (command, event) => ({ hooks: [{ type: "command", command, timeout: event === "SessionEnd" || event === "Interrupt" ? 3 : 5 }] }),
|
|
224
226
|
commands: (entry) => (entry.hooks || []).map((h) => h.command),
|
|
225
227
|
seed: () => ({}),
|
|
226
228
|
// Codex records a trusted_hash per hook in config.toml and asks before
|
|
@@ -384,6 +386,82 @@ function expectedEvents(id) {
|
|
|
384
386
|
return [ev.start, ev.stop, ...(ev.wait || []), ...(ev.resume || []), ev.failure, ev.end, ev.interrupt].filter(Boolean);
|
|
385
387
|
}
|
|
386
388
|
|
|
389
|
+
/**
|
|
390
|
+
* The agents that load this repository as a plugin, and the hooks file each
|
|
391
|
+
* one reads. Copilot CLI loads `.claude-plugin/` manifests and Qwen Code
|
|
392
|
+
* converts the plugin on install (copying the folder, substituting
|
|
393
|
+
* CLAUDE_PLUGIN_ROOT), so both read Claude Code's hooks/hooks.json. Codex
|
|
394
|
+
* 0.160 would too, but Claude Code's validator rejects an event it does not
|
|
395
|
+
* know ("hooks.Interrupt: Invalid key in record") and then loads none of the
|
|
396
|
+
* file, so Codex's Interrupt cannot live there: `.codex-plugin/plugin.json`,
|
|
397
|
+
* which Codex reads before `.claude-plugin/` and Claude Code never reads,
|
|
398
|
+
* points Codex at a file of its own. Gemini CLI has its own extension format
|
|
399
|
+
* and is not one of these.
|
|
400
|
+
*/
|
|
401
|
+
const PLUGIN_FILES = {
|
|
402
|
+
"hooks/hooks.json": ["claude", "copilot", "qwen"],
|
|
403
|
+
"hooks/codex.json": ["codex"]
|
|
404
|
+
};
|
|
405
|
+
const PLUGIN_AGENTS = Object.values(PLUGIN_FILES).flat();
|
|
406
|
+
|
|
407
|
+
const SLOT_ACTION = { start: "hook-start", stop: "hook-stop", failure: "hook-stop", wait: "hook-wait", resume: "hook-resume", end: "hook-end", interrupt: "hook-end" };
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Every event the given agents fire, with the one action it runs. One file
|
|
411
|
+
* can serve several agents, so it carries the union; an event an agent does
|
|
412
|
+
* not use there is dropped at runtime by `--event` (see runHookAction).
|
|
413
|
+
* Throws if two agents would need different actions for one event name,
|
|
414
|
+
* since the file could not serve both.
|
|
415
|
+
*/
|
|
416
|
+
function pluginHookEvents(agents) {
|
|
417
|
+
const actions = new Map();
|
|
418
|
+
for (const id of agents) {
|
|
419
|
+
for (const [slot, value] of Object.entries(TARGETS[id].events)) {
|
|
420
|
+
if (!SLOT_ACTION[slot]) continue; // `tool` is reactive mode, an --install-hooks option
|
|
421
|
+
for (const event of [].concat(value)) {
|
|
422
|
+
const prior = actions.get(event);
|
|
423
|
+
if (prior && prior !== SLOT_ACTION[slot]) throw new Error(`${event}: ${prior} for one agent, ${SLOT_ACTION[slot]} for ${id}`);
|
|
424
|
+
actions.set(event, SLOT_ACTION[slot]);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return actions;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* A plugin hooks file (see PLUGIN_FILES), generated so it cannot drift from
|
|
433
|
+
* TARGETS. Agents sharing a file share an entry shape, so the first one's is
|
|
434
|
+
* used. `script` is the CLI's path as the host spells it: Gemini's extension
|
|
435
|
+
* substitutes ${extensionPath} itself, the others leave CLAUDE_PLUGIN_ROOT to
|
|
436
|
+
* the shell.
|
|
437
|
+
*/
|
|
438
|
+
function pluginHooksFile(agents, script = "${CLAUDE_PLUGIN_ROOT}/bin/vibeaudio.js") {
|
|
439
|
+
const hooks = {};
|
|
440
|
+
for (const [event, action] of pluginHookEvents(agents)) {
|
|
441
|
+
hooks[event] = [TARGETS[agents[0]].entry(`node "${script}" --${action} --plugin --event ${event}`, event)];
|
|
442
|
+
}
|
|
443
|
+
return { hooks };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Which agent is running a plugin hook, from the environment each one sets:
|
|
448
|
+
* Copilot CLI sets COPILOT_PLUGIN_ROOT (its changelog), Qwen Code sets
|
|
449
|
+
* QWEN_PROJECT_DIR for every hook (hookRunner.ts), Gemini CLI sets
|
|
450
|
+
* GEMINI_PROJECT_DIR for its extension's hooks (hookRunner.ts), and Codex sets
|
|
451
|
+
* PLUGIN_ROOT (discovery.rs). Claude Code sets none of these, only
|
|
452
|
+
* CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA / CLAUDE_PROJECT_DIR (2.1.195's
|
|
453
|
+
* bundle). Copilot also sets PLUGIN_ROOT, so it is checked first.
|
|
454
|
+
* ponytail: a variable exported in the user's own shell would leak into
|
|
455
|
+
* hooks and misattribute them; none of these names is one a user sets.
|
|
456
|
+
*/
|
|
457
|
+
function pluginAgent(env = process.env) {
|
|
458
|
+
if (env.COPILOT_PLUGIN_ROOT) return "copilot";
|
|
459
|
+
if (env.QWEN_PROJECT_DIR) return "qwen";
|
|
460
|
+
if (env.GEMINI_PROJECT_DIR) return "gemini"; // after Qwen, a fork that sets it too
|
|
461
|
+
if (env.PLUGIN_ROOT) return "codex";
|
|
462
|
+
return "claude";
|
|
463
|
+
}
|
|
464
|
+
|
|
387
465
|
function settingsPath() {
|
|
388
466
|
return TARGETS.claude.file();
|
|
389
467
|
}
|
|
@@ -456,14 +534,25 @@ function daemonPlaying() {
|
|
|
456
534
|
|
|
457
535
|
/**
|
|
458
536
|
* Stops the player. `keepSessions` leaves the session files alone: the hooks
|
|
459
|
-
* use it because they decide per session what is still going on
|
|
460
|
-
*
|
|
461
|
-
*
|
|
537
|
+
* use it because they decide per session what is still going on, and --mute
|
|
538
|
+
* because a mute is about sound - the turns are still in flight, and should
|
|
539
|
+
* still reach --state, --events and --report. --stop and uninstall end
|
|
540
|
+
* everything, or a later tool call would resume music nobody is waiting for;
|
|
541
|
+
* each session dropped gets an `ended` event with that `reason`, or an
|
|
542
|
+
* --events consumer would show it working forever (#24).
|
|
462
543
|
*/
|
|
463
|
-
function stopDaemon({ keepSessions = false } = {}) {
|
|
544
|
+
function stopDaemon({ keepSessions = false, reason = "stop" } = {}) {
|
|
464
545
|
const pid = readPid();
|
|
465
546
|
fs.rmSync(PID_FILE, { force: true });
|
|
466
|
-
if (!keepSessions)
|
|
547
|
+
if (!keepSessions) {
|
|
548
|
+
// Removed before its event, so each line's status is what it left behind
|
|
549
|
+
// and the last one reads idle.
|
|
550
|
+
for (const s of listSessions()) {
|
|
551
|
+
fs.rmSync(sessionFile(s.id), { force: true });
|
|
552
|
+
emitEvent("ended", s.id, s.project, { reason });
|
|
553
|
+
}
|
|
554
|
+
fs.rmSync(SESSIONS_DIR, { recursive: true, force: true });
|
|
555
|
+
}
|
|
467
556
|
if (pid === null || !isOurDaemon(pid)) return false;
|
|
468
557
|
|
|
469
558
|
try {
|
|
@@ -800,6 +889,12 @@ function runDaemon(genre, volume, { reactive = false, volumeSource = null } = {}
|
|
|
800
889
|
setTimeout(shutdown, MAX_DAEMON_MS);
|
|
801
890
|
|
|
802
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();
|
|
803
898
|
// A volume saved while this plays reaches it now, not at the next prompt.
|
|
804
899
|
if (volumeSource) player.setVolume(volumeSource());
|
|
805
900
|
if (sweep()) return;
|
|
@@ -1145,7 +1240,7 @@ function setHook(hooks, event, command, id) {
|
|
|
1145
1240
|
// keeps the existing entries at their original index, which is what Codex
|
|
1146
1241
|
// keys its per-hook trust records by.
|
|
1147
1242
|
const kept = (hooks[event] || []).filter((entry) => !isVibeHook(entry, id));
|
|
1148
|
-
kept.push(target(id).entry(command));
|
|
1243
|
+
kept.push(target(id).entry(command, event));
|
|
1149
1244
|
hooks[event] = kept;
|
|
1150
1245
|
}
|
|
1151
1246
|
|
|
@@ -1168,8 +1263,8 @@ function readVibeEntryCount(file, id) {
|
|
|
1168
1263
|
}
|
|
1169
1264
|
|
|
1170
1265
|
/** True when --install-hooks has already written our entries for Claude Code. */
|
|
1171
|
-
function userHooksInstalled(file = null) {
|
|
1172
|
-
return readVibeEntryCount(file || TARGETS.
|
|
1266
|
+
function userHooksInstalled(file = null, id = "claude") {
|
|
1267
|
+
return readVibeEntryCount(file || TARGETS[id].file(), id) > 0;
|
|
1173
1268
|
}
|
|
1174
1269
|
|
|
1175
1270
|
function loadSettings(file, t = null) {
|
|
@@ -1383,6 +1478,11 @@ module.exports = {
|
|
|
1383
1478
|
claudeConfigDir,
|
|
1384
1479
|
codexHome,
|
|
1385
1480
|
expectedEvents,
|
|
1481
|
+
PLUGIN_FILES,
|
|
1482
|
+
PLUGIN_AGENTS,
|
|
1483
|
+
pluginHookEvents,
|
|
1484
|
+
pluginHooksFile,
|
|
1485
|
+
pluginAgent,
|
|
1386
1486
|
runDaemon,
|
|
1387
1487
|
hookStart,
|
|
1388
1488
|
hookStop,
|
package/src/player.js
CHANGED
|
@@ -23,7 +23,10 @@ const { hashString } = require("./synth/generator");
|
|
|
23
23
|
const { addTension } = require("./synth/tension");
|
|
24
24
|
const pkg = require("../package.json");
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// VIBE_CACHE_DIR is internal: the suite points it at a temp directory, so it
|
|
27
|
+
// neither races live hooks pruning the real cache nor evicts a real project's
|
|
28
|
+
// audio (#38).
|
|
29
|
+
const CACHE_ROOT = process.env.VIBE_CACHE_DIR || path.join(os.homedir(), ".vibeaudio", "cache");
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* The cache key has to change whenever the audio would, or existing users keep
|
|
@@ -189,21 +192,23 @@ function ensureCacheDir() {
|
|
|
189
192
|
*/
|
|
190
193
|
function pruneStaleCache() {
|
|
191
194
|
try {
|
|
192
|
-
|
|
193
|
-
const full = path.join(CACHE_ROOT, entry.name);
|
|
194
|
-
if (entry.isDirectory() && /^v\d/.test(entry.name) && entry.name !== path.basename(CACHE_DIR)) {
|
|
195
|
-
fs.rmSync(full, { recursive: true, force: true });
|
|
196
|
-
} else if (entry.isFile() && entry.name.endsWith(".wav")) {
|
|
197
|
-
fs.rmSync(full, { force: true });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
195
|
+
removeCacheEntries(path.basename(CACHE_DIR));
|
|
200
196
|
} catch (e) {
|
|
201
197
|
// Pruning is best-effort; a stale cache is not worth failing a run over.
|
|
202
198
|
}
|
|
203
199
|
}
|
|
204
200
|
|
|
201
|
+
// Only what we write - key directories, and loose WAVs from before keys - and
|
|
202
|
+
// never the root itself: VIBE_CACHE_DIR may name a directory holding more.
|
|
203
|
+
function removeCacheEntries(keep = null) {
|
|
204
|
+
for (const entry of fs.readdirSync(CACHE_ROOT, { withFileTypes: true })) {
|
|
205
|
+
const ours = entry.isDirectory() ? /^v\d/.test(entry.name) && entry.name !== keep : entry.isFile() && entry.name.endsWith(".wav");
|
|
206
|
+
if (ours) fs.rmSync(path.join(CACHE_ROOT, entry.name), { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
205
210
|
function clearCache() {
|
|
206
|
-
fs.
|
|
211
|
+
if (fs.existsSync(CACHE_ROOT)) removeCacheEntries();
|
|
207
212
|
return CACHE_ROOT;
|
|
208
213
|
}
|
|
209
214
|
|