vibeaudio 0.16.0 → 0.16.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 +8 -4
- package/package.json +1 -1
- package/src/interactive.js +3 -7
- package/src/mcp.js +34 -3
- package/src/output.js +3 -1
- package/src/player.js +4 -2
package/README.md
CHANGED
|
@@ -921,10 +921,6 @@ Apart from those two, the three commands above remove everything VibeAudio write
|
|
|
921
921
|
|
|
922
922
|
---
|
|
923
923
|
|
|
924
|
-
If VibeAudio is useful to you, consider giving it a ⭐ on [GitHub](https://github.com/kiril6/vibeaudio) — it helps others find it.
|
|
925
|
-
|
|
926
|
-
---
|
|
927
|
-
|
|
928
924
|
## 🤝 Contributing
|
|
929
925
|
|
|
930
926
|
Contributions welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md) for the ground rules (zero runtime dependencies, no build step, pure synth modules) and the dev loop. Found a bug? [Open an issue](https://github.com/kiril6/vibeaudio/issues/new) with your **OS**, **Node version**, and which audio player you have installed.
|
|
@@ -940,5 +936,13 @@ npm test
|
|
|
940
936
|
|
|
941
937
|
---
|
|
942
938
|
|
|
939
|
+
## 💛 Supporting the Project
|
|
940
|
+
|
|
941
|
+
If VibeAudio is useful to you, consider giving it a ⭐ — it helps others find it.
|
|
942
|
+
|
|
943
|
+
Support development: <a href="https://ko-fi.com/K3K2X0ERJ"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="ko-fi" align="absmiddle" /></a>
|
|
944
|
+
|
|
945
|
+
---
|
|
946
|
+
|
|
943
947
|
## 📄 License
|
|
944
948
|
MIT © 2026
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeaudio",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.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",
|
package/src/interactive.js
CHANGED
|
@@ -3,19 +3,15 @@
|
|
|
3
3
|
* Zero dependencies - Pure Node.js ANSI and Readline
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const { spawnSync } = require("child_process");
|
|
7
7
|
const readline = require("readline");
|
|
8
8
|
|
|
9
9
|
// Windows has no `which`; without this every tool shows as missing there.
|
|
10
10
|
const LOOKUP_CMD = process.platform === "win32" ? "where" : "which";
|
|
11
11
|
|
|
12
12
|
function isInstalled(cmd) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return true;
|
|
16
|
-
} catch (e) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
13
|
+
// An argument vector, not a command string: nothing in `cmd` reaches a shell.
|
|
14
|
+
return spawnSync(LOOKUP_CMD, [cmd], { stdio: "ignore" }).status === 0;
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
// Convenience only - the wrapper runs any command, and "Custom command..."
|
package/src/mcp.js
CHANGED
|
@@ -13,6 +13,19 @@ const pkg = require("../package.json");
|
|
|
13
13
|
// own ceiling rather than looping forever.
|
|
14
14
|
const MAX_PLAYBACK_MS = 15 * 60 * 1000;
|
|
15
15
|
|
|
16
|
+
// All four hints on every tool, as the spec's ToolAnnotations defines them, so
|
|
17
|
+
// a host can tell what each one does before the model calls it. None touches
|
|
18
|
+
// anything beyond the local audio player and VibeAudio's own cache.
|
|
19
|
+
const annotate = (title, { readOnly, idempotent }) => ({
|
|
20
|
+
title,
|
|
21
|
+
readOnlyHint: readOnly,
|
|
22
|
+
destructiveHint: false,
|
|
23
|
+
idempotentHint: idempotent,
|
|
24
|
+
openWorldHint: false
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const OUTCOMES = ["success", "failure"];
|
|
28
|
+
|
|
16
29
|
const TOOLS = [
|
|
17
30
|
{
|
|
18
31
|
name: "vibe_play",
|
|
@@ -22,6 +35,7 @@ const TOOLS = [
|
|
|
22
35
|
"long file edits, repeated tool calls, anything the user will wait through. " +
|
|
23
36
|
"Always pair it with vibe_stop when the task resolves. Skip it for quick " +
|
|
24
37
|
"answers: music around a one-second reply is worse than silence.",
|
|
38
|
+
annotations: annotate("Start focus music", { readOnly: false, idempotent: true }),
|
|
25
39
|
inputSchema: {
|
|
26
40
|
type: "object",
|
|
27
41
|
properties: {
|
|
@@ -46,13 +60,14 @@ const TOOLS = [
|
|
|
46
60
|
"task resolves and you are ready to hand back a result, including when it " +
|
|
47
61
|
"failed - pass outcome 'failure' so the chime says so. Never leave music " +
|
|
48
62
|
"playing after a vibe_play task is done.",
|
|
63
|
+
annotations: annotate("Stop focus music", { readOnly: false, idempotent: false }),
|
|
49
64
|
inputSchema: {
|
|
50
65
|
type: "object",
|
|
51
66
|
properties: {
|
|
52
67
|
outcome: {
|
|
53
68
|
type: "string",
|
|
54
69
|
description: "Resolution outcome: 'success' (ascending chime) or 'failure' (soft minor tone)",
|
|
55
|
-
enum:
|
|
70
|
+
enum: OUTCOMES
|
|
56
71
|
},
|
|
57
72
|
playChime: {
|
|
58
73
|
type: "boolean",
|
|
@@ -67,6 +82,7 @@ const TOOLS = [
|
|
|
67
82
|
"Check whether focus music is currently playing, and in which genre and " +
|
|
68
83
|
"intensity tier. Use it to avoid starting a second track, or to confirm " +
|
|
69
84
|
"nothing was left running.",
|
|
85
|
+
annotations: annotate("Check focus music", { readOnly: true, idempotent: true }),
|
|
70
86
|
inputSchema: {
|
|
71
87
|
type: "object",
|
|
72
88
|
properties: {}
|
|
@@ -137,7 +153,10 @@ function handleMessage(player, msg) {
|
|
|
137
153
|
// saved file here is what makes `vibe --genre jazz` mean jazz in a
|
|
138
154
|
// desktop client too, rather than only in the terminal.
|
|
139
155
|
const saved = loadConfig();
|
|
140
|
-
|
|
156
|
+
// The argument comes from the client: only a string counts. A number or
|
|
157
|
+
// object here used to reach `.toLowerCase()` and fail the whole call.
|
|
158
|
+
const asked = typeof args.genre === "string" ? args.genre.trim() : "";
|
|
159
|
+
const requested = String(asked || process.env.VIBE_GENRE || saved.genre || "lofi");
|
|
141
160
|
// An unknown genre already fell back to lofi inside the generator, but
|
|
142
161
|
// player.genre kept the name nobody implements - so the model told the
|
|
143
162
|
// user it was playing something that does not exist.
|
|
@@ -161,9 +180,12 @@ function handleMessage(player, msg) {
|
|
|
161
180
|
// start() returns false for three unrelated reasons, and the model
|
|
162
181
|
// relays whatever we say here to the user. Reporting a deliberate mute
|
|
163
182
|
// as a missing audio player sends them debugging their sound stack.
|
|
183
|
+
// What goes back to the model is client-supplied text, so echo a short
|
|
184
|
+
// plain version of it, not whatever was sent.
|
|
185
|
+
const shown = requested.replace(/[^\w.-]/g, "").slice(0, 24);
|
|
164
186
|
const fallbackNote = genre === requested
|
|
165
187
|
? ""
|
|
166
|
-
: ` (requested genre '${
|
|
188
|
+
: ` (requested genre '${shown}' is not one of ${AVAILABLE_GENRES.join(", ")}, random)`;
|
|
167
189
|
const text = started
|
|
168
190
|
? `Started playing ${player.genre} procedural focus music at ${Math.round(volume * 100)}% volume.${fallbackNote}`
|
|
169
191
|
: player.isPlaying
|
|
@@ -183,6 +205,15 @@ function handleMessage(player, msg) {
|
|
|
183
205
|
|
|
184
206
|
if (name === "vibe_stop") {
|
|
185
207
|
const outcome = args.outcome || "success";
|
|
208
|
+
// The schema's enum is a hint to the model, not a check: nothing stops a
|
|
209
|
+
// client sending anything, and this string is echoed back below.
|
|
210
|
+
if (!OUTCOMES.includes(outcome)) {
|
|
211
|
+
return {
|
|
212
|
+
jsonrpc: "2.0",
|
|
213
|
+
id,
|
|
214
|
+
result: { isError: true, content: [{ type: "text", text: `outcome must be one of: ${OUTCOMES.join(", ")}.` }] }
|
|
215
|
+
};
|
|
216
|
+
}
|
|
186
217
|
const playChime = args.playChime !== false;
|
|
187
218
|
|
|
188
219
|
const wasPlaying = player.stop({ playChime, outcome });
|
package/src/output.js
CHANGED
|
@@ -45,7 +45,9 @@ function parseDefaultOutput(json) {
|
|
|
45
45
|
function macOutput() {
|
|
46
46
|
if (process.platform !== "darwin") return null;
|
|
47
47
|
const run = (cmd, args) => {
|
|
48
|
-
|
|
48
|
+
// SIGKILL, not the default SIGTERM: a lookup stuck on audio hardware (a CI
|
|
49
|
+
// runner has none) can ignore TERM, and spawnSync then waits on it forever.
|
|
50
|
+
const r = spawnSync(cmd, args, { encoding: "utf8", timeout: 5000, killSignal: "SIGKILL" });
|
|
49
51
|
return r.status === 0 ? r.stdout : null;
|
|
50
52
|
};
|
|
51
53
|
const settings = run("osascript", ["-e", "get volume settings"]);
|
package/src/player.js
CHANGED
|
@@ -698,7 +698,9 @@ function chimeKeyFor(genre) {
|
|
|
698
698
|
|
|
699
699
|
function getChimePath(outcome = "success", gain = 1, key = DEFAULT_CHIME_KEY) {
|
|
700
700
|
ensureCacheDir();
|
|
701
|
-
|
|
701
|
+
// Own keys only: `CHIMES["constructor"]` is truthy, and used to reach the
|
|
702
|
+
// generator call below as a function that is not one of ours.
|
|
703
|
+
const kind = outcome === "error" ? "failure" : Object.hasOwn(CHIMES, outcome) ? outcome : "success";
|
|
702
704
|
// Only the success chime has keys, and the default keeps its original file
|
|
703
705
|
// name so an upgrade does not leave a cached copy behind.
|
|
704
706
|
const keyed = kind === "success" && key !== DEFAULT_CHIME_KEY && SUCCESS_CHIME_NOTES[key];
|
|
@@ -955,7 +957,7 @@ class AudioPlayer {
|
|
|
955
957
|
.on("error", () => {})
|
|
956
958
|
.unref();
|
|
957
959
|
} else {
|
|
958
|
-
spawnSync(backend.cmd, backend.args(chimeFile, clamped), { stdio: "ignore", timeout: 2500 });
|
|
960
|
+
spawnSync(backend.cmd, backend.args(chimeFile, clamped), { stdio: "ignore", timeout: 2500, killSignal: "SIGKILL" });
|
|
959
961
|
}
|
|
960
962
|
} catch (e) {
|
|
961
963
|
// Ignore timeout
|