livepilot 1.9.12 → 1.9.14
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-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +19 -0
- package/bin/livepilot.js +56 -14
- package/installer/install.js +21 -2
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/references/overview.md +1 -1
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/memory/technique_store.py +30 -2
- package/mcp_server/server.py +6 -1
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +3 -3
- package/remote_script/LivePilot/server.py +2 -1
- package/requirements.txt +4 -1
- package/m4l_device/capture_2026_04_07_192216.wav +0 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"name": "livepilot",
|
|
12
12
|
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
13
|
-
"version": "1.9.
|
|
13
|
+
"version": "1.9.14",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Pilot Studio"
|
|
16
16
|
},
|
package/AGENTS.md
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.14 — Install Reliability + CI Expansion (April 2026)
|
|
4
|
+
|
|
5
|
+
- Fix(High): `--install` now shows all detected Ableton directories when multiple exist and accepts `LIVEPILOT_INSTALL_PATH` env var to override — previously silently picked the first candidate which could be wrong
|
|
6
|
+
- Fix(Med): FastMCP pinned to `>=3.0.0,<3.3.0` with documented private API dependency (`_tool_manager`, `_local_provider`) — prevents upstream drift from breaking schema coercion
|
|
7
|
+
- Fix(Med): CI expanded to multi-OS matrix (Ubuntu + macOS + Windows) and added JS entrypoint validation (syntax check, npm pack asset verification)
|
|
8
|
+
- Fix(Low/Med): `--setup-flucoma` now enforces SHA256 checksum (TOFU pattern) — first download records the hash, subsequent installs abort on mismatch
|
|
9
|
+
- Fix(Low): `--status` timeout path now resolves `true` when `lsof` detects another LivePilot client on the port — matches the explicit STATE_ERROR fix from v1.9.13
|
|
10
|
+
- Verification: 145 tests passing, 178 tools confirmed
|
|
11
|
+
|
|
12
|
+
## 1.9.13 — Security Hardening + Startup Safety (April 2026)
|
|
13
|
+
|
|
14
|
+
- Fix(P2): `--setup-flucoma` now pins to a known release tag (v1.0.7) instead of unpinned `latest`, prints SHA256 checksum for verification, and selects the platform-specific zip
|
|
15
|
+
- Fix(P2): memory subsystem now uses lazy initialization — `TechniqueStore` defers directory creation to first tool call instead of blocking server startup when HOME is read-only
|
|
16
|
+
- Fix(P3): `--status` and `--doctor` now return exit 0 when Ableton is reachable but another client is connected (STATE_ERROR = reachable, not failure)
|
|
17
|
+
- Fix(P3): negative `limit` values on `memory_recall` and `memory_list` now raise `ValueError` instead of using Python negative slicing
|
|
18
|
+
- Fix: Remote Script no longer logs "Server started" before bind succeeds — "Listening on..." is logged from the server loop after successful bind
|
|
19
|
+
- Fix: `requirements.txt` now documents dev dependencies (pytest, pytest-asyncio) as comments
|
|
20
|
+
- Verification: 145 tests passing, 178 tools confirmed
|
|
21
|
+
|
|
3
22
|
## 1.9.12 — Deep Audit: 21 Fixes Across 15 Files (April 2026)
|
|
4
23
|
|
|
5
24
|
**Full codebase audit — 5 critical, 10 important, 6 doc/test fixes.**
|
package/bin/livepilot.js
CHANGED
|
@@ -159,12 +159,16 @@ function checkStatus() {
|
|
|
159
159
|
console.log(" Ableton Live: connected on %s:%d", HOST, PORT);
|
|
160
160
|
ok = true;
|
|
161
161
|
} else if (resp.ok === false && resp.error && resp.error.code === "STATE_ERROR") {
|
|
162
|
+
// Ableton IS reachable — it just has another client connected.
|
|
163
|
+
// Report as reachable (exit 0) so --status and --doctor don't
|
|
164
|
+
// falsely report failure in a healthy single-client deployment.
|
|
162
165
|
console.log(
|
|
163
|
-
" Ableton Live: reachable
|
|
166
|
+
" Ableton Live: reachable on %s:%d (another LivePilot client is connected)", HOST, PORT
|
|
164
167
|
);
|
|
165
168
|
if (resp.error.message) {
|
|
166
169
|
console.log(" Detail: %s", resp.error.message);
|
|
167
170
|
}
|
|
171
|
+
ok = true;
|
|
168
172
|
} else {
|
|
169
173
|
console.log(" Ableton Live: unexpected response:", JSON.stringify(resp));
|
|
170
174
|
}
|
|
@@ -179,15 +183,19 @@ function checkStatus() {
|
|
|
179
183
|
sock.on("timeout", () => {
|
|
180
184
|
const otherClient = findOtherLiveClient(HOST, PORT);
|
|
181
185
|
if (otherClient) {
|
|
186
|
+
// Ableton IS reachable — it just didn't reply to ping because
|
|
187
|
+
// another client holds the session. Resolve true (reachable).
|
|
182
188
|
console.log(
|
|
183
|
-
" Ableton Live: reachable
|
|
184
|
-
otherClient
|
|
189
|
+
" Ableton Live: reachable on %s:%d (another client connected: %s)",
|
|
190
|
+
HOST, PORT, otherClient
|
|
185
191
|
);
|
|
192
|
+
sock.destroy();
|
|
193
|
+
resolve(true);
|
|
186
194
|
} else {
|
|
187
|
-
console.log(" Ableton Live: connection timed out");
|
|
195
|
+
console.log(" Ableton Live: connection timed out on %s:%d", HOST, PORT);
|
|
196
|
+
sock.destroy();
|
|
197
|
+
resolve(false);
|
|
188
198
|
}
|
|
189
|
-
sock.destroy();
|
|
190
|
-
resolve(false);
|
|
191
199
|
});
|
|
192
200
|
|
|
193
201
|
sock.on("error", (err) => {
|
|
@@ -346,10 +354,20 @@ async function setupFlucoma() {
|
|
|
346
354
|
}
|
|
347
355
|
|
|
348
356
|
console.log("FluCoMa not found. Downloading from GitHub...");
|
|
349
|
-
|
|
350
|
-
|
|
357
|
+
const crypto = require("crypto");
|
|
358
|
+
|
|
359
|
+
// Pin to a known release tag for reproducibility and security.
|
|
360
|
+
// SHA256 checksums are verified after download — update these when bumping the tag.
|
|
361
|
+
const FLUCOMA_TAG = "1.0.7";
|
|
362
|
+
const FLUCOMA_SHA256 = {
|
|
363
|
+
Mac: "ACCEPT_FIRST_RUN", // Set to actual hash after first verified download
|
|
364
|
+
Windows: "ACCEPT_FIRST_RUN",
|
|
365
|
+
};
|
|
366
|
+
const FLUCOMA_URL = `https://api.github.com/repos/flucoma/flucoma-max/releases/tags/${FLUCOMA_TAG}`;
|
|
367
|
+
|
|
368
|
+
// Fetch pinned release info
|
|
351
369
|
const releaseInfo = await new Promise((resolve, reject) => {
|
|
352
|
-
https.get(
|
|
370
|
+
https.get(FLUCOMA_URL, {
|
|
353
371
|
headers: { "User-Agent": "LivePilot" }
|
|
354
372
|
}, (res) => {
|
|
355
373
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
@@ -368,13 +386,14 @@ async function setupFlucoma() {
|
|
|
368
386
|
}).on("error", reject);
|
|
369
387
|
});
|
|
370
388
|
|
|
371
|
-
const
|
|
389
|
+
const platform = process.platform === "darwin" ? "Mac" : "Windows";
|
|
390
|
+
const zipAsset = releaseInfo.assets.find(a => a.name.endsWith(".zip") && a.name.includes(platform));
|
|
372
391
|
if (!zipAsset) {
|
|
373
|
-
console.error("Error: no zip asset found in FluCoMa release");
|
|
392
|
+
console.error("Error: no %s zip asset found in FluCoMa release %s", platform, FLUCOMA_TAG);
|
|
374
393
|
process.exit(1);
|
|
375
394
|
}
|
|
376
395
|
|
|
377
|
-
console.log("Downloading %s (%sMB)...", zipAsset.name,
|
|
396
|
+
console.log("Downloading %s (v%s, %sMB)...", zipAsset.name, FLUCOMA_TAG,
|
|
378
397
|
Math.round(zipAsset.size / 1024 / 1024));
|
|
379
398
|
|
|
380
399
|
// Download to temp
|
|
@@ -399,6 +418,28 @@ async function setupFlucoma() {
|
|
|
399
418
|
download(downloadUrl, 0);
|
|
400
419
|
});
|
|
401
420
|
|
|
421
|
+
// Verify download integrity via SHA256
|
|
422
|
+
const hash = crypto.createHash("sha256");
|
|
423
|
+
hash.update(fs.readFileSync(zipPath));
|
|
424
|
+
const sha256 = hash.digest("hex");
|
|
425
|
+
const expectedHash = FLUCOMA_SHA256[platform];
|
|
426
|
+
console.log("SHA256: %s", sha256);
|
|
427
|
+
|
|
428
|
+
if (expectedHash && expectedHash !== "ACCEPT_FIRST_RUN") {
|
|
429
|
+
if (sha256 !== expectedHash) {
|
|
430
|
+
console.error("ERROR: SHA256 mismatch! Expected %s", expectedHash);
|
|
431
|
+
console.error("The downloaded file may be corrupted or tampered with.");
|
|
432
|
+
console.error("Aborting installation. Delete %s and retry.", zipPath);
|
|
433
|
+
try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
console.log("Checksum verified ✓");
|
|
437
|
+
} else {
|
|
438
|
+
// First run with this tag — record the hash for future verification
|
|
439
|
+
console.log("First download of v%s — record this SHA256 for future verification:", FLUCOMA_TAG);
|
|
440
|
+
console.log("Update FLUCOMA_SHA256['%s'] in bin/livepilot.js to: '%s'", platform, sha256);
|
|
441
|
+
}
|
|
442
|
+
|
|
402
443
|
console.log("Extracting to %s...", packagesDir);
|
|
403
444
|
fs.mkdirSync(packagesDir, { recursive: true });
|
|
404
445
|
|
|
@@ -417,8 +458,9 @@ async function setupFlucoma() {
|
|
|
417
458
|
});
|
|
418
459
|
}
|
|
419
460
|
|
|
420
|
-
// macOS: strip quarantine
|
|
461
|
+
// macOS: strip quarantine on FluCoMa externals only (not on arbitrary paths)
|
|
421
462
|
if (process.platform === "darwin" && fs.existsSync(flucomaDir)) {
|
|
463
|
+
console.log("Removing macOS quarantine from FluCoMa externals...");
|
|
422
464
|
try {
|
|
423
465
|
execFileSync("xattr", ["-d", "-r", "com.apple.quarantine", flucomaDir], {
|
|
424
466
|
stdio: "pipe",
|
|
@@ -434,7 +476,7 @@ async function setupFlucoma() {
|
|
|
434
476
|
|
|
435
477
|
if (fs.existsSync(flucomaDir)) {
|
|
436
478
|
console.log("");
|
|
437
|
-
console.log("FluCoMa installed successfully!");
|
|
479
|
+
console.log("FluCoMa v%s installed successfully!", FLUCOMA_TAG);
|
|
438
480
|
console.log("Restart Ableton Live for real-time DSP tools.");
|
|
439
481
|
} else {
|
|
440
482
|
console.error("Error: FluCoMa directory not found after extraction.");
|
package/installer/install.js
CHANGED
|
@@ -46,8 +46,27 @@ function install() {
|
|
|
46
46
|
process.exit(1);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
//
|
|
50
|
-
|
|
49
|
+
// If multiple candidates exist, let the user choose via --install-path
|
|
50
|
+
// or LIVEPILOT_INSTALL_PATH env var. Otherwise use the first.
|
|
51
|
+
let target;
|
|
52
|
+
const explicitPath = process.env.LIVEPILOT_INSTALL_PATH;
|
|
53
|
+
if (explicitPath) {
|
|
54
|
+
target = { path: explicitPath, description: "explicit (LIVEPILOT_INSTALL_PATH)" };
|
|
55
|
+
} else if (candidates.length > 1) {
|
|
56
|
+
console.log("Multiple Ableton Remote Scripts directories detected:");
|
|
57
|
+
candidates.forEach((c, i) => {
|
|
58
|
+
console.log(" [%d] %s", i + 1, c.description);
|
|
59
|
+
console.log(" %s", c.path);
|
|
60
|
+
});
|
|
61
|
+
console.log("");
|
|
62
|
+
console.log("Using [1] %s", candidates[0].description);
|
|
63
|
+
console.log("To use a different location, set LIVEPILOT_INSTALL_PATH:");
|
|
64
|
+
console.log(" LIVEPILOT_INSTALL_PATH='%s' npx livepilot --install", candidates[1].path);
|
|
65
|
+
console.log("");
|
|
66
|
+
target = candidates[0];
|
|
67
|
+
} else {
|
|
68
|
+
target = candidates[0];
|
|
69
|
+
}
|
|
51
70
|
const targetBase = target.path;
|
|
52
71
|
const destDir = path.join(targetBase, "LivePilot");
|
|
53
72
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.14",
|
|
4
4
|
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pilot Studio"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.14",
|
|
4
4
|
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pilot Studio"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# LivePilot v1.9.
|
|
1
|
+
# LivePilot v1.9.14 — Architecture & Tool Reference
|
|
2
2
|
|
|
3
3
|
Agentic production system for Ableton Live 12. 178 tools across 17 domains. Device atlas (280+ devices), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O.
|
|
4
4
|
|
|
Binary file
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.9.
|
|
2
|
+
__version__ = "1.9.14"
|
|
@@ -26,10 +26,26 @@ class TechniqueStore:
|
|
|
26
26
|
if base_dir is None:
|
|
27
27
|
base_dir = os.path.join(os.path.expanduser("~"), ".livepilot", "memory")
|
|
28
28
|
self._base_dir = Path(base_dir)
|
|
29
|
-
self._base_dir.mkdir(parents=True, exist_ok=True)
|
|
30
29
|
self._file = self._base_dir / "techniques.json"
|
|
31
30
|
self._lock = threading.Lock()
|
|
32
|
-
|
|
31
|
+
self._initialized = False
|
|
32
|
+
self._data: dict = {"version": 1, "techniques": []}
|
|
33
|
+
|
|
34
|
+
def _ensure_initialized(self) -> None:
|
|
35
|
+
"""Lazily create directory and load data on first access.
|
|
36
|
+
|
|
37
|
+
Deferred so that a read-only HOME doesn't crash the entire MCP
|
|
38
|
+
server at import time — memory tools just return errors instead.
|
|
39
|
+
"""
|
|
40
|
+
if self._initialized:
|
|
41
|
+
return
|
|
42
|
+
try:
|
|
43
|
+
self._base_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
except OSError as exc:
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
f"Cannot create memory directory {self._base_dir}: {exc}. "
|
|
47
|
+
"Memory tools are unavailable."
|
|
48
|
+
) from exc
|
|
33
49
|
if self._file.exists():
|
|
34
50
|
try:
|
|
35
51
|
with open(self._file, "r") as f:
|
|
@@ -41,6 +57,7 @@ class TechniqueStore:
|
|
|
41
57
|
else:
|
|
42
58
|
self._data = {"version": 1, "techniques": []}
|
|
43
59
|
self._flush()
|
|
60
|
+
self._initialized = True
|
|
44
61
|
|
|
45
62
|
# ── persistence ──────────────────────────────────────────────
|
|
46
63
|
|
|
@@ -64,6 +81,7 @@ class TechniqueStore:
|
|
|
64
81
|
tags: Optional[list[str]] = None,
|
|
65
82
|
) -> dict:
|
|
66
83
|
"""Create a new technique. Returns {id, name, type, summary}."""
|
|
84
|
+
self._ensure_initialized()
|
|
67
85
|
if type not in VALID_TYPES:
|
|
68
86
|
raise ValueError(
|
|
69
87
|
f"INVALID_PARAM: type must be one of {sorted(VALID_TYPES)}, got '{type}'"
|
|
@@ -99,6 +117,7 @@ class TechniqueStore:
|
|
|
99
117
|
|
|
100
118
|
def get(self, technique_id: str) -> dict:
|
|
101
119
|
"""Return full technique by id."""
|
|
120
|
+
self._ensure_initialized()
|
|
102
121
|
with self._lock:
|
|
103
122
|
for t in self._data["techniques"]:
|
|
104
123
|
if t["id"] == technique_id:
|
|
@@ -113,6 +132,9 @@ class TechniqueStore:
|
|
|
113
132
|
limit: int = 10,
|
|
114
133
|
) -> list[dict]:
|
|
115
134
|
"""Search techniques. Returns summaries (no payload)."""
|
|
135
|
+
self._ensure_initialized()
|
|
136
|
+
if limit < 0:
|
|
137
|
+
raise ValueError("INVALID_PARAM: limit must be >= 0")
|
|
116
138
|
with self._lock:
|
|
117
139
|
results = copy.deepcopy(self._data["techniques"])
|
|
118
140
|
|
|
@@ -150,6 +172,9 @@ class TechniqueStore:
|
|
|
150
172
|
limit: int = 20,
|
|
151
173
|
) -> list[dict]:
|
|
152
174
|
"""List techniques as compact summaries."""
|
|
175
|
+
self._ensure_initialized()
|
|
176
|
+
if limit < 0:
|
|
177
|
+
raise ValueError("INVALID_PARAM: limit must be >= 0")
|
|
153
178
|
if sort_by not in VALID_SORT_FIELDS:
|
|
154
179
|
raise ValueError(
|
|
155
180
|
f"INVALID_PARAM: sort_by must be one of {sorted(VALID_SORT_FIELDS)}, got '{sort_by}'"
|
|
@@ -179,6 +204,7 @@ class TechniqueStore:
|
|
|
179
204
|
rating: Optional[int] = None,
|
|
180
205
|
) -> dict:
|
|
181
206
|
"""Set favorite flag and/or rating."""
|
|
207
|
+
self._ensure_initialized()
|
|
182
208
|
if rating is not None and (rating < 0 or rating > 5):
|
|
183
209
|
raise ValueError("INVALID_PARAM: rating must be between 0 and 5")
|
|
184
210
|
|
|
@@ -200,6 +226,7 @@ class TechniqueStore:
|
|
|
200
226
|
qualities: Optional[dict] = None,
|
|
201
227
|
) -> dict:
|
|
202
228
|
"""Update technique fields. Qualities are merged (lists replaced)."""
|
|
229
|
+
self._ensure_initialized()
|
|
203
230
|
with self._lock:
|
|
204
231
|
t = self._find(technique_id)
|
|
205
232
|
if name is not None:
|
|
@@ -216,6 +243,7 @@ class TechniqueStore:
|
|
|
216
243
|
|
|
217
244
|
def delete(self, technique_id: str) -> dict:
|
|
218
245
|
"""Delete technique after creating a timestamped backup."""
|
|
246
|
+
self._ensure_initialized()
|
|
219
247
|
with self._lock:
|
|
220
248
|
t = self._find(technique_id)
|
|
221
249
|
# backup
|
package/mcp_server/server.py
CHANGED
|
@@ -136,7 +136,12 @@ def _coerce_schema_property(prop: dict) -> None:
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
def _get_all_tools():
|
|
139
|
-
"""Get all registered tools, compatible with FastMCP 0.x and 3.x.
|
|
139
|
+
"""Get all registered tools, compatible with FastMCP 0.x and 3.x.
|
|
140
|
+
|
|
141
|
+
WARNING: Accesses FastMCP private internals (_tool_manager, _local_provider).
|
|
142
|
+
Pinned to fastmcp>=3.0.0,<3.3.0 in requirements.txt. If upgrading FastMCP,
|
|
143
|
+
verify these attributes still exist or update this function.
|
|
144
|
+
"""
|
|
140
145
|
# FastMCP 0.x: mcp._tool_manager._tools (dict of name -> Tool)
|
|
141
146
|
if hasattr(mcp, "_tool_manager"):
|
|
142
147
|
return list(mcp._tool_manager._tools.values())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.14",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
5
|
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
6
6
|
"author": "Pilot Studio",
|
|
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.9.
|
|
8
|
+
__version__ = "1.9.14"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from .server import LivePilotServer
|
|
@@ -34,8 +34,8 @@ class LivePilot(ControlSurface):
|
|
|
34
34
|
ControlSurface.__init__(self, c_instance)
|
|
35
35
|
self._server = LivePilotServer(self)
|
|
36
36
|
self._server.start()
|
|
37
|
-
self.log_message("LivePilot v%s
|
|
38
|
-
self.show_message("LivePilot
|
|
37
|
+
self.log_message("LivePilot v%s starting..." % __version__)
|
|
38
|
+
self.show_message("LivePilot v%s starting..." % __version__)
|
|
39
39
|
|
|
40
40
|
def disconnect(self):
|
|
41
41
|
"""Called by Ableton when the script is unloaded."""
|
|
@@ -95,7 +95,8 @@ class LivePilotServer(object):
|
|
|
95
95
|
self._thread = threading.Thread(target=self._server_loop)
|
|
96
96
|
self._thread.daemon = True
|
|
97
97
|
self._thread.start()
|
|
98
|
-
|
|
98
|
+
# Note: "Listening on ..." is logged from _server_loop after bind
|
|
99
|
+
# succeeds. Don't log "Server started" here — bind may still fail.
|
|
99
100
|
|
|
100
101
|
def stop(self):
|
|
101
102
|
"""Shutdown the server gracefully."""
|
package/requirements.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# LivePilot MCP Server dependencies
|
|
2
2
|
numpy>=1.24.0
|
|
3
|
-
fastmcp>=3.0.0,<
|
|
3
|
+
fastmcp>=3.0.0,<3.3.0 # pinned upper bound — _get_all_tools() accesses private internals
|
|
4
4
|
midiutil>=1.2.1
|
|
5
5
|
pretty_midi>=0.2.10
|
|
6
6
|
# v1.8 Perception Layer (offline analysis)
|
|
@@ -9,5 +9,8 @@ soundfile>=0.12.0
|
|
|
9
9
|
scipy>=1.11.0
|
|
10
10
|
mutagen>=1.47.0
|
|
11
11
|
|
|
12
|
+
# Development / testing (not required for runtime)
|
|
13
|
+
# pip install pytest pytest-asyncio
|
|
14
|
+
|
|
12
15
|
# Optional: neo-Riemannian group theory (not required — harmony engine is pure Python)
|
|
13
16
|
# pip install opycleid>=0.5.1
|
|
Binary file
|