claude-music 1.1.0 → 1.1.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.
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { EventEmitter } from "node:events";
3
+ import { installShutdownHooks } from "../server.js";
4
+ describe("installShutdownHooks", () => {
5
+ it("stops the daemon when the stdio pipe closes (Claude Code exits)", () => {
6
+ const stop = vi.fn();
7
+ const exit = vi.fn();
8
+ const stdin = new EventEmitter();
9
+ installShutdownHooks(stop, { stdin, exit });
10
+ expect(stop).not.toHaveBeenCalled();
11
+ // Parent closed the pipe → stdin hits EOF.
12
+ stdin.emit("end");
13
+ expect(stop).toHaveBeenCalledOnce();
14
+ expect(exit).toHaveBeenCalledWith(0);
15
+ });
16
+ it("stops at most once even if several signals arrive", () => {
17
+ const stop = vi.fn();
18
+ const exit = vi.fn();
19
+ const stdin = new EventEmitter();
20
+ installShutdownHooks(stop, { stdin, exit });
21
+ stdin.emit("end");
22
+ stdin.emit("close");
23
+ expect(stop).toHaveBeenCalledOnce();
24
+ });
25
+ it("survives a throwing stop() without rethrowing", () => {
26
+ const stop = vi.fn(() => {
27
+ throw new Error("daemon already gone");
28
+ });
29
+ const exit = vi.fn();
30
+ const stdin = new EventEmitter();
31
+ installShutdownHooks(stop, { stdin, exit });
32
+ expect(() => stdin.emit("end")).not.toThrow();
33
+ expect(exit).toHaveBeenCalledWith(0);
34
+ });
35
+ });
package/dist/server.js CHANGED
@@ -96,6 +96,41 @@ export function stopDaemon() {
96
96
  }
97
97
  fs.rmSync(SOCKET_PATH, { force: true });
98
98
  }
99
+ /**
100
+ * Tie the daemon's lifetime to this MCP server process. The daemon is spawned
101
+ * detached so it survives across tool calls, which also means nothing stops it
102
+ * when Claude Code shuts us down. We bridge that gap here: when the host closes
103
+ * our stdio pipe (the normal "Claude Code exited" path — stdin hits EOF) or
104
+ * sends us a termination signal, stop the daemon so the music doesn't outlive
105
+ * the session.
106
+ */
107
+ export function installShutdownHooks(stop, opts = {}) {
108
+ const stdin = opts.stdin ?? process.stdin;
109
+ const exit = opts.exit ?? ((code) => process.exit(code));
110
+ let done = false;
111
+ const cleanup = (shouldExit) => {
112
+ if (done)
113
+ return;
114
+ done = true;
115
+ try {
116
+ stop();
117
+ }
118
+ catch {
119
+ /* best effort — we're on the way out regardless */
120
+ }
121
+ if (shouldExit)
122
+ exit(0);
123
+ };
124
+ // Parent (Claude Code) closed the stdio pipe → EOF reaches us as 'end'/'close'.
125
+ stdin.on("end", () => cleanup(true));
126
+ stdin.on("close", () => cleanup(true));
127
+ // Host asked us to terminate.
128
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
129
+ process.on(sig, () => cleanup(true));
130
+ }
131
+ // Last-ditch sync safety net for any other exit path.
132
+ process.on("exit", () => cleanup(false));
133
+ }
99
134
  /** Build and connect the MCP stdio server. Invoked by `claude-music mcp`. */
100
135
  export async function runServer() {
101
136
  const server = new McpServer({ name: "mrt2-mcp", version: "1.0.0" });
@@ -142,4 +177,6 @@ export async function runServer() {
142
177
  });
143
178
  const transport = new StdioServerTransport();
144
179
  await server.connect(transport);
180
+ // connect() puts stdin in flowing mode, so 'end' fires reliably on EOF.
181
+ installShutdownHooks(stopDaemon);
145
182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-music",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Live AI background music for Claude Code, scored in real time by Claude itself (Magenta RealTime 2 on Apple Silicon).",
5
5
  "type": "module",
6
6
  "bin": {