livepilot 1.9.12 → 1.9.13
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 +10 -0
- package/bin/livepilot.js +26 -8
- 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_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/memory/technique_store.py +30 -2
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +3 -3
- package/remote_script/LivePilot/server.py +2 -1
- package/requirements.txt +3 -0
- 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.13",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Pilot Studio"
|
|
16
16
|
},
|
package/AGENTS.md
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.13 — Security Hardening + Startup Safety (April 2026)
|
|
4
|
+
|
|
5
|
+
- 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
|
|
6
|
+
- 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
|
|
7
|
+
- Fix(P3): `--status` and `--doctor` now return exit 0 when Ableton is reachable but another client is connected (STATE_ERROR = reachable, not failure)
|
|
8
|
+
- Fix(P3): negative `limit` values on `memory_recall` and `memory_list` now raise `ValueError` instead of using Python negative slicing
|
|
9
|
+
- Fix: Remote Script no longer logs "Server started" before bind succeeds — "Listening on..." is logged from the server loop after successful bind
|
|
10
|
+
- Fix: `requirements.txt` now documents dev dependencies (pytest, pytest-asyncio) as comments
|
|
11
|
+
- Verification: 145 tests passing, 178 tools confirmed
|
|
12
|
+
|
|
3
13
|
## 1.9.12 — Deep Audit: 21 Fixes Across 15 Files (April 2026)
|
|
4
14
|
|
|
5
15
|
**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
|
}
|
|
@@ -346,10 +350,15 @@ async function setupFlucoma() {
|
|
|
346
350
|
}
|
|
347
351
|
|
|
348
352
|
console.log("FluCoMa not found. Downloading from GitHub...");
|
|
353
|
+
const crypto = require("crypto");
|
|
354
|
+
|
|
355
|
+
// Pin to a known release tag for reproducibility
|
|
356
|
+
const FLUCOMA_TAG = "1.0.7";
|
|
357
|
+
const FLUCOMA_URL = `https://api.github.com/repos/flucoma/flucoma-max/releases/tags/${FLUCOMA_TAG}`;
|
|
349
358
|
|
|
350
|
-
// Fetch
|
|
359
|
+
// Fetch pinned release info
|
|
351
360
|
const releaseInfo = await new Promise((resolve, reject) => {
|
|
352
|
-
https.get(
|
|
361
|
+
https.get(FLUCOMA_URL, {
|
|
353
362
|
headers: { "User-Agent": "LivePilot" }
|
|
354
363
|
}, (res) => {
|
|
355
364
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
@@ -368,13 +377,14 @@ async function setupFlucoma() {
|
|
|
368
377
|
}).on("error", reject);
|
|
369
378
|
});
|
|
370
379
|
|
|
371
|
-
const
|
|
380
|
+
const platform = process.platform === "darwin" ? "Mac" : "Windows";
|
|
381
|
+
const zipAsset = releaseInfo.assets.find(a => a.name.endsWith(".zip") && a.name.includes(platform));
|
|
372
382
|
if (!zipAsset) {
|
|
373
|
-
console.error("Error: no zip asset found in FluCoMa release");
|
|
383
|
+
console.error("Error: no %s zip asset found in FluCoMa release %s", platform, FLUCOMA_TAG);
|
|
374
384
|
process.exit(1);
|
|
375
385
|
}
|
|
376
386
|
|
|
377
|
-
console.log("Downloading %s (%sMB)...", zipAsset.name,
|
|
387
|
+
console.log("Downloading %s (v%s, %sMB)...", zipAsset.name, FLUCOMA_TAG,
|
|
378
388
|
Math.round(zipAsset.size / 1024 / 1024));
|
|
379
389
|
|
|
380
390
|
// Download to temp
|
|
@@ -399,6 +409,13 @@ async function setupFlucoma() {
|
|
|
399
409
|
download(downloadUrl, 0);
|
|
400
410
|
});
|
|
401
411
|
|
|
412
|
+
// Verify download integrity via SHA256 of the zip file
|
|
413
|
+
const hash = crypto.createHash("sha256");
|
|
414
|
+
hash.update(fs.readFileSync(zipPath));
|
|
415
|
+
const sha256 = hash.digest("hex");
|
|
416
|
+
console.log("SHA256: %s", sha256);
|
|
417
|
+
console.log("Verify this matches the checksum on https://github.com/flucoma/flucoma-max/releases/tag/%s", FLUCOMA_TAG);
|
|
418
|
+
|
|
402
419
|
console.log("Extracting to %s...", packagesDir);
|
|
403
420
|
fs.mkdirSync(packagesDir, { recursive: true });
|
|
404
421
|
|
|
@@ -417,8 +434,9 @@ async function setupFlucoma() {
|
|
|
417
434
|
});
|
|
418
435
|
}
|
|
419
436
|
|
|
420
|
-
// macOS: strip quarantine
|
|
437
|
+
// macOS: strip quarantine on FluCoMa externals only (not on arbitrary paths)
|
|
421
438
|
if (process.platform === "darwin" && fs.existsSync(flucomaDir)) {
|
|
439
|
+
console.log("Removing macOS quarantine from FluCoMa externals...");
|
|
422
440
|
try {
|
|
423
441
|
execFileSync("xattr", ["-d", "-r", "com.apple.quarantine", flucomaDir], {
|
|
424
442
|
stdio: "pipe",
|
|
@@ -434,7 +452,7 @@ async function setupFlucoma() {
|
|
|
434
452
|
|
|
435
453
|
if (fs.existsSync(flucomaDir)) {
|
|
436
454
|
console.log("");
|
|
437
|
-
console.log("FluCoMa installed successfully!");
|
|
455
|
+
console.log("FluCoMa v%s installed successfully!", FLUCOMA_TAG);
|
|
438
456
|
console.log("Restart Ableton Live for real-time DSP tools.");
|
|
439
457
|
} else {
|
|
440
458
|
console.error("Error: FluCoMa directory not found after extraction.");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.13",
|
|
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.13",
|
|
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.13 — 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
|
|
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.13"
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.13",
|
|
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.13"
|
|
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
|
@@ -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
|