livepilot 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +10 -0
- package/.mcpregistry_github_token +1 -0
- package/.mcpregistry_registry_token +1 -0
- package/.playwright-mcp/console-2026-03-17T15-47-29-021Z.log +10 -0
- package/.playwright-mcp/console-2026-03-17T15-51-09-247Z.log +10 -0
- package/.playwright-mcp/console-2026-03-17T15-52-22-831Z.log +12 -0
- package/.playwright-mcp/console-2026-03-17T15-52-29-709Z.log +10 -0
- package/.playwright-mcp/console-2026-03-17T15-53-20-147Z.log +1 -0
- package/.playwright-mcp/glama-snapshot.md +2140 -0
- package/.playwright-mcp/page-2026-03-17T15-49-02-625Z.png +0 -0
- package/.playwright-mcp/page-2026-03-17T15-52-15-149Z.png +0 -0
- package/.playwright-mcp/page-2026-03-17T15-52-57-333Z.png +0 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/bin/livepilot.js +376 -0
- package/installer/install.js +95 -0
- package/installer/paths.js +79 -0
- package/mcp_server/__init__.py +2 -0
- package/mcp_server/__main__.py +5 -0
- package/mcp_server/connection.py +207 -0
- package/mcp_server/server.py +40 -0
- package/mcp_server/tools/__init__.py +1 -0
- package/mcp_server/tools/arrangement.py +399 -0
- package/mcp_server/tools/browser.py +78 -0
- package/mcp_server/tools/clips.py +187 -0
- package/mcp_server/tools/devices.py +238 -0
- package/mcp_server/tools/mixing.py +113 -0
- package/mcp_server/tools/notes.py +266 -0
- package/mcp_server/tools/scenes.py +63 -0
- package/mcp_server/tools/tracks.py +148 -0
- package/mcp_server/tools/transport.py +113 -0
- package/package.json +38 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/agents/livepilot-producer/AGENT.md +61 -0
- package/plugin/commands/beat.md +18 -0
- package/plugin/commands/mix.md +15 -0
- package/plugin/commands/session.md +13 -0
- package/plugin/commands/sounddesign.md +16 -0
- package/plugin/plugin.json +18 -0
- package/plugin/skills/livepilot-core/SKILL.md +160 -0
- package/plugin/skills/livepilot-core/references/ableton-workflow-patterns.md +831 -0
- package/plugin/skills/livepilot-core/references/m4l-devices.md +352 -0
- package/plugin/skills/livepilot-core/references/midi-recipes.md +402 -0
- package/plugin/skills/livepilot-core/references/mixing-patterns.md +578 -0
- package/plugin/skills/livepilot-core/references/overview.md +191 -0
- package/plugin/skills/livepilot-core/references/sound-design.md +392 -0
- package/remote_script/LivePilot/__init__.py +42 -0
- package/remote_script/LivePilot/arrangement.py +678 -0
- package/remote_script/LivePilot/browser.py +325 -0
- package/remote_script/LivePilot/clips.py +172 -0
- package/remote_script/LivePilot/devices.py +466 -0
- package/remote_script/LivePilot/diagnostics.py +198 -0
- package/remote_script/LivePilot/mixing.py +194 -0
- package/remote_script/LivePilot/notes.py +339 -0
- package/remote_script/LivePilot/router.py +74 -0
- package/remote_script/LivePilot/scenes.py +75 -0
- package/remote_script/LivePilot/server.py +286 -0
- package/remote_script/LivePilot/tracks.py +229 -0
- package/remote_script/LivePilot/transport.py +147 -0
- package/remote_script/LivePilot/utils.py +112 -0
- package/requirements.txt +2 -0
- package/server.json +20 -0
package/bin/livepilot.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { execFileSync, execSync, spawn } = require("child_process");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const net = require("net");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
10
|
+
const PKG = require(path.join(ROOT, "package.json"));
|
|
11
|
+
const VENV_DIR = path.join(ROOT, ".venv");
|
|
12
|
+
const REQUIREMENTS = path.join(ROOT, "requirements.txt");
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Python detection
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function findPython() {
|
|
19
|
+
for (const cmd of ["python3", "python"]) {
|
|
20
|
+
try {
|
|
21
|
+
const out = execFileSync(cmd, ["--version"], {
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
timeout: 5000,
|
|
24
|
+
}).trim();
|
|
25
|
+
const match = out.match(/Python\s+(\d+)\.(\d+)/);
|
|
26
|
+
if (match) {
|
|
27
|
+
const major = parseInt(match[1], 10);
|
|
28
|
+
const minor = parseInt(match[2], 10);
|
|
29
|
+
if (major === 3 && minor >= 10) {
|
|
30
|
+
return { cmd, version: out };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// command not found or failed — try next
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Return the path to the Python binary inside the venv.
|
|
42
|
+
*/
|
|
43
|
+
function venvPython() {
|
|
44
|
+
const isWin = process.platform === "win32";
|
|
45
|
+
return path.join(VENV_DIR, isWin ? "Scripts" : "bin", isWin ? "python.exe" : "python3");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Virtual environment bootstrap
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Ensure a local .venv exists with dependencies installed.
|
|
54
|
+
* Returns the path to the venv Python binary.
|
|
55
|
+
*/
|
|
56
|
+
function ensureVenv(systemPython) {
|
|
57
|
+
const venvPy = venvPython();
|
|
58
|
+
|
|
59
|
+
// Check if venv already exists and has our deps
|
|
60
|
+
if (fs.existsSync(venvPy)) {
|
|
61
|
+
try {
|
|
62
|
+
execFileSync(venvPy, ["-c", "import fastmcp"], {
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
timeout: 10000,
|
|
65
|
+
stdio: "pipe",
|
|
66
|
+
});
|
|
67
|
+
return venvPy; // venv exists and fastmcp is importable
|
|
68
|
+
} catch {
|
|
69
|
+
// venv exists but deps missing — reinstall
|
|
70
|
+
console.log("LivePilot: reinstalling Python dependencies...");
|
|
71
|
+
execFileSync(venvPy, ["-m", "pip", "install", "-q", "-r", REQUIREMENTS], {
|
|
72
|
+
cwd: ROOT,
|
|
73
|
+
stdio: "inherit",
|
|
74
|
+
timeout: 120000,
|
|
75
|
+
});
|
|
76
|
+
return venvPy;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create venv from scratch
|
|
81
|
+
console.log("LivePilot: setting up Python environment (first run)...");
|
|
82
|
+
execFileSync(systemPython, ["-m", "venv", VENV_DIR], {
|
|
83
|
+
cwd: ROOT,
|
|
84
|
+
stdio: "inherit",
|
|
85
|
+
timeout: 30000,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
console.log("LivePilot: installing dependencies...");
|
|
89
|
+
execFileSync(venvPython(), ["-m", "pip", "install", "-q", "-r", REQUIREMENTS], {
|
|
90
|
+
cwd: ROOT,
|
|
91
|
+
stdio: "inherit",
|
|
92
|
+
timeout: 120000,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return venvPython();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Status check — TCP ping to Remote Script
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
function checkStatus() {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const sock = new net.Socket();
|
|
105
|
+
const PORT = parseInt(process.env.LIVE_MCP_PORT || "9878", 10);
|
|
106
|
+
const HOST = process.env.LIVE_MCP_HOST || "127.0.0.1";
|
|
107
|
+
|
|
108
|
+
sock.setTimeout(3000);
|
|
109
|
+
|
|
110
|
+
sock.on("connect", () => {
|
|
111
|
+
const ping = JSON.stringify({ id: "ping", type: "ping" }) + "\n";
|
|
112
|
+
sock.write(ping);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
let buf = "";
|
|
116
|
+
sock.on("data", (chunk) => {
|
|
117
|
+
buf += chunk.toString();
|
|
118
|
+
if (buf.includes("\n")) {
|
|
119
|
+
try {
|
|
120
|
+
const resp = JSON.parse(buf.split("\n")[0]);
|
|
121
|
+
if (resp.ok === true && resp.result && resp.result.pong) {
|
|
122
|
+
console.log(" Ableton Live: connected on %s:%d", HOST, PORT);
|
|
123
|
+
} else {
|
|
124
|
+
console.log(" Ableton Live: unexpected response:", JSON.stringify(resp));
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
console.log(" Ableton Live: invalid response");
|
|
128
|
+
}
|
|
129
|
+
sock.destroy();
|
|
130
|
+
resolve(true);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
sock.on("timeout", () => {
|
|
135
|
+
console.log(" Ableton Live: connection timed out");
|
|
136
|
+
sock.destroy();
|
|
137
|
+
resolve(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
sock.on("error", (err) => {
|
|
141
|
+
if (err.code === "ECONNREFUSED") {
|
|
142
|
+
console.log(" Ableton Live: not running (connection refused on %s:%d)", HOST, PORT);
|
|
143
|
+
} else {
|
|
144
|
+
console.log(" Ableton Live: %s", err.message);
|
|
145
|
+
}
|
|
146
|
+
resolve(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
sock.connect(PORT, HOST);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Doctor — comprehensive diagnostic
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
async function doctor() {
|
|
158
|
+
console.log("LivePilot Doctor v%s", PKG.version);
|
|
159
|
+
console.log("─".repeat(50));
|
|
160
|
+
|
|
161
|
+
let ok = true;
|
|
162
|
+
|
|
163
|
+
// 1. Python
|
|
164
|
+
const pyInfo = findPython();
|
|
165
|
+
if (pyInfo) {
|
|
166
|
+
console.log(" Python: %s (%s)", pyInfo.version, pyInfo.cmd);
|
|
167
|
+
} else {
|
|
168
|
+
console.log(" Python: NOT FOUND (need >= 3.10)");
|
|
169
|
+
console.log(" Fix: install Python 3.10+ and add to PATH");
|
|
170
|
+
ok = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Virtual environment
|
|
174
|
+
const venvPy = venvPython();
|
|
175
|
+
if (fs.existsSync(venvPy)) {
|
|
176
|
+
console.log(" Venv: %s", VENV_DIR);
|
|
177
|
+
} else {
|
|
178
|
+
console.log(" Venv: NOT CREATED (run 'npx livepilot' to bootstrap)");
|
|
179
|
+
console.log(" Fix: run 'npx livepilot' once to auto-create the virtual environment");
|
|
180
|
+
ok = false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 3. fastmcp import
|
|
184
|
+
if (fs.existsSync(venvPy)) {
|
|
185
|
+
try {
|
|
186
|
+
const ver = execFileSync(venvPy, ["-c", "import fastmcp; print(fastmcp.__version__)"], {
|
|
187
|
+
encoding: "utf-8",
|
|
188
|
+
timeout: 10000,
|
|
189
|
+
stdio: "pipe",
|
|
190
|
+
}).trim();
|
|
191
|
+
console.log(" fastmcp: v%s", ver);
|
|
192
|
+
} catch {
|
|
193
|
+
console.log(" fastmcp: NOT INSTALLED in venv");
|
|
194
|
+
console.log(" Fix: run 'npx livepilot' to auto-install dependencies");
|
|
195
|
+
ok = false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 4. MCP server module
|
|
200
|
+
const serverInit = path.join(ROOT, "mcp_server", "__init__.py");
|
|
201
|
+
if (fs.existsSync(serverInit)) {
|
|
202
|
+
console.log(" MCP server: found at %s", path.join(ROOT, "mcp_server"));
|
|
203
|
+
} else {
|
|
204
|
+
console.log(" MCP server: MISSING (mcp_server/ directory not found)");
|
|
205
|
+
ok = false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 5. Remote Script
|
|
209
|
+
const remoteInit = path.join(ROOT, "remote_script", "LivePilot", "__init__.py");
|
|
210
|
+
if (fs.existsSync(remoteInit)) {
|
|
211
|
+
console.log(" Remote Script: found at %s", path.join(ROOT, "remote_script", "LivePilot"));
|
|
212
|
+
} else {
|
|
213
|
+
console.log(" Remote Script: MISSING");
|
|
214
|
+
ok = false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 6. Remote Script installed in Ableton?
|
|
218
|
+
try {
|
|
219
|
+
const { findAbletonPaths } = require(path.join(ROOT, "installer", "paths.js"));
|
|
220
|
+
const candidates = findAbletonPaths();
|
|
221
|
+
let installed = false;
|
|
222
|
+
for (const c of candidates) {
|
|
223
|
+
const dest = path.join(c.path, "LivePilot", "__init__.py");
|
|
224
|
+
if (fs.existsSync(dest)) {
|
|
225
|
+
console.log(" Ableton install: %s", path.join(c.path, "LivePilot"));
|
|
226
|
+
installed = true;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!installed) {
|
|
231
|
+
console.log(" Ableton install: NOT INSTALLED");
|
|
232
|
+
console.log(" Fix: run 'npx livepilot --install' to copy Remote Script");
|
|
233
|
+
ok = false;
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
console.log(" Ableton install: could not check (installer module error)");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 7. Environment overrides
|
|
240
|
+
if (process.env.LIVE_MCP_HOST || process.env.LIVE_MCP_PORT) {
|
|
241
|
+
console.log(" Env overrides: HOST=%s PORT=%s",
|
|
242
|
+
process.env.LIVE_MCP_HOST || "(default 127.0.0.1)",
|
|
243
|
+
process.env.LIVE_MCP_PORT || "(default 9878)");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 8. TCP connection to Ableton
|
|
247
|
+
console.log("");
|
|
248
|
+
console.log("Connection test:");
|
|
249
|
+
await checkStatus();
|
|
250
|
+
|
|
251
|
+
// Summary
|
|
252
|
+
console.log("");
|
|
253
|
+
console.log("─".repeat(50));
|
|
254
|
+
if (ok) {
|
|
255
|
+
console.log("All checks passed.");
|
|
256
|
+
} else {
|
|
257
|
+
console.log("Some checks failed — see Fix suggestions above.");
|
|
258
|
+
}
|
|
259
|
+
return ok;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Main
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
async function main() {
|
|
267
|
+
const args = process.argv.slice(2);
|
|
268
|
+
const flag = args[0] || "";
|
|
269
|
+
|
|
270
|
+
// --version / -v
|
|
271
|
+
if (flag === "--version" || flag === "-v") {
|
|
272
|
+
console.log("livepilot v%s", PKG.version);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --help / -h
|
|
277
|
+
if (flag === "--help" || flag === "-h") {
|
|
278
|
+
console.log("livepilot v%s — AI copilot for Ableton Live 12", PKG.version);
|
|
279
|
+
console.log("");
|
|
280
|
+
console.log("Usage: npx livepilot [command]");
|
|
281
|
+
console.log("");
|
|
282
|
+
console.log("Commands:");
|
|
283
|
+
console.log(" (none) Start the MCP server");
|
|
284
|
+
console.log(" --install Install Remote Script into Ableton Live");
|
|
285
|
+
console.log(" --uninstall Remove Remote Script from Ableton Live");
|
|
286
|
+
console.log(" --status Check if Ableton Live is reachable");
|
|
287
|
+
console.log(" --doctor Run diagnostics (Python, deps, connection)");
|
|
288
|
+
console.log(" --version Show version");
|
|
289
|
+
console.log(" --help Show this help");
|
|
290
|
+
console.log("");
|
|
291
|
+
console.log("Environment:");
|
|
292
|
+
console.log(" LIVE_MCP_HOST Remote Script host (default: 127.0.0.1)");
|
|
293
|
+
console.log(" LIVE_MCP_PORT Remote Script port (default: 9878)");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --install
|
|
298
|
+
if (flag === "--install") {
|
|
299
|
+
const { install } = require(path.join(ROOT, "installer", "install.js"));
|
|
300
|
+
install();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// --uninstall
|
|
305
|
+
if (flag === "--uninstall") {
|
|
306
|
+
const { uninstall } = require(path.join(ROOT, "installer", "install.js"));
|
|
307
|
+
uninstall();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --status
|
|
312
|
+
if (flag === "--status") {
|
|
313
|
+
const reachable = await checkStatus();
|
|
314
|
+
process.exit(reachable ? 0 : 1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --doctor
|
|
318
|
+
if (flag === "--doctor") {
|
|
319
|
+
const passed = await doctor();
|
|
320
|
+
process.exit(passed ? 0 : 1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Default: start MCP server
|
|
324
|
+
const pyInfo = findPython();
|
|
325
|
+
if (!pyInfo) {
|
|
326
|
+
console.error("Error: Python >= 3.10 is required but was not found.");
|
|
327
|
+
console.error("");
|
|
328
|
+
console.error("Install Python 3.10+ and ensure 'python3' or 'python' is on your PATH.");
|
|
329
|
+
console.error(" macOS: brew install python@3.12");
|
|
330
|
+
console.error(" Ubuntu: sudo apt install python3");
|
|
331
|
+
console.error(" Windows: https://www.python.org/downloads/");
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Bootstrap venv and install deps automatically
|
|
336
|
+
let pythonBin;
|
|
337
|
+
try {
|
|
338
|
+
pythonBin = ensureVenv(pyInfo.cmd);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
console.error("Error: failed to set up Python environment.");
|
|
341
|
+
console.error(" %s", err.message);
|
|
342
|
+
if (err.stderr) {
|
|
343
|
+
console.error("");
|
|
344
|
+
console.error("pip output:");
|
|
345
|
+
console.error(" %s", err.stderr.toString().trim());
|
|
346
|
+
}
|
|
347
|
+
if (err.stdout) {
|
|
348
|
+
console.error(" %s", err.stdout.toString().trim());
|
|
349
|
+
}
|
|
350
|
+
console.error("");
|
|
351
|
+
console.error("You can try manually:");
|
|
352
|
+
console.error(" cd %s", ROOT);
|
|
353
|
+
console.error(" %s -m venv .venv", pyInfo.cmd);
|
|
354
|
+
console.error(" .venv/bin/pip install -r requirements.txt");
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const child = spawn(pythonBin, ["-m", "mcp_server"], {
|
|
359
|
+
cwd: ROOT,
|
|
360
|
+
stdio: "inherit",
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
child.on("error", (err) => {
|
|
364
|
+
console.error("Failed to start MCP server: %s", err.message);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
child.on("exit", (code) => {
|
|
369
|
+
process.exit(code || 0);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
main().catch((err) => {
|
|
374
|
+
console.error(err);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { findAbletonPaths } = require("./paths");
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
8
|
+
const SOURCE_DIR = path.join(ROOT, "remote_script", "LivePilot");
|
|
9
|
+
|
|
10
|
+
// Files / dirs to skip during copy
|
|
11
|
+
const SKIP = new Set(["__pycache__", ".DS_Store"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Recursively copy a directory, skipping __pycache__ and .DS_Store.
|
|
15
|
+
*/
|
|
16
|
+
function copyDirSync(src, dest) {
|
|
17
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
18
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (SKIP.has(entry.name)) continue;
|
|
21
|
+
const srcPath = path.join(src, entry.name);
|
|
22
|
+
const destPath = path.join(dest, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
copyDirSync(srcPath, destPath);
|
|
25
|
+
} else {
|
|
26
|
+
fs.copyFileSync(srcPath, destPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Install the LivePilot Remote Script into Ableton's Remote Scripts folder.
|
|
33
|
+
*/
|
|
34
|
+
function install() {
|
|
35
|
+
const candidates = findAbletonPaths();
|
|
36
|
+
|
|
37
|
+
if (candidates.length === 0) {
|
|
38
|
+
console.log("Could not auto-detect an Ableton Live Remote Scripts directory.");
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("Manual install:");
|
|
41
|
+
console.log(" 1. Open Ableton Live > Preferences > File/Folder");
|
|
42
|
+
console.log(" 2. Find the User Remote Scripts folder path");
|
|
43
|
+
console.log(" 3. Copy the 'remote_script/LivePilot' folder into that directory");
|
|
44
|
+
console.log(" 4. Restart Ableton Live");
|
|
45
|
+
console.log(" 5. In Preferences > Link/Tempo/MIDI, set a Control Surface to 'LivePilot'");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Use the first valid candidate
|
|
50
|
+
const target = candidates[0];
|
|
51
|
+
const targetBase = target.path;
|
|
52
|
+
const destDir = path.join(targetBase, "LivePilot");
|
|
53
|
+
|
|
54
|
+
// Ensure target base exists
|
|
55
|
+
fs.mkdirSync(targetBase, { recursive: true });
|
|
56
|
+
|
|
57
|
+
console.log("Installing LivePilot Remote Script...");
|
|
58
|
+
console.log(" Source: %s", SOURCE_DIR);
|
|
59
|
+
console.log(" Target: %s", destDir);
|
|
60
|
+
console.log(" Location: %s", target.description);
|
|
61
|
+
console.log("");
|
|
62
|
+
|
|
63
|
+
copyDirSync(SOURCE_DIR, destDir);
|
|
64
|
+
|
|
65
|
+
console.log("Done! Next steps:");
|
|
66
|
+
console.log(" 1. Restart Ableton Live (or press Cmd+, to open Preferences)");
|
|
67
|
+
console.log(" 2. Go to Link/Tempo/MIDI > Control Surface");
|
|
68
|
+
console.log(" 3. Select 'LivePilot' from the dropdown");
|
|
69
|
+
console.log(" 4. Run 'npx livepilot --status' to verify the connection");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remove the LivePilot Remote Script from all detected locations.
|
|
74
|
+
*/
|
|
75
|
+
function uninstall() {
|
|
76
|
+
const candidates = findAbletonPaths();
|
|
77
|
+
let removed = 0;
|
|
78
|
+
|
|
79
|
+
for (const candidate of candidates) {
|
|
80
|
+
const destDir = path.join(candidate.path, "LivePilot");
|
|
81
|
+
if (fs.existsSync(destDir)) {
|
|
82
|
+
console.log("Removing: %s", destDir);
|
|
83
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
84
|
+
removed++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (removed === 0) {
|
|
89
|
+
console.log("No LivePilot Remote Script installations found.");
|
|
90
|
+
} else {
|
|
91
|
+
console.log("Uninstalled %d location(s). Restart Ableton Live to complete.", removed);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { install, uninstall };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Return an array of {path, description} for candidate Ableton Remote Scripts
|
|
9
|
+
* directories on the current platform.
|
|
10
|
+
*/
|
|
11
|
+
function findAbletonPaths() {
|
|
12
|
+
const candidates = [];
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
const platform = os.platform();
|
|
15
|
+
|
|
16
|
+
if (platform === "darwin") {
|
|
17
|
+
// macOS candidate 1: User Library default
|
|
18
|
+
candidates.push({
|
|
19
|
+
path: path.join(home, "Music", "Ableton", "User Library", "Remote Scripts"),
|
|
20
|
+
description: "Ableton User Library (default)",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// macOS candidate 2: Preferences — scan for Live 12.* dirs
|
|
24
|
+
const prefsDir = path.join(home, "Library", "Preferences", "Ableton");
|
|
25
|
+
if (fs.existsSync(prefsDir)) {
|
|
26
|
+
try {
|
|
27
|
+
const entries = fs.readdirSync(prefsDir);
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.startsWith("Live 12")) {
|
|
30
|
+
candidates.push({
|
|
31
|
+
path: path.join(prefsDir, entry, "User Remote Scripts"),
|
|
32
|
+
description: `Ableton Preferences (${entry})`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// permission error — skip
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else if (platform === "win32") {
|
|
41
|
+
const userProfile = process.env.USERPROFILE || home;
|
|
42
|
+
const appData = process.env.APPDATA || path.join(userProfile, "AppData", "Roaming");
|
|
43
|
+
|
|
44
|
+
// Windows candidate 1: Documents User Library
|
|
45
|
+
candidates.push({
|
|
46
|
+
path: path.join(userProfile, "Documents", "Ableton", "User Library", "Remote Scripts"),
|
|
47
|
+
description: "Ableton User Library (default)",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Windows candidate 2: AppData — scan for Live 12.* dirs
|
|
51
|
+
const abletonAppData = path.join(appData, "Ableton");
|
|
52
|
+
if (fs.existsSync(abletonAppData)) {
|
|
53
|
+
try {
|
|
54
|
+
const entries = fs.readdirSync(abletonAppData);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry.startsWith("Live 12")) {
|
|
57
|
+
candidates.push({
|
|
58
|
+
path: path.join(abletonAppData, entry, "Preferences", "User Remote Scripts"),
|
|
59
|
+
description: `Ableton AppData (${entry})`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// permission error — skip
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Filter: keep candidate if the path itself exists, or its parent exists
|
|
70
|
+
return candidates.filter((c) => {
|
|
71
|
+
try {
|
|
72
|
+
return fs.existsSync(c.path) || fs.existsSync(path.dirname(c.path));
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { findAbletonPaths };
|