@mthines/reaper-mcp 0.19.0-beta.19.2 → 0.19.0-beta.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main.js +154 -634
- package/package.json +1 -1
- package/reaper/mcp_bridge.lua +99 -167
- package/reaper/mcp_midi_emitter.jsfx +44 -0
package/main.js
CHANGED
|
@@ -27,10 +27,10 @@ var SCOPE_NAME = "reaper-mcp-server";
|
|
|
27
27
|
function readServiceVersion() {
|
|
28
28
|
try {
|
|
29
29
|
const require2 = createRequire(import.meta.url);
|
|
30
|
-
const
|
|
30
|
+
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
31
31
|
const pkgPaths = [
|
|
32
|
-
join(
|
|
33
|
-
join(
|
|
32
|
+
join(__dirname2, "..", "package.json"),
|
|
33
|
+
join(__dirname2, "package.json")
|
|
34
34
|
];
|
|
35
35
|
for (const p of pkgPaths) {
|
|
36
36
|
try {
|
|
@@ -1375,6 +1375,59 @@ function registerMidiTools(server) {
|
|
|
1375
1375
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1376
1376
|
}
|
|
1377
1377
|
);
|
|
1378
|
+
server.tool(
|
|
1379
|
+
"send_midi_cc",
|
|
1380
|
+
"Send a MIDI CC (continuous controller) event in real-time to a track's MIDI output. Auto-inserts the MCP MIDI Emitter JSFX if not present. Common CC numbers: 1=modulation, 7=volume, 10=pan, 11=expression, 64=sustain, 74=filter cutoff.",
|
|
1381
|
+
{
|
|
1382
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
1383
|
+
cc: z10.coerce.number().min(0).max(127).describe("CC number (0-127)"),
|
|
1384
|
+
value: z10.coerce.number().min(0).max(127).describe("CC value (0-127)"),
|
|
1385
|
+
channel: z10.coerce.number().min(0).max(15).optional().describe("MIDI channel 0-15 (default 0)")
|
|
1386
|
+
},
|
|
1387
|
+
async ({ trackIndex, cc, value, channel }) => {
|
|
1388
|
+
const res = await sendCommand("send_midi_cc", { trackIndex, cc, value, channel: channel ?? 0 });
|
|
1389
|
+
if (!res.success) {
|
|
1390
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1391
|
+
}
|
|
1392
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1393
|
+
}
|
|
1394
|
+
);
|
|
1395
|
+
server.tool(
|
|
1396
|
+
"send_midi_pc",
|
|
1397
|
+
"Send a MIDI program change in real-time, optionally preceded by bank select (CC0 MSB + CC32 LSB). Auto-inserts the MCP MIDI Emitter JSFX if not present.",
|
|
1398
|
+
{
|
|
1399
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
1400
|
+
program: z10.coerce.number().min(0).max(127).describe("Program number (0-127)"),
|
|
1401
|
+
channel: z10.coerce.number().min(0).max(15).optional().describe("MIDI channel 0-15 (default 0)"),
|
|
1402
|
+
bankMsb: z10.coerce.number().min(0).max(127).optional().describe("Bank select MSB (CC0, 0-127) \u2014 omit if no bank select needed"),
|
|
1403
|
+
bankLsb: z10.coerce.number().min(0).max(127).optional().describe("Bank select LSB (CC32, 0-127) \u2014 omit if no bank select needed")
|
|
1404
|
+
},
|
|
1405
|
+
async ({ trackIndex, program, channel, bankMsb, bankLsb }) => {
|
|
1406
|
+
const res = await sendCommand("send_midi_pc", { trackIndex, program, channel: channel ?? 0, bankMsb, bankLsb });
|
|
1407
|
+
if (!res.success) {
|
|
1408
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1409
|
+
}
|
|
1410
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1411
|
+
}
|
|
1412
|
+
);
|
|
1413
|
+
server.tool(
|
|
1414
|
+
"send_midi_note",
|
|
1415
|
+
"Send a MIDI note-on in real-time to a track's MIDI output. If durationMs is provided, a note-off is automatically scheduled after that many milliseconds. Auto-inserts the MCP MIDI Emitter JSFX if not present.",
|
|
1416
|
+
{
|
|
1417
|
+
trackIndex: z10.coerce.number().min(0).describe("0-based track index"),
|
|
1418
|
+
pitch: z10.coerce.number().min(0).max(127).describe("MIDI note number (0-127, 60=C4/Middle C)"),
|
|
1419
|
+
velocity: z10.coerce.number().min(1).max(127).describe("Note velocity (1-127)"),
|
|
1420
|
+
channel: z10.coerce.number().min(0).max(15).optional().describe("MIDI channel 0-15 (default 0)"),
|
|
1421
|
+
durationMs: z10.coerce.number().min(1).int().optional().describe("Note duration in milliseconds (\u22651) \u2014 if provided, schedules an automatic note-off")
|
|
1422
|
+
},
|
|
1423
|
+
async ({ trackIndex, pitch, velocity, channel, durationMs }) => {
|
|
1424
|
+
const res = await sendCommand("send_midi_note", { trackIndex, pitch, velocity, channel: channel ?? 0, durationMs });
|
|
1425
|
+
if (!res.success) {
|
|
1426
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1427
|
+
}
|
|
1428
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1429
|
+
}
|
|
1430
|
+
);
|
|
1378
1431
|
}
|
|
1379
1432
|
|
|
1380
1433
|
// apps/reaper-mcp-server/src/tools/media.ts
|
|
@@ -2179,288 +2232,6 @@ function registerCategoryTools(server) {
|
|
|
2179
2232
|
);
|
|
2180
2233
|
}
|
|
2181
2234
|
|
|
2182
|
-
// apps/reaper-mcp-server/src/tools/aesthetics.ts
|
|
2183
|
-
import { z as z17 } from "zod/v4";
|
|
2184
|
-
import { unlink as unlink2 } from "node:fs/promises";
|
|
2185
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2186
|
-
|
|
2187
|
-
// apps/reaper-mcp-server/src/sidecar.ts
|
|
2188
|
-
import { spawn } from "node:child_process";
|
|
2189
|
-
import { existsSync } from "node:fs";
|
|
2190
|
-
import { join as join3 } from "node:path";
|
|
2191
|
-
import { homedir as homedir2, platform as platform2 } from "node:os";
|
|
2192
|
-
var VENV_BIN = platform2() === "win32" ? "Scripts" : "bin";
|
|
2193
|
-
var VENV_PYTHON_NAME = platform2() === "win32" ? "python.exe" : "python";
|
|
2194
|
-
var SIDECAR_VENV_PATH = join3(homedir2(), ".reaper-mcp", "sidecar-venv");
|
|
2195
|
-
var SIDECAR_SCRIPT_PATH = join3(homedir2(), ".reaper-mcp", "sidecar", "server.py");
|
|
2196
|
-
var VENV_PYTHON = join3(SIDECAR_VENV_PATH, VENV_BIN, VENV_PYTHON_NAME);
|
|
2197
|
-
var _rawTimeout = Number(process.env["REAPER_MCP_SIDECAR_TIMEOUT_MS"]);
|
|
2198
|
-
var ANALYZE_TIMEOUT_MS = Number.isFinite(_rawTimeout) && _rawTimeout > 0 ? _rawTimeout : 6e4;
|
|
2199
|
-
var SidecarClientImpl = class {
|
|
2200
|
-
process = null;
|
|
2201
|
-
pending = /* @__PURE__ */ new Map();
|
|
2202
|
-
nextId = 1;
|
|
2203
|
-
lineBuffer = "";
|
|
2204
|
-
restarting = false;
|
|
2205
|
-
restartAttempted = false;
|
|
2206
|
-
/** Cached spawn promise — prevents double-spawn on concurrent cold-start calls. */
|
|
2207
|
-
spawnPromise = null;
|
|
2208
|
-
isAvailable() {
|
|
2209
|
-
return existsSync(SIDECAR_SCRIPT_PATH) && existsSync(VENV_PYTHON);
|
|
2210
|
-
}
|
|
2211
|
-
async analyze(wavPath, startTime, endTime) {
|
|
2212
|
-
if (!this.isAvailable()) {
|
|
2213
|
-
throw new Error(
|
|
2214
|
-
"Audio understanding sidecar not installed. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar"
|
|
2215
|
-
);
|
|
2216
|
-
}
|
|
2217
|
-
await this.ensureRunning();
|
|
2218
|
-
return new Promise((resolve, reject) => {
|
|
2219
|
-
const id = this.nextId++;
|
|
2220
|
-
const request = {
|
|
2221
|
-
jsonrpc: "2.0",
|
|
2222
|
-
id,
|
|
2223
|
-
method: "analyze",
|
|
2224
|
-
params: { path: wavPath, startTime, endTime }
|
|
2225
|
-
};
|
|
2226
|
-
const timer = setTimeout(() => {
|
|
2227
|
-
if (this.pending.delete(id)) {
|
|
2228
|
-
process.stderr.write(
|
|
2229
|
-
`[reaper-mcp] Sidecar analyze() timed out after ${ANALYZE_TIMEOUT_MS}ms \u2014 killing process
|
|
2230
|
-
`
|
|
2231
|
-
);
|
|
2232
|
-
if (this.process) {
|
|
2233
|
-
this.process.kill("SIGKILL");
|
|
2234
|
-
this.process = null;
|
|
2235
|
-
}
|
|
2236
|
-
reject(new Error(
|
|
2237
|
-
`Sidecar timeout after ${ANALYZE_TIMEOUT_MS}ms. Override with REAPER_MCP_SIDECAR_TIMEOUT_MS env var.`
|
|
2238
|
-
));
|
|
2239
|
-
}
|
|
2240
|
-
}, ANALYZE_TIMEOUT_MS);
|
|
2241
|
-
this.pending.set(id, {
|
|
2242
|
-
resolve: (r) => {
|
|
2243
|
-
clearTimeout(timer);
|
|
2244
|
-
resolve(r);
|
|
2245
|
-
},
|
|
2246
|
-
reject: (e) => {
|
|
2247
|
-
clearTimeout(timer);
|
|
2248
|
-
reject(e);
|
|
2249
|
-
}
|
|
2250
|
-
});
|
|
2251
|
-
const line = JSON.stringify(request) + "\n";
|
|
2252
|
-
try {
|
|
2253
|
-
const proc = this.process;
|
|
2254
|
-
if (!proc || !proc.stdin) {
|
|
2255
|
-
throw new Error("Sidecar process not running");
|
|
2256
|
-
}
|
|
2257
|
-
proc.stdin.write(line);
|
|
2258
|
-
} catch (err) {
|
|
2259
|
-
clearTimeout(timer);
|
|
2260
|
-
this.pending.delete(id);
|
|
2261
|
-
reject(new Error(`Failed to write to sidecar stdin: ${err instanceof Error ? err.message : String(err)}`));
|
|
2262
|
-
}
|
|
2263
|
-
});
|
|
2264
|
-
}
|
|
2265
|
-
shutdown() {
|
|
2266
|
-
if (this.process) {
|
|
2267
|
-
this.process.kill("SIGTERM");
|
|
2268
|
-
this.process = null;
|
|
2269
|
-
}
|
|
2270
|
-
for (const [id, { reject }] of this.pending.entries()) {
|
|
2271
|
-
reject(new Error("Sidecar shut down"));
|
|
2272
|
-
this.pending.delete(id);
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
// -------------------------------------------------------------------------
|
|
2276
|
-
// Private
|
|
2277
|
-
// -------------------------------------------------------------------------
|
|
2278
|
-
async ensureRunning() {
|
|
2279
|
-
if (this.process && !this.process.killed) return;
|
|
2280
|
-
if (!this.spawnPromise) {
|
|
2281
|
-
this.spawnPromise = this.spawn().finally(() => {
|
|
2282
|
-
this.spawnPromise = null;
|
|
2283
|
-
});
|
|
2284
|
-
}
|
|
2285
|
-
await this.spawnPromise;
|
|
2286
|
-
}
|
|
2287
|
-
async spawn() {
|
|
2288
|
-
const child = spawn(VENV_PYTHON, [SIDECAR_SCRIPT_PATH], {
|
|
2289
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2290
|
-
});
|
|
2291
|
-
if (child.stdout) {
|
|
2292
|
-
child.stdout.setEncoding("utf8");
|
|
2293
|
-
child.stdout.on("data", (chunk) => this.onStdout(chunk));
|
|
2294
|
-
}
|
|
2295
|
-
if (child.stderr) {
|
|
2296
|
-
child.stderr.setEncoding("utf8");
|
|
2297
|
-
child.stderr.on("data", (data) => {
|
|
2298
|
-
process.stderr.write(`[reaper-mcp-sidecar] ${data}`);
|
|
2299
|
-
});
|
|
2300
|
-
}
|
|
2301
|
-
child.on("exit", (code) => {
|
|
2302
|
-
process.stderr.write(`[reaper-mcp] Sidecar process exited (code ${code})
|
|
2303
|
-
`);
|
|
2304
|
-
this.process = null;
|
|
2305
|
-
this.handleProcessDeath();
|
|
2306
|
-
});
|
|
2307
|
-
child.on("error", (err) => {
|
|
2308
|
-
process.stderr.write(`[reaper-mcp] Sidecar spawn error: ${err.message}
|
|
2309
|
-
`);
|
|
2310
|
-
this.process = null;
|
|
2311
|
-
this.handleProcessDeath();
|
|
2312
|
-
});
|
|
2313
|
-
this.process = child;
|
|
2314
|
-
this.lineBuffer = "";
|
|
2315
|
-
this.restarting = false;
|
|
2316
|
-
this.restartAttempted = false;
|
|
2317
|
-
}
|
|
2318
|
-
onStdout(chunk) {
|
|
2319
|
-
this.lineBuffer += chunk;
|
|
2320
|
-
let newlineIdx;
|
|
2321
|
-
while ((newlineIdx = this.lineBuffer.indexOf("\n")) !== -1) {
|
|
2322
|
-
const line = this.lineBuffer.slice(0, newlineIdx).trim();
|
|
2323
|
-
this.lineBuffer = this.lineBuffer.slice(newlineIdx + 1);
|
|
2324
|
-
if (line) this.onResponse(line);
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
onResponse(line) {
|
|
2328
|
-
let response;
|
|
2329
|
-
try {
|
|
2330
|
-
response = JSON.parse(line);
|
|
2331
|
-
} catch {
|
|
2332
|
-
process.stderr.write(`[reaper-mcp] Sidecar returned malformed JSON: ${line}
|
|
2333
|
-
`);
|
|
2334
|
-
for (const [id, { reject }] of this.pending.entries()) {
|
|
2335
|
-
reject(new Error(`Sidecar returned malformed JSON: ${line}`));
|
|
2336
|
-
this.pending.delete(id);
|
|
2337
|
-
}
|
|
2338
|
-
return;
|
|
2339
|
-
}
|
|
2340
|
-
const pending = this.pending.get(response.id);
|
|
2341
|
-
if (!pending) {
|
|
2342
|
-
process.stderr.write(`[reaper-mcp] Sidecar response for unknown id ${response.id}
|
|
2343
|
-
`);
|
|
2344
|
-
return;
|
|
2345
|
-
}
|
|
2346
|
-
this.pending.delete(response.id);
|
|
2347
|
-
if (response.error) {
|
|
2348
|
-
pending.reject(new Error(response.error.message));
|
|
2349
|
-
} else if (response.result) {
|
|
2350
|
-
pending.resolve(response.result);
|
|
2351
|
-
} else {
|
|
2352
|
-
pending.reject(new Error("Sidecar response missing both result and error fields"));
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
handleProcessDeath() {
|
|
2356
|
-
if (this.restarting || this.pending.size === 0) {
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
if (!this.restartAttempted) {
|
|
2360
|
-
this.restarting = true;
|
|
2361
|
-
this.restartAttempted = true;
|
|
2362
|
-
process.stderr.write("[reaper-mcp] Sidecar crashed; attempting one restart...\n");
|
|
2363
|
-
this.spawn().then(() => {
|
|
2364
|
-
for (const [id, { reject }] of this.pending.entries()) {
|
|
2365
|
-
reject(new Error("Sidecar restarted; please retry the operation"));
|
|
2366
|
-
this.pending.delete(id);
|
|
2367
|
-
}
|
|
2368
|
-
}).catch((err) => {
|
|
2369
|
-
for (const [id, { reject }] of this.pending.entries()) {
|
|
2370
|
-
reject(new Error(`Sidecar restart failed: ${err.message}. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar`));
|
|
2371
|
-
this.pending.delete(id);
|
|
2372
|
-
}
|
|
2373
|
-
});
|
|
2374
|
-
} else {
|
|
2375
|
-
for (const [id, { reject }] of this.pending.entries()) {
|
|
2376
|
-
reject(new Error("Python sidecar unavailable after restart attempt. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar"));
|
|
2377
|
-
this.pending.delete(id);
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
};
|
|
2382
|
-
var _singleton = null;
|
|
2383
|
-
function getSidecarClient() {
|
|
2384
|
-
if (!_singleton) {
|
|
2385
|
-
_singleton = new SidecarClientImpl();
|
|
2386
|
-
}
|
|
2387
|
-
return _singleton;
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
// apps/reaper-mcp-server/src/tools/aesthetics.ts
|
|
2391
|
-
var SIDECAR_NOT_INSTALLED_MSG = "Audio understanding sidecar not installed. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar";
|
|
2392
|
-
function registerAestheticsTools(server) {
|
|
2393
|
-
server.tool(
|
|
2394
|
-
"analyze_track_aesthetics",
|
|
2395
|
-
"Analyze the perceptual aesthetic quality of a track's audio using Meta's Audiobox Aesthetics model (CC-BY 4.0). Bounces the track to a temp WAV file (post-FX, post-fader) and runs 4-axis perceptual quality scoring: Production Quality (PQ), Production Complexity (PC), Content Enjoyment (CE), Content Usefulness (CU). Returns scores 0-10. Requires the Python sidecar (run: node dist/apps/reaper-mcp-server/main.js setup-sidecar).",
|
|
2396
|
-
{
|
|
2397
|
-
trackIndex: z17.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
2398
|
-
startTime: z17.coerce.number().min(0).optional().describe("Start time in seconds (default: 0 or use REAPER time selection)"),
|
|
2399
|
-
endTime: z17.coerce.number().min(0).optional().describe("End time in seconds (default: startTime + durationSeconds)"),
|
|
2400
|
-
durationSeconds: z17.coerce.number().min(0.5).max(30).optional().default(5).describe("Duration to analyze in seconds (default 5, max 30). Ignored if endTime is provided.")
|
|
2401
|
-
},
|
|
2402
|
-
async ({ trackIndex, startTime, endTime, durationSeconds }) => {
|
|
2403
|
-
const sidecar = getSidecarClient();
|
|
2404
|
-
if (!sidecar.isAvailable()) {
|
|
2405
|
-
return {
|
|
2406
|
-
content: [{ type: "text", text: SIDECAR_NOT_INSTALLED_MSG }],
|
|
2407
|
-
isError: true
|
|
2408
|
-
};
|
|
2409
|
-
}
|
|
2410
|
-
const resolvedStart = startTime ?? 0;
|
|
2411
|
-
const resolvedEnd = endTime ?? resolvedStart + (durationSeconds ?? 5);
|
|
2412
|
-
if (resolvedEnd <= resolvedStart) {
|
|
2413
|
-
return {
|
|
2414
|
-
content: [{ type: "text", text: "endTime must be greater than startTime" }],
|
|
2415
|
-
isError: true
|
|
2416
|
-
};
|
|
2417
|
-
}
|
|
2418
|
-
const commandId = randomUUID2();
|
|
2419
|
-
const renderRes = await sendCommand("render_track_to_wav", {
|
|
2420
|
-
trackIndex,
|
|
2421
|
-
startTime: resolvedStart,
|
|
2422
|
-
endTime: resolvedEnd,
|
|
2423
|
-
commandId
|
|
2424
|
-
});
|
|
2425
|
-
if (!renderRes.success) {
|
|
2426
|
-
return {
|
|
2427
|
-
content: [{ type: "text", text: `Render failed: ${renderRes.error}` }],
|
|
2428
|
-
isError: true
|
|
2429
|
-
};
|
|
2430
|
-
}
|
|
2431
|
-
const renderData = renderRes.data;
|
|
2432
|
-
const wavPath = renderData.wavPath;
|
|
2433
|
-
try {
|
|
2434
|
-
const scores = await sidecar.analyze(wavPath, resolvedStart, resolvedEnd);
|
|
2435
|
-
const result = {
|
|
2436
|
-
trackIndex,
|
|
2437
|
-
trackName: renderData.trackName,
|
|
2438
|
-
productionQuality: scores.PQ,
|
|
2439
|
-
productionComplexity: scores.PC,
|
|
2440
|
-
contentEnjoyment: scores.CE,
|
|
2441
|
-
contentUsefulness: scores.CU,
|
|
2442
|
-
startTime: resolvedStart,
|
|
2443
|
-
endTime: resolvedEnd,
|
|
2444
|
-
durationSeconds: resolvedEnd - resolvedStart,
|
|
2445
|
-
modelVersion: scores.modelVersion
|
|
2446
|
-
};
|
|
2447
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2448
|
-
} catch (err) {
|
|
2449
|
-
return {
|
|
2450
|
-
content: [{
|
|
2451
|
-
type: "text",
|
|
2452
|
-
text: err instanceof Error ? err.message : String(err)
|
|
2453
|
-
}],
|
|
2454
|
-
isError: true
|
|
2455
|
-
};
|
|
2456
|
-
} finally {
|
|
2457
|
-
await unlink2(wavPath).catch(() => {
|
|
2458
|
-
});
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
);
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
2235
|
// apps/reaper-mcp-server/src/server.ts
|
|
2465
2236
|
function instrumentToolHandlers(server) {
|
|
2466
2237
|
const originalTool = server.tool.bind(server);
|
|
@@ -2520,43 +2291,40 @@ function createServer() {
|
|
|
2520
2291
|
registerEnvelopeTools(server);
|
|
2521
2292
|
registerBatchTools(server);
|
|
2522
2293
|
registerCategoryTools(server);
|
|
2523
|
-
registerAestheticsTools(server);
|
|
2524
2294
|
return server;
|
|
2525
2295
|
}
|
|
2526
2296
|
|
|
2527
2297
|
// apps/reaper-mcp-server/src/main.ts
|
|
2528
|
-
import { existsSync as
|
|
2529
|
-
import { join as
|
|
2530
|
-
import { fileURLToPath as
|
|
2531
|
-
import { homedir as
|
|
2532
|
-
import { exec as execCb } from "node:child_process";
|
|
2533
|
-
import { promisify as promisifyUtil } from "node:util";
|
|
2298
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
2299
|
+
import { join as join5, dirname as dirname2 } from "node:path";
|
|
2300
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2301
|
+
import { homedir as homedir3 } from "node:os";
|
|
2534
2302
|
|
|
2535
2303
|
// apps/reaper-mcp-server/src/cli.ts
|
|
2536
|
-
import { copyFileSync, existsSync
|
|
2537
|
-
import { join as
|
|
2304
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2305
|
+
import { join as join3 } from "node:path";
|
|
2538
2306
|
function resolveAssetDir(baseDir, name) {
|
|
2539
|
-
const sibling =
|
|
2540
|
-
if (
|
|
2541
|
-
const parent =
|
|
2542
|
-
if (
|
|
2307
|
+
const sibling = join3(baseDir, name);
|
|
2308
|
+
if (existsSync(sibling)) return sibling;
|
|
2309
|
+
const parent = join3(baseDir, "..", name);
|
|
2310
|
+
if (existsSync(parent)) return parent;
|
|
2543
2311
|
let dir = baseDir;
|
|
2544
2312
|
for (let i = 0; i < 5; i++) {
|
|
2545
|
-
const candidate =
|
|
2546
|
-
if (
|
|
2547
|
-
const up =
|
|
2313
|
+
const candidate = join3(dir, name);
|
|
2314
|
+
if (existsSync(candidate)) return candidate;
|
|
2315
|
+
const up = join3(dir, "..");
|
|
2548
2316
|
if (up === dir) break;
|
|
2549
2317
|
dir = up;
|
|
2550
2318
|
}
|
|
2551
2319
|
return sibling;
|
|
2552
2320
|
}
|
|
2553
2321
|
function copyDirSync(src, dest) {
|
|
2554
|
-
if (!
|
|
2322
|
+
if (!existsSync(src)) return 0;
|
|
2555
2323
|
mkdirSync(dest, { recursive: true });
|
|
2556
2324
|
let count = 0;
|
|
2557
2325
|
for (const entry of readdirSync(src)) {
|
|
2558
|
-
const srcPath =
|
|
2559
|
-
const destPath =
|
|
2326
|
+
const srcPath = join3(src, entry);
|
|
2327
|
+
const destPath = join3(dest, entry);
|
|
2560
2328
|
if (statSync(srcPath).isDirectory()) {
|
|
2561
2329
|
count += copyDirSync(srcPath, destPath);
|
|
2562
2330
|
} else {
|
|
@@ -2567,14 +2335,14 @@ function copyDirSync(src, dest) {
|
|
|
2567
2335
|
return count;
|
|
2568
2336
|
}
|
|
2569
2337
|
function installFile(src, dest) {
|
|
2570
|
-
if (
|
|
2338
|
+
if (existsSync(src)) {
|
|
2571
2339
|
copyFileSync(src, dest);
|
|
2572
2340
|
return true;
|
|
2573
2341
|
}
|
|
2574
2342
|
return false;
|
|
2575
2343
|
}
|
|
2576
2344
|
function createMcpJson(targetPath) {
|
|
2577
|
-
if (
|
|
2345
|
+
if (existsSync(targetPath)) return false;
|
|
2578
2346
|
const config = JSON.stringify({
|
|
2579
2347
|
mcpServers: {
|
|
2580
2348
|
reaper: {
|
|
@@ -2592,6 +2360,7 @@ var REAPER_ASSETS = [
|
|
|
2592
2360
|
"mcp_lufs_meter.jsfx",
|
|
2593
2361
|
"mcp_correlation_meter.jsfx",
|
|
2594
2362
|
"mcp_crest_factor.jsfx",
|
|
2363
|
+
"mcp_midi_emitter.jsfx",
|
|
2595
2364
|
"mcp_snapshot_manager.lua",
|
|
2596
2365
|
"mcp_snapshot_lib.lua",
|
|
2597
2366
|
"mcp_snapshot_next.lua",
|
|
@@ -2652,6 +2421,9 @@ var MCP_TOOL_NAMES = [
|
|
|
2652
2421
|
"delete_midi_cc",
|
|
2653
2422
|
"get_midi_item_properties",
|
|
2654
2423
|
"set_midi_item_properties",
|
|
2424
|
+
"send_midi_cc",
|
|
2425
|
+
"send_midi_pc",
|
|
2426
|
+
"send_midi_note",
|
|
2655
2427
|
// media
|
|
2656
2428
|
"list_media_items",
|
|
2657
2429
|
"get_media_item_properties",
|
|
@@ -2697,16 +2469,12 @@ var MCP_TOOL_NAMES = [
|
|
|
2697
2469
|
// progressive discovery
|
|
2698
2470
|
"list_tool_categories",
|
|
2699
2471
|
"enable_tool_category",
|
|
2700
|
-
"disable_tool_category"
|
|
2701
|
-
// semantic audio analysis (requires Python sidecar — opt-in)
|
|
2702
|
-
"analyze_track_aesthetics"
|
|
2703
|
-
// Note: 'render_track_to_wav' is an internal Lua bridge command, NOT a public MCP tool.
|
|
2704
|
-
// It must NOT be listed here — MCP_TOOL_NAMES drives the Claude Code allowlist.
|
|
2472
|
+
"disable_tool_category"
|
|
2705
2473
|
];
|
|
2706
2474
|
function ensureClaudeSettings(settingsPath) {
|
|
2707
2475
|
const allowList = MCP_TOOL_NAMES.map((t) => `mcp__reaper__${t}`);
|
|
2708
|
-
if (!
|
|
2709
|
-
mkdirSync(
|
|
2476
|
+
if (!existsSync(settingsPath)) {
|
|
2477
|
+
mkdirSync(join3(settingsPath, ".."), { recursive: true });
|
|
2710
2478
|
const config = { permissions: { allow: allowList } };
|
|
2711
2479
|
writeFileSync(settingsPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2712
2480
|
return "created";
|
|
@@ -2723,21 +2491,21 @@ function ensureClaudeSettings(settingsPath) {
|
|
|
2723
2491
|
}
|
|
2724
2492
|
function resolveAssetDirWithFallback(baseDir, buildName, sourceName) {
|
|
2725
2493
|
const resolved = resolveAssetDir(baseDir, buildName);
|
|
2726
|
-
if (
|
|
2494
|
+
if (existsSync(resolved)) return resolved;
|
|
2727
2495
|
return resolveAssetDir(baseDir, sourceName);
|
|
2728
2496
|
}
|
|
2729
2497
|
|
|
2730
2498
|
// apps/reaper-mcp-server/src/init.ts
|
|
2731
2499
|
import { checkbox, select } from "@inquirer/prompts";
|
|
2732
|
-
import { existsSync as
|
|
2733
|
-
import { join as
|
|
2734
|
-
import { homedir as
|
|
2500
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
2501
|
+
import { join as join4 } from "node:path";
|
|
2502
|
+
import { homedir as homedir2 } from "node:os";
|
|
2735
2503
|
async function runInit(opts, dirResolver) {
|
|
2736
2504
|
const isTTY = Boolean(process.stdin.isTTY);
|
|
2737
2505
|
const headless = opts.yes || !isTTY;
|
|
2738
2506
|
console.log("REAPER MCP \u2014 Interactive Setup\n");
|
|
2739
2507
|
const bridgeDir = await ensureBridgeDir();
|
|
2740
|
-
console.log(`REAPER resource path: ${
|
|
2508
|
+
console.log(`REAPER resource path: ${join4(bridgeDir, "..", "..")}
|
|
2741
2509
|
`);
|
|
2742
2510
|
let selections;
|
|
2743
2511
|
if (headless) {
|
|
@@ -2782,27 +2550,27 @@ async function runInit(opts, dirResolver) {
|
|
|
2782
2550
|
skillsScope
|
|
2783
2551
|
};
|
|
2784
2552
|
}
|
|
2785
|
-
const
|
|
2553
|
+
const __dirname2 = dirResolver();
|
|
2786
2554
|
if (selections.bridge) {
|
|
2787
2555
|
console.log("Installing REAPER Bridge...");
|
|
2788
2556
|
const scriptsDir = getReaperScriptsPath();
|
|
2789
2557
|
mkdirSync2(scriptsDir, { recursive: true });
|
|
2790
|
-
const reaperDir = resolveAssetDir(
|
|
2558
|
+
const reaperDir = resolveAssetDir(__dirname2, "reaper");
|
|
2791
2559
|
for (const luaFile of ["mcp_bridge.lua", "mcp_snapshot_manager.lua"]) {
|
|
2792
|
-
const src =
|
|
2793
|
-
const dest =
|
|
2560
|
+
const src = join4(reaperDir, luaFile);
|
|
2561
|
+
const dest = join4(scriptsDir, luaFile);
|
|
2794
2562
|
if (installFile(src, dest)) {
|
|
2795
2563
|
console.log(` Installed: ${luaFile}`);
|
|
2796
2564
|
} else {
|
|
2797
2565
|
console.log(` Not found: ${src}`);
|
|
2798
2566
|
}
|
|
2799
2567
|
}
|
|
2800
|
-
const effectsDir =
|
|
2568
|
+
const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
|
|
2801
2569
|
mkdirSync2(effectsDir, { recursive: true });
|
|
2802
2570
|
for (const asset of REAPER_ASSETS) {
|
|
2803
2571
|
if (asset.endsWith(".lua")) continue;
|
|
2804
|
-
const src =
|
|
2805
|
-
const dest =
|
|
2572
|
+
const src = join4(reaperDir, asset);
|
|
2573
|
+
const dest = join4(effectsDir, asset);
|
|
2806
2574
|
if (installFile(src, dest)) {
|
|
2807
2575
|
console.log(` Installed: reaper-mcp/${asset}`);
|
|
2808
2576
|
} else {
|
|
@@ -2813,32 +2581,32 @@ async function runInit(opts, dirResolver) {
|
|
|
2813
2581
|
}
|
|
2814
2582
|
if (selections.skills) {
|
|
2815
2583
|
const isGlobal = selections.skillsScope === "global";
|
|
2816
|
-
const baseDir = isGlobal ?
|
|
2817
|
-
const claudeDir = isGlobal ? baseDir :
|
|
2584
|
+
const baseDir = isGlobal ? join4(homedir2(), ".claude") : process.cwd();
|
|
2585
|
+
const claudeDir = isGlobal ? baseDir : join4(baseDir, ".claude");
|
|
2818
2586
|
console.log(`Installing AI Skills & Agents (${selections.skillsScope})...`);
|
|
2819
|
-
const knowledgeSrc = resolveAssetDir(
|
|
2820
|
-
if (
|
|
2821
|
-
const dest =
|
|
2587
|
+
const knowledgeSrc = resolveAssetDir(__dirname2, "knowledge");
|
|
2588
|
+
if (existsSync2(knowledgeSrc)) {
|
|
2589
|
+
const dest = join4(isGlobal ? join4(homedir2(), ".claude") : process.cwd(), "knowledge");
|
|
2822
2590
|
const count = copyDirSync(knowledgeSrc, dest);
|
|
2823
2591
|
console.log(` Installed knowledge base: ${count} files`);
|
|
2824
2592
|
} else {
|
|
2825
2593
|
console.log(" Knowledge base not found in package. Skipping.");
|
|
2826
2594
|
}
|
|
2827
|
-
const rulesSrc = resolveAssetDirWithFallback(
|
|
2828
|
-
if (
|
|
2829
|
-
const dest =
|
|
2595
|
+
const rulesSrc = resolveAssetDirWithFallback(__dirname2, "claude-rules", join4(".claude", "rules"));
|
|
2596
|
+
if (existsSync2(rulesSrc)) {
|
|
2597
|
+
const dest = join4(claudeDir, "rules");
|
|
2830
2598
|
const count = copyDirSync(rulesSrc, dest);
|
|
2831
2599
|
console.log(` Installed Claude rules: ${count} files`);
|
|
2832
2600
|
}
|
|
2833
|
-
const skillsSrc = resolveAssetDirWithFallback(
|
|
2834
|
-
if (
|
|
2835
|
-
const dest =
|
|
2601
|
+
const skillsSrc = resolveAssetDirWithFallback(__dirname2, "claude-skills", join4(".claude", "skills"));
|
|
2602
|
+
if (existsSync2(skillsSrc)) {
|
|
2603
|
+
const dest = join4(claudeDir, "skills");
|
|
2836
2604
|
const count = copyDirSync(skillsSrc, dest);
|
|
2837
2605
|
console.log(` Installed Claude skills: ${count} files`);
|
|
2838
2606
|
}
|
|
2839
|
-
const agentsSrc = resolveAssetDirWithFallback(
|
|
2840
|
-
if (
|
|
2841
|
-
const dest =
|
|
2607
|
+
const agentsSrc = resolveAssetDirWithFallback(__dirname2, "claude-agents", join4(".claude", "agents"));
|
|
2608
|
+
if (existsSync2(agentsSrc)) {
|
|
2609
|
+
const dest = join4(claudeDir, "agents");
|
|
2842
2610
|
const count = copyDirSync(agentsSrc, dest);
|
|
2843
2611
|
console.log(` Installed Claude agents: ${count} files`);
|
|
2844
2612
|
}
|
|
@@ -2846,8 +2614,8 @@ async function runInit(opts, dirResolver) {
|
|
|
2846
2614
|
}
|
|
2847
2615
|
if (selections.settings) {
|
|
2848
2616
|
console.log("Configuring Claude Code Settings...");
|
|
2849
|
-
const settingsDir =
|
|
2850
|
-
const settingsPath =
|
|
2617
|
+
const settingsDir = join4(homedir2(), ".claude");
|
|
2618
|
+
const settingsPath = join4(settingsDir, "settings.json");
|
|
2851
2619
|
const result = ensureClaudeSettings(settingsPath);
|
|
2852
2620
|
if (result === "created") {
|
|
2853
2621
|
console.log(` Created: ${settingsPath}`);
|
|
@@ -2860,7 +2628,7 @@ async function runInit(opts, dirResolver) {
|
|
|
2860
2628
|
}
|
|
2861
2629
|
if (selections.projectConfig) {
|
|
2862
2630
|
console.log("Creating Project Config...");
|
|
2863
|
-
const mcpJsonPath =
|
|
2631
|
+
const mcpJsonPath = join4(process.cwd(), ".mcp.json");
|
|
2864
2632
|
if (createMcpJson(mcpJsonPath)) {
|
|
2865
2633
|
console.log(` Created: ${mcpJsonPath}`);
|
|
2866
2634
|
} else {
|
|
@@ -2871,18 +2639,18 @@ async function runInit(opts, dirResolver) {
|
|
|
2871
2639
|
console.log("Running system check...");
|
|
2872
2640
|
const bridgeRunning = await isBridgeRunning();
|
|
2873
2641
|
console.log(` Lua bridge: ${bridgeRunning ? "Connected" : "Not detected (start after REAPER is open)"}`);
|
|
2874
|
-
const globalClaudeDir =
|
|
2875
|
-
const localAgents =
|
|
2876
|
-
const globalAgents =
|
|
2642
|
+
const globalClaudeDir = join4(homedir2(), ".claude");
|
|
2643
|
+
const localAgents = existsSync2(join4(process.cwd(), ".claude", "agents"));
|
|
2644
|
+
const globalAgents = existsSync2(join4(globalClaudeDir, "agents"));
|
|
2877
2645
|
const agentsExist = localAgents || globalAgents;
|
|
2878
2646
|
console.log(` Mix agents: ${agentsExist ? "Installed" : "Not installed"}`);
|
|
2879
|
-
const mcpJsonExists =
|
|
2647
|
+
const mcpJsonExists = existsSync2(join4(process.cwd(), ".mcp.json"));
|
|
2880
2648
|
console.log(` MCP config: ${mcpJsonExists ? ".mcp.json found" : ".mcp.json not present"}`);
|
|
2881
2649
|
console.log("");
|
|
2882
2650
|
console.log("Setup complete! Next steps:");
|
|
2883
2651
|
if (selections.bridge) {
|
|
2884
2652
|
const scriptsDir = getReaperScriptsPath();
|
|
2885
|
-
const luaDest =
|
|
2653
|
+
const luaDest = join4(scriptsDir, "mcp_bridge.lua");
|
|
2886
2654
|
console.log(" 1. Open REAPER");
|
|
2887
2655
|
console.log(" 2. Actions > Show action list > Load ReaScript");
|
|
2888
2656
|
console.log(` 3. Select: ${luaDest}`);
|
|
@@ -2897,206 +2665,33 @@ async function runInit(opts, dirResolver) {
|
|
|
2897
2665
|
}
|
|
2898
2666
|
}
|
|
2899
2667
|
|
|
2900
|
-
// apps/reaper-mcp-server/src/setup-sidecar.ts
|
|
2901
|
-
import { exec as execImpl } from "node:child_process";
|
|
2902
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2, rmSync } from "node:fs";
|
|
2903
|
-
import { join as join6, dirname as dirname2 } from "node:path";
|
|
2904
|
-
import { homedir as homedir4, platform as platform3 } from "node:os";
|
|
2905
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2906
|
-
var VENV_BIN2 = platform3() === "win32" ? "Scripts" : "bin";
|
|
2907
|
-
var VENV_PYTHON_NAME2 = platform3() === "win32" ? "python.exe" : "python";
|
|
2908
|
-
var VENV_PIP_NAME = platform3() === "win32" ? "pip.exe" : "pip";
|
|
2909
|
-
function exec(cmd) {
|
|
2910
|
-
return new Promise((resolve, reject) => {
|
|
2911
|
-
execImpl(cmd, (err, stdout, stderr) => {
|
|
2912
|
-
if (err) reject(err);
|
|
2913
|
-
else resolve({ stdout, stderr });
|
|
2914
|
-
});
|
|
2915
|
-
});
|
|
2916
|
-
}
|
|
2917
|
-
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
2918
|
-
var SIDECAR_VENV_PATH2 = join6(homedir4(), ".reaper-mcp", "sidecar-venv");
|
|
2919
|
-
var SIDECAR_DIR = join6(homedir4(), ".reaper-mcp", "sidecar");
|
|
2920
|
-
var SIDECAR_SCRIPT_DEST = join6(SIDECAR_DIR, "server.py");
|
|
2921
|
-
var MODEL_ID = "facebook/audiobox-aesthetics";
|
|
2922
|
-
var MIN_PYTHON_MAJOR = 3;
|
|
2923
|
-
var MIN_PYTHON_MINOR = 10;
|
|
2924
|
-
function getPythonBin() {
|
|
2925
|
-
return process.env["PYTHON_BIN"] ?? "python3";
|
|
2926
|
-
}
|
|
2927
|
-
function parsePythonVersion(output) {
|
|
2928
|
-
const match = output.match(/Python\s+(\d+)\.(\d+)\.(\d+)/i);
|
|
2929
|
-
if (!match) return null;
|
|
2930
|
-
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
|
|
2931
|
-
}
|
|
2932
|
-
async function checkPython(pythonBin) {
|
|
2933
|
-
let stdout;
|
|
2934
|
-
try {
|
|
2935
|
-
const result = await exec(`"${pythonBin}" --version`);
|
|
2936
|
-
stdout = (result.stdout + result.stderr).trim();
|
|
2937
|
-
} catch {
|
|
2938
|
-
const notFound = `Python interpreter not found: ${pythonBin}
|
|
2939
|
-
Install Python 3.10+ from https://python.org or set the PYTHON_BIN environment variable.
|
|
2940
|
-
Example: PYTHON_BIN=/usr/local/bin/python3.11 node dist/apps/reaper-mcp-server/main.js setup-sidecar`;
|
|
2941
|
-
throw new Error(notFound);
|
|
2942
|
-
}
|
|
2943
|
-
const version = parsePythonVersion(stdout);
|
|
2944
|
-
if (!version) {
|
|
2945
|
-
throw new Error(`Could not parse Python version from: "${stdout}". Is PYTHON_BIN set correctly?`);
|
|
2946
|
-
}
|
|
2947
|
-
const [major, minor, patch] = version;
|
|
2948
|
-
if (major < MIN_PYTHON_MAJOR || major === MIN_PYTHON_MAJOR && minor < MIN_PYTHON_MINOR) {
|
|
2949
|
-
throw new Error(
|
|
2950
|
-
`Python ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR}+ required. Found: ${major}.${minor}.${patch} at ${pythonBin}.
|
|
2951
|
-
Install Python 3.10+ from https://python.org or set PYTHON_BIN to a newer interpreter.`
|
|
2952
|
-
);
|
|
2953
|
-
}
|
|
2954
|
-
return `${major}.${minor}.${patch}`;
|
|
2955
|
-
}
|
|
2956
|
-
async function createVenv(pythonBin) {
|
|
2957
|
-
mkdirSync3(join6(homedir4(), ".reaper-mcp"), { recursive: true });
|
|
2958
|
-
await exec(`"${pythonBin}" -m venv --clear "${SIDECAR_VENV_PATH2}"`);
|
|
2959
|
-
}
|
|
2960
|
-
function cleanupBrokenVenv() {
|
|
2961
|
-
try {
|
|
2962
|
-
if (existsSync4(SIDECAR_VENV_PATH2)) {
|
|
2963
|
-
rmSync(SIDECAR_VENV_PATH2, { recursive: true, force: true });
|
|
2964
|
-
}
|
|
2965
|
-
} catch {
|
|
2966
|
-
}
|
|
2967
|
-
}
|
|
2968
|
-
function resolveRequirementsTxt(baseDir) {
|
|
2969
|
-
const sidecarDir = resolveAssetDir(baseDir, "sidecar");
|
|
2970
|
-
return join6(sidecarDir, "requirements.txt");
|
|
2971
|
-
}
|
|
2972
|
-
async function installDeps(requirementsPath) {
|
|
2973
|
-
const pip = join6(SIDECAR_VENV_PATH2, VENV_BIN2, VENV_PIP_NAME);
|
|
2974
|
-
await exec(`"${pip}" install --upgrade pip`);
|
|
2975
|
-
const { stdout, stderr } = await exec(`"${pip}" install -r "${requirementsPath}"`);
|
|
2976
|
-
if (stdout) process.stdout.write(stdout);
|
|
2977
|
-
if (stderr) process.stderr.write(stderr);
|
|
2978
|
-
}
|
|
2979
|
-
async function downloadModelWeights() {
|
|
2980
|
-
const python = join6(SIDECAR_VENV_PATH2, VENV_BIN2, VENV_PYTHON_NAME2);
|
|
2981
|
-
const script = [
|
|
2982
|
-
"from huggingface_hub import snapshot_download",
|
|
2983
|
-
`print("Downloading ${MODEL_ID} weights to ~/.cache/huggingface/hub/ ...")`,
|
|
2984
|
-
`snapshot_download("${MODEL_ID}")`,
|
|
2985
|
-
`print("Download complete.")`
|
|
2986
|
-
].join("; ");
|
|
2987
|
-
const { stdout, stderr } = await exec(`"${python}" -c "${script}"`);
|
|
2988
|
-
if (stdout) process.stdout.write(stdout);
|
|
2989
|
-
if (stderr) process.stderr.write(stderr);
|
|
2990
|
-
}
|
|
2991
|
-
function copySidecarScript(baseDir) {
|
|
2992
|
-
const sidecarDir = resolveAssetDir(baseDir, "sidecar");
|
|
2993
|
-
const src = join6(sidecarDir, "server.py");
|
|
2994
|
-
if (!existsSync4(src)) {
|
|
2995
|
-
throw new Error(
|
|
2996
|
-
`sidecar/server.py not found at: ${src}
|
|
2997
|
-
Run \`pnpm nx build reaper-mcp-server\` first.`
|
|
2998
|
-
);
|
|
2999
|
-
}
|
|
3000
|
-
mkdirSync3(SIDECAR_DIR, { recursive: true });
|
|
3001
|
-
copyFileSync2(src, SIDECAR_SCRIPT_DEST);
|
|
3002
|
-
}
|
|
3003
|
-
async function setupSidecar() {
|
|
3004
|
-
console.log("REAPER MCP \u2014 Setup Python Sidecar\n");
|
|
3005
|
-
console.log("This installs the opt-in audio AI subsystem for perceptual analysis.");
|
|
3006
|
-
console.log("Requires Python 3.10+ and an internet connection (~831 MB download).\n");
|
|
3007
|
-
const pythonBin = getPythonBin();
|
|
3008
|
-
console.log(`Checking Python interpreter: ${pythonBin}`);
|
|
3009
|
-
let version;
|
|
3010
|
-
try {
|
|
3011
|
-
version = await checkPython(pythonBin);
|
|
3012
|
-
} catch (err) {
|
|
3013
|
-
console.error(`
|
|
3014
|
-
Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3015
|
-
process.exit(1);
|
|
3016
|
-
}
|
|
3017
|
-
console.log(` Using Python interpreter: ${pythonBin} (${version})`);
|
|
3018
|
-
console.log(`
|
|
3019
|
-
Creating virtual environment at: ${SIDECAR_VENV_PATH2}`);
|
|
3020
|
-
try {
|
|
3021
|
-
await createVenv(pythonBin);
|
|
3022
|
-
console.log(" Virtual environment created.");
|
|
3023
|
-
} catch (err) {
|
|
3024
|
-
console.error(`
|
|
3025
|
-
Failed to create venv: ${err instanceof Error ? err.message : String(err)}`);
|
|
3026
|
-
process.exit(1);
|
|
3027
|
-
}
|
|
3028
|
-
const requirementsPath = resolveRequirementsTxt(__dirname);
|
|
3029
|
-
console.log(`
|
|
3030
|
-
Installing Python dependencies from: ${requirementsPath}`);
|
|
3031
|
-
if (!existsSync4(requirementsPath)) {
|
|
3032
|
-
console.error(`
|
|
3033
|
-
Error: requirements.txt not found at: ${requirementsPath}`);
|
|
3034
|
-
console.error("Run `pnpm nx build reaper-mcp-server` first.");
|
|
3035
|
-
process.exit(1);
|
|
3036
|
-
}
|
|
3037
|
-
try {
|
|
3038
|
-
await installDeps(requirementsPath);
|
|
3039
|
-
console.log(" Dependencies installed.");
|
|
3040
|
-
} catch (err) {
|
|
3041
|
-
console.error(`
|
|
3042
|
-
Failed to install dependencies: ${err instanceof Error ? err.message : String(err)}`);
|
|
3043
|
-
cleanupBrokenVenv();
|
|
3044
|
-
console.error("Removed partial venv so the next setup-sidecar run starts clean.");
|
|
3045
|
-
process.exit(1);
|
|
3046
|
-
}
|
|
3047
|
-
console.log("\nPre-downloading Audiobox Aesthetics model weights (~831 MB)...");
|
|
3048
|
-
console.log("This may take several minutes depending on your internet connection.");
|
|
3049
|
-
try {
|
|
3050
|
-
await downloadModelWeights();
|
|
3051
|
-
} catch (err) {
|
|
3052
|
-
console.error(`
|
|
3053
|
-
Model download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3054
|
-
console.error("Check your internet connection and run setup-sidecar again.");
|
|
3055
|
-
process.exit(1);
|
|
3056
|
-
}
|
|
3057
|
-
console.log(`
|
|
3058
|
-
Installing sidecar script to: ${SIDECAR_SCRIPT_DEST}`);
|
|
3059
|
-
try {
|
|
3060
|
-
copySidecarScript(__dirname);
|
|
3061
|
-
console.log(" Sidecar script installed.");
|
|
3062
|
-
} catch (err) {
|
|
3063
|
-
console.error(`
|
|
3064
|
-
Failed to install sidecar script: ${err instanceof Error ? err.message : String(err)}`);
|
|
3065
|
-
process.exit(1);
|
|
3066
|
-
}
|
|
3067
|
-
console.log("\nSidecar setup complete!\n");
|
|
3068
|
-
console.log("The analyze_track_aesthetics tool is now available.");
|
|
3069
|
-
console.log("Run `node dist/apps/reaper-mcp-server/main.js doctor` to verify.\n");
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
2668
|
// apps/reaper-mcp-server/src/main.ts
|
|
3073
|
-
var
|
|
3074
|
-
var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
|
|
2669
|
+
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
3075
2670
|
async function setup() {
|
|
3076
2671
|
console.log("REAPER MCP Server \u2014 Setup\n");
|
|
3077
2672
|
const bridgeDir = await ensureBridgeDir();
|
|
3078
2673
|
console.log(`Bridge directory: ${bridgeDir}
|
|
3079
2674
|
`);
|
|
3080
2675
|
const scriptsDir = getReaperScriptsPath();
|
|
3081
|
-
|
|
3082
|
-
const reaperDir = resolveAssetDir(
|
|
2676
|
+
mkdirSync3(scriptsDir, { recursive: true });
|
|
2677
|
+
const reaperDir = resolveAssetDir(__dirname, "reaper");
|
|
3083
2678
|
console.log("Installing Lua scripts...");
|
|
3084
2679
|
for (const luaFile of ["mcp_bridge.lua", "mcp_snapshot_manager.lua"]) {
|
|
3085
|
-
const src =
|
|
3086
|
-
const dest =
|
|
2680
|
+
const src = join5(reaperDir, luaFile);
|
|
2681
|
+
const dest = join5(scriptsDir, luaFile);
|
|
3087
2682
|
if (installFile(src, dest)) {
|
|
3088
2683
|
console.log(` Installed: ${luaFile}`);
|
|
3089
2684
|
} else {
|
|
3090
2685
|
console.log(` Not found: ${src}`);
|
|
3091
2686
|
}
|
|
3092
2687
|
}
|
|
3093
|
-
const effectsDir =
|
|
3094
|
-
|
|
2688
|
+
const effectsDir = join5(getReaperEffectsPath(), "reaper-mcp");
|
|
2689
|
+
mkdirSync3(effectsDir, { recursive: true });
|
|
3095
2690
|
console.log("\nInstalling JSFX analyzers...");
|
|
3096
2691
|
for (const asset of REAPER_ASSETS) {
|
|
3097
2692
|
if (asset.endsWith(".lua")) continue;
|
|
3098
|
-
const src =
|
|
3099
|
-
const dest =
|
|
2693
|
+
const src = join5(reaperDir, asset);
|
|
2694
|
+
const dest = join5(effectsDir, asset);
|
|
3100
2695
|
if (installFile(src, dest)) {
|
|
3101
2696
|
console.log(` Installed: reaper-mcp/${asset}`);
|
|
3102
2697
|
} else {
|
|
@@ -3107,7 +2702,7 @@ async function setup() {
|
|
|
3107
2702
|
console.log("Next steps:");
|
|
3108
2703
|
console.log(" 1. Open REAPER");
|
|
3109
2704
|
console.log(" 2. Actions > Show action list > Load ReaScript");
|
|
3110
|
-
console.log(` 3. Select: ${
|
|
2705
|
+
console.log(` 3. Select: ${join5(scriptsDir, "mcp_bridge.lua")}`);
|
|
3111
2706
|
console.log(" 4. Run the script (it will keep running in background via defer loop)");
|
|
3112
2707
|
console.log(" 5. Add reaper-mcp to your Claude Code config (see: npx @mthines/reaper-mcp doctor)");
|
|
3113
2708
|
}
|
|
@@ -3119,41 +2714,41 @@ async function installSkills(scope) {
|
|
|
3119
2714
|
console.log(`REAPER MCP \u2014 Install AI Mix Engineer Skills (scope: ${scope})
|
|
3120
2715
|
`);
|
|
3121
2716
|
const isGlobal = scope === "global";
|
|
3122
|
-
const baseDir = isGlobal ?
|
|
3123
|
-
const claudeDir = isGlobal ? baseDir :
|
|
3124
|
-
const knowledgeSrc = resolveAssetDir(
|
|
3125
|
-
if (
|
|
3126
|
-
const dest =
|
|
2717
|
+
const baseDir = isGlobal ? join5(homedir3(), ".claude") : process.cwd();
|
|
2718
|
+
const claudeDir = isGlobal ? baseDir : join5(baseDir, ".claude");
|
|
2719
|
+
const knowledgeSrc = resolveAssetDir(__dirname, "knowledge");
|
|
2720
|
+
if (existsSync3(knowledgeSrc)) {
|
|
2721
|
+
const dest = join5(baseDir, "knowledge");
|
|
3127
2722
|
const count = copyDirSync(knowledgeSrc, dest);
|
|
3128
2723
|
console.log(`Installed knowledge base: ${count} files \u2192 ${dest}`);
|
|
3129
2724
|
} else {
|
|
3130
2725
|
console.log("Knowledge base not found in package. Skipping.");
|
|
3131
2726
|
}
|
|
3132
|
-
const rulesSrc = resolveAssetDirWithFallback(
|
|
3133
|
-
if (
|
|
3134
|
-
const dest =
|
|
2727
|
+
const rulesSrc = resolveAssetDirWithFallback(__dirname, "claude-rules", join5(".claude", "rules"));
|
|
2728
|
+
if (existsSync3(rulesSrc)) {
|
|
2729
|
+
const dest = join5(claudeDir, "rules");
|
|
3135
2730
|
const count = copyDirSync(rulesSrc, dest);
|
|
3136
2731
|
console.log(`Installed Claude rules: ${count} files \u2192 ${dest}`);
|
|
3137
2732
|
} else {
|
|
3138
2733
|
console.log("Claude rules not found in package. Skipping.");
|
|
3139
2734
|
}
|
|
3140
|
-
const skillsSrc = resolveAssetDirWithFallback(
|
|
3141
|
-
if (
|
|
3142
|
-
const dest =
|
|
2735
|
+
const skillsSrc = resolveAssetDirWithFallback(__dirname, "claude-skills", join5(".claude", "skills"));
|
|
2736
|
+
if (existsSync3(skillsSrc)) {
|
|
2737
|
+
const dest = join5(claudeDir, "skills");
|
|
3143
2738
|
const count = copyDirSync(skillsSrc, dest);
|
|
3144
2739
|
console.log(`Installed Claude skills: ${count} files \u2192 ${dest}`);
|
|
3145
2740
|
} else {
|
|
3146
2741
|
console.log("Claude skills not found in package. Skipping.");
|
|
3147
2742
|
}
|
|
3148
|
-
const agentsSrc = resolveAssetDirWithFallback(
|
|
3149
|
-
if (
|
|
3150
|
-
const dest =
|
|
2743
|
+
const agentsSrc = resolveAssetDirWithFallback(__dirname, "claude-agents", join5(".claude", "agents"));
|
|
2744
|
+
if (existsSync3(agentsSrc)) {
|
|
2745
|
+
const dest = join5(claudeDir, "agents");
|
|
3151
2746
|
const count = copyDirSync(agentsSrc, dest);
|
|
3152
2747
|
console.log(`Installed Claude agents: ${count} files \u2192 ${dest}`);
|
|
3153
2748
|
} else {
|
|
3154
2749
|
console.log("Claude agents not found in package. Skipping.");
|
|
3155
2750
|
}
|
|
3156
|
-
const settingsPath =
|
|
2751
|
+
const settingsPath = join5(claudeDir, "settings.json");
|
|
3157
2752
|
const result = ensureClaudeSettings(settingsPath);
|
|
3158
2753
|
if (result === "created") {
|
|
3159
2754
|
console.log(`Created Claude settings: ${settingsPath}`);
|
|
@@ -3163,7 +2758,7 @@ async function installSkills(scope) {
|
|
|
3163
2758
|
console.log(`Claude settings already has all REAPER tools: ${settingsPath}`);
|
|
3164
2759
|
}
|
|
3165
2760
|
if (!isGlobal) {
|
|
3166
|
-
const mcpJsonPath =
|
|
2761
|
+
const mcpJsonPath = join5(baseDir, ".mcp.json");
|
|
3167
2762
|
if (createMcpJson(mcpJsonPath)) {
|
|
3168
2763
|
console.log(`
|
|
3169
2764
|
Created: ${mcpJsonPath}`);
|
|
@@ -3184,92 +2779,31 @@ async function doctor() {
|
|
|
3184
2779
|
if (!bridgeRunning) {
|
|
3185
2780
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp setup" then load mcp_bridge.lua in REAPER');
|
|
3186
2781
|
}
|
|
3187
|
-
const globalClaudeDir =
|
|
3188
|
-
const localAgents =
|
|
3189
|
-
const globalAgents =
|
|
2782
|
+
const globalClaudeDir = join5(homedir3(), ".claude");
|
|
2783
|
+
const localAgents = existsSync3(join5(process.cwd(), ".claude", "agents"));
|
|
2784
|
+
const globalAgents = existsSync3(join5(globalClaudeDir, "agents"));
|
|
3190
2785
|
const agentsExist = localAgents || globalAgents;
|
|
3191
2786
|
const agentsLocation = localAgents ? ".claude/agents/" : globalAgents ? "~/.claude/agents/" : "";
|
|
3192
2787
|
console.log(`Mix agents: ${agentsExist ? `\u2713 Found (${agentsLocation})` : "\u2717 Not installed"}`);
|
|
3193
2788
|
if (!agentsExist) {
|
|
3194
2789
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
3195
2790
|
}
|
|
3196
|
-
const localKnowledge =
|
|
3197
|
-
const globalKnowledge =
|
|
2791
|
+
const localKnowledge = existsSync3(join5(process.cwd(), "knowledge"));
|
|
2792
|
+
const globalKnowledge = existsSync3(join5(globalClaudeDir, "knowledge"));
|
|
3198
2793
|
const knowledgeExists = localKnowledge || globalKnowledge;
|
|
3199
2794
|
const knowledgeLocation = localKnowledge ? "project" : globalKnowledge ? "~/.claude/" : "";
|
|
3200
2795
|
console.log(`Knowledge base: ${knowledgeExists ? `\u2713 Found (${knowledgeLocation})` : "\u2717 Not installed"}`);
|
|
3201
2796
|
if (!knowledgeExists) {
|
|
3202
2797
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
3203
2798
|
}
|
|
3204
|
-
const mcpJsonExists =
|
|
2799
|
+
const mcpJsonExists = existsSync3(join5(process.cwd(), ".mcp.json"));
|
|
3205
2800
|
console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
|
|
3206
2801
|
if (!mcpJsonExists) {
|
|
3207
2802
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills --project" to create .mcp.json');
|
|
3208
2803
|
}
|
|
3209
2804
|
console.log('\nTo check SWS Extensions, start REAPER and use the "list_available_fx" tool.');
|
|
3210
2805
|
console.log("SWS provides enhanced plugin discovery and snapshot support.\n");
|
|
3211
|
-
|
|
3212
|
-
const sidecarVenvPath = join7(homedir5(), ".reaper-mcp", "sidecar-venv");
|
|
3213
|
-
const venvBin = platform4() === "win32" ? "Scripts" : "bin";
|
|
3214
|
-
const venvPythonName = platform4() === "win32" ? "python.exe" : "python";
|
|
3215
|
-
const venvPython = join7(sidecarVenvPath, venvBin, venvPythonName);
|
|
3216
|
-
const hfCachePath = join7(homedir5(), ".cache", "huggingface", "hub");
|
|
3217
|
-
let pythonOk = false;
|
|
3218
|
-
let pythonDetail = "not found";
|
|
3219
|
-
try {
|
|
3220
|
-
const pythonBin = process.env["PYTHON_BIN"] ?? "python3";
|
|
3221
|
-
const { stdout, stderr } = await execAsync(`"${pythonBin}" --version`);
|
|
3222
|
-
const versionStr = (stdout + stderr).trim();
|
|
3223
|
-
const match = versionStr.match(/Python\s+(\d+)\.(\d+)/i);
|
|
3224
|
-
if (match) {
|
|
3225
|
-
const major = parseInt(match[1], 10);
|
|
3226
|
-
const minor = parseInt(match[2], 10);
|
|
3227
|
-
pythonOk = major > 3 || major === 3 && minor >= 10;
|
|
3228
|
-
pythonDetail = versionStr;
|
|
3229
|
-
}
|
|
3230
|
-
} catch {
|
|
3231
|
-
pythonDetail = "not found";
|
|
3232
|
-
}
|
|
3233
|
-
console.log(` Python \u2265 3.10: ${pythonOk ? `\u2713 ${pythonDetail}` : `\u2717 ${pythonDetail}`}`);
|
|
3234
|
-
if (!pythonOk) {
|
|
3235
|
-
console.log(" \u2192 Install Python 3.10+ from https://python.org or set PYTHON_BIN");
|
|
3236
|
-
}
|
|
3237
|
-
const venvExists = existsSync5(sidecarVenvPath);
|
|
3238
|
-
console.log(` Venv: ${venvExists ? `\u2713 ${sidecarVenvPath}` : "\u2717 Not installed"}`);
|
|
3239
|
-
if (!venvExists) {
|
|
3240
|
-
console.log(" \u2192 Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar");
|
|
3241
|
-
}
|
|
3242
|
-
let depsOk = false;
|
|
3243
|
-
if (venvExists && existsSync5(venvPython)) {
|
|
3244
|
-
try {
|
|
3245
|
-
await execAsync(`"${venvPython}" -c "import audiobox_aesthetics"`);
|
|
3246
|
-
depsOk = true;
|
|
3247
|
-
} catch {
|
|
3248
|
-
depsOk = false;
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3251
|
-
console.log(` Dependencies: ${depsOk ? "\u2713 audiobox-aesthetics importable" : "\u2717 Not installed"}`);
|
|
3252
|
-
if (!depsOk && venvExists) {
|
|
3253
|
-
console.log(" \u2192 Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar");
|
|
3254
|
-
}
|
|
3255
|
-
const weightsPath = join7(hfCachePath, "models--facebook--audiobox-aesthetics");
|
|
3256
|
-
const weightsExist = existsSync5(weightsPath);
|
|
3257
|
-
console.log(` Model weights: ${weightsExist ? `\u2713 ${weightsPath}` : "\u2717 Not downloaded"}`);
|
|
3258
|
-
if (!weightsExist) {
|
|
3259
|
-
console.log(" \u2192 Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar");
|
|
3260
|
-
}
|
|
3261
|
-
const sidecarReady = venvExists && depsOk && weightsExist;
|
|
3262
|
-
const sidecarOptedIn = venvExists || weightsExist;
|
|
3263
|
-
const sidecarBroken = sidecarOptedIn && !sidecarReady;
|
|
3264
|
-
if (!sidecarOptedIn) {
|
|
3265
|
-
console.log("\n Sidecar: not installed (opt-in). Run setup-sidecar to enable audio AI tools.");
|
|
3266
|
-
} else if (sidecarBroken) {
|
|
3267
|
-
console.log("\n Sidecar: PARTIALLY INSTALLED \u2014 run setup-sidecar to repair.");
|
|
3268
|
-
} else {
|
|
3269
|
-
console.log("\n Sidecar: fully installed and ready.");
|
|
3270
|
-
}
|
|
3271
|
-
console.log("");
|
|
3272
|
-
process.exit(bridgeRunning && knowledgeExists && mcpJsonExists && !sidecarBroken ? 0 : 1);
|
|
2806
|
+
process.exit(bridgeRunning && knowledgeExists && mcpJsonExists ? 0 : 1);
|
|
3273
2807
|
}
|
|
3274
2808
|
async function serve() {
|
|
3275
2809
|
const log = (...args) => console.error("[reaper-mcp]", ...args);
|
|
@@ -3278,7 +2812,7 @@ async function serve() {
|
|
|
3278
2812
|
startDiagnosticsPoller();
|
|
3279
2813
|
startEventsPoller();
|
|
3280
2814
|
log("Starting REAPER MCP Server...");
|
|
3281
|
-
log(`Entry: ${
|
|
2815
|
+
log(`Entry: ${fileURLToPath2(import.meta.url)}`);
|
|
3282
2816
|
const tracer = getTracer();
|
|
3283
2817
|
await tracer.startActiveSpan("mcp.server.startup", { kind: SpanKind3.INTERNAL }, async (startupSpan) => {
|
|
3284
2818
|
try {
|
|
@@ -3332,7 +2866,7 @@ switch (command) {
|
|
|
3332
2866
|
case "init":
|
|
3333
2867
|
runInit(
|
|
3334
2868
|
{ yes: hasYesFlag, project: hasProjectFlag },
|
|
3335
|
-
() =>
|
|
2869
|
+
() => __dirname
|
|
3336
2870
|
).catch((err) => {
|
|
3337
2871
|
console.error("Init failed:", err);
|
|
3338
2872
|
process.exit(1);
|
|
@@ -3356,12 +2890,6 @@ switch (command) {
|
|
|
3356
2890
|
process.exit(1);
|
|
3357
2891
|
});
|
|
3358
2892
|
break;
|
|
3359
|
-
case "setup-sidecar":
|
|
3360
|
-
setupSidecar().catch((err) => {
|
|
3361
|
-
console.error("Sidecar setup failed:", err);
|
|
3362
|
-
process.exit(1);
|
|
3363
|
-
});
|
|
3364
|
-
break;
|
|
3365
2893
|
case "status": {
|
|
3366
2894
|
(async () => {
|
|
3367
2895
|
const running = await isBridgeRunning();
|
|
@@ -3387,7 +2915,6 @@ Usage:
|
|
|
3387
2915
|
npx @mthines/reaper-mcp init --yes Non-interactive setup (install everything with defaults)
|
|
3388
2916
|
npx @mthines/reaper-mcp init --project Include .mcp.json in current directory
|
|
3389
2917
|
npx @mthines/reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
|
|
3390
|
-
npx @mthines/reaper-mcp setup-sidecar Install Python sidecar for perceptual audio analysis (opt-in, ~831 MB)
|
|
3391
2918
|
npx @mthines/reaper-mcp install-skills Install AI knowledge + agents globally (default)
|
|
3392
2919
|
npx @mthines/reaper-mcp install-skills --project Install into current project directory
|
|
3393
2920
|
npx @mthines/reaper-mcp install-skills --global Install into ~/.claude/ (default)
|
|
@@ -3409,31 +2936,24 @@ Tip: install globally for shorter commands:
|
|
|
3409
2936
|
`);
|
|
3410
2937
|
break;
|
|
3411
2938
|
}
|
|
3412
|
-
function shutdownAll() {
|
|
3413
|
-
stopPollers();
|
|
3414
|
-
try {
|
|
3415
|
-
getSidecarClient().shutdown();
|
|
3416
|
-
} catch {
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
2939
|
process.on("SIGINT", () => {
|
|
3420
2940
|
console.error("[reaper-mcp] Interrupted");
|
|
3421
|
-
|
|
2941
|
+
stopPollers();
|
|
3422
2942
|
shutdownTelemetry().finally(() => process.exit(0));
|
|
3423
2943
|
});
|
|
3424
2944
|
process.on("SIGTERM", () => {
|
|
3425
2945
|
console.error("[reaper-mcp] Terminated");
|
|
3426
|
-
|
|
2946
|
+
stopPollers();
|
|
3427
2947
|
shutdownTelemetry().finally(() => process.exit(0));
|
|
3428
2948
|
});
|
|
3429
2949
|
process.on("uncaughtException", (err) => {
|
|
3430
2950
|
console.error("[reaper-mcp] Uncaught exception:", err);
|
|
3431
|
-
|
|
2951
|
+
stopPollers();
|
|
3432
2952
|
shutdownTelemetry().finally(() => process.exit(1));
|
|
3433
2953
|
});
|
|
3434
2954
|
process.on("unhandledRejection", (reason) => {
|
|
3435
2955
|
console.error("[reaper-mcp] Unhandled rejection:", reason);
|
|
3436
|
-
|
|
2956
|
+
stopPollers();
|
|
3437
2957
|
shutdownTelemetry().finally(() => process.exit(1));
|
|
3438
2958
|
});
|
|
3439
2959
|
//# sourceMappingURL=main.js.map
|