livepilot 1.9.11 → 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 +57 -0
- package/README.md +11 -10
- package/bin/livepilot.js +27 -9
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/SKILL.md +6 -6
- package/livepilot/skills/livepilot-core/references/overview.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +4 -4
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +77 -3
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/connection.py +5 -1
- package/mcp_server/curves.py +5 -1
- package/mcp_server/m4l_bridge.py +37 -20
- package/mcp_server/memory/technique_store.py +30 -2
- package/mcp_server/server.py +10 -2
- package/mcp_server/tools/analyzer.py +12 -1
- package/mcp_server/tools/arrangement.py +20 -5
- package/mcp_server/tools/automation.py +52 -0
- package/mcp_server/tools/devices.py +2 -3
- package/mcp_server/tools/generative.py +4 -0
- package/mcp_server/tools/midi_io.py +22 -4
- package/mcp_server/tools/theory.py +8 -1
- package/mcp_server/tools/transport.py +16 -6
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +3 -3
- package/remote_script/LivePilot/arrangement.py +12 -18
- package/remote_script/LivePilot/browser.py +19 -8
- package/remote_script/LivePilot/clip_automation.py +5 -4
- package/remote_script/LivePilot/clips.py +14 -6
- package/remote_script/LivePilot/devices.py +6 -3
- package/remote_script/LivePilot/server.py +20 -7
- package/remote_script/LivePilot/tracks.py +7 -2
- package/remote_script/LivePilot/transport.py +3 -3
- package/requirements.txt +6 -1
|
@@ -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,55 @@
|
|
|
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
|
+
|
|
13
|
+
## 1.9.12 — Deep Audit: 21 Fixes Across 15 Files (April 2026)
|
|
14
|
+
|
|
15
|
+
**Full codebase audit — 5 critical, 10 important, 6 doc/test fixes.**
|
|
16
|
+
|
|
17
|
+
### Critical Fixes
|
|
18
|
+
- Fix(P1): `capture_stop` no longer deadlocks — `cancel_capture_future` removed lock acquisition that blocked behind `send_capture`
|
|
19
|
+
- Fix(P1): `import_midi_to_clip` now distinguishes empty-slot NOT_FOUND from INDEX_ERROR/TIMEOUT instead of swallowing all AbletonConnectionErrors
|
|
20
|
+
- Fix(P1): capture audio files now write to `~/Documents/LivePilot/captures/` (stable path) instead of beside the .amxd preset
|
|
21
|
+
- Fix(P1): `check_flucoma` now uses `Folder.end` to detect FluCoMa — `typelist` check was always true
|
|
22
|
+
- Fix(P1): CI workflow updated to `actions/checkout@v4` + `actions/setup-python@v5` (v6 doesn't exist)
|
|
23
|
+
|
|
24
|
+
### Safety & Validation
|
|
25
|
+
- Fix(P2): 5 automation tools now validate `track_index >= 0` and `clip_index >= 0` (matching all peer modules)
|
|
26
|
+
- Fix(P2): `cmd_stop_scrub` now checks `cursor_a.id === 0` for empty clip slots (matching all peer bridge functions)
|
|
27
|
+
- Fix(P2): `cmd_get_selected` now resolves return tracks (negative indices) and master track (-1000)
|
|
28
|
+
- Fix(P2): `duplicate_track` uses count-before/after delta for correct group track duplication index
|
|
29
|
+
- Fix(P2): `create_arrangement_clip` locates first clip by `start_time` instead of stale index after trim pass
|
|
30
|
+
- Fix(P2): `get_session_info` reuses already-built lists instead of re-iterating `song.tracks`/`song.scenes`
|
|
31
|
+
- Fix(P2): client disconnect race — socket now closes before clearing `_client_connected` flag
|
|
32
|
+
|
|
33
|
+
### Tests
|
|
34
|
+
- Fix: transport validation tests now import production `_validate_tempo`/`_validate_time_signature` instead of testing local copies
|
|
35
|
+
- Fix: added `load_sample_to_simpler` to analyzer tool contract (was 28/29)
|
|
36
|
+
- Fix: removed duplicate `test_release_quick_verify_checks_both_plugin_manifests`
|
|
37
|
+
- New: 5 automation negative tests (index validation, parameter_type validation)
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
- Fix: `docs/manual/index.md` domain map — Tracks 14→17, Devices 12→15, Scenes 8→12
|
|
41
|
+
- Fix: README perception split — 145+33 → 149+29 (actual analyzer tool count is 29)
|
|
42
|
+
- Fix: M4L_BRIDGE.md command count — 22→28 (6 commands undocumented)
|
|
43
|
+
- Fix: tool-reference.md MIDI docs — `export_clip_midi` and `import_midi_to_clip` parameter tables matched to actual signatures
|
|
44
|
+
|
|
45
|
+
### Deferred (documented, low-impact)
|
|
46
|
+
- Timed-out commands still execute on main thread (needs cancellation token redesign)
|
|
47
|
+
- Chunked UDP reassembly fragile on packet loss (loopback mitigates)
|
|
48
|
+
- Diatonic transpose octave correction edge case (needs musical test suite)
|
|
49
|
+
- `cmd_map_plugin_param` reports false success (LiveAPI lacks Configure mapping API)
|
|
50
|
+
|
|
51
|
+
Verification: 145 tests passing (non-fastmcp), 178 tools confirmed, 15 files changed
|
|
52
|
+
|
|
3
53
|
## 1.9.11 — Session Diagnostics + Client Conflict Clarity (March 2026)
|
|
4
54
|
|
|
5
55
|
**Live-tested against the open Ableton set after reloading the updated Remote Script.**
|
|
@@ -11,6 +61,13 @@
|
|
|
11
61
|
- Fix(P2): `--status` and TCP timeout paths now explain when another LivePilot client appears to be connected instead of only reporting a generic timeout
|
|
12
62
|
- Docs: beat/sounddesign/core skill guidance now includes device-health checks, sample-dependent plugin cautions, and pitch-audit discipline from the live stress-test sessions
|
|
13
63
|
- Verification: `292 passed`, `npm pack --dry-run` passed, live set diagnostics succeeded, analyzer bridge streamed on the master track, and conflict reproduction now reports the competing client PID
|
|
64
|
+
- Fix(P1): brownian automation curve reflection loop now has 100-iteration guard with hard clamp fallback — high volatility values could previously hang the server
|
|
65
|
+
- Fix(P1): schema coercion now recurses into array `items` so `list[float]` params benefit from string-to-number widening for MCP clients that serialize as strings
|
|
66
|
+
- Fix(P1): `apply_automation_shape` and `apply_automation_recipe` now validate `parameter_type` and required companion params before sending to Ableton
|
|
67
|
+
- Fix(P2): Remote Script `AssertionError` fallbacks now return STATE_ERROR instead of running LOM calls on the TCP thread during ControlSurface disconnection
|
|
68
|
+
- Fix(P2): M4L bridge ping version corrected to 1.9.11; `check_flucoma` now probes disk for FluCoMa package instead of returning hardcoded `true`
|
|
69
|
+
- Verification: deep audit across 45+ files (3 parallel agents), 292 unit tests + 15 live integration tests against Ableton session, all passing
|
|
70
|
+
|
|
14
71
|
## 1.9.10 — Analyzer Capture Finalization + Release Sync (March 2026)
|
|
15
72
|
|
|
16
73
|
**Live-tested in Ableton after a full analyzer rebuild and master-track validation.**
|
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
```
|
|
2
|
+
██╗ ██╗██╗ ██╗███████╗██████╗ ██╗██╗ ██████╗ ████████╗
|
|
3
|
+
██║ ██║██║ ██║██╔════╝██╔══██╗██║██║ ██╔═══██╗╚══██╔══╝
|
|
4
|
+
██║ ██║██║ ██║█████╗ ██████╔╝██║██║ ██║ ██║ ██║
|
|
5
|
+
██║ ██║╚██╗ ██╔╝██╔══╝ ██╔═══╝ ██║██║ ██║ ██║ ██║
|
|
6
|
+
███████╗██║ ╚████╔╝ ███████╗██║ ██║███████╗╚██████╔╝ ██║
|
|
7
|
+
╚══════╝╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝
|
|
8
|
+
```
|
|
8
9
|
|
|
9
10
|
<p align="center">
|
|
10
11
|
<a href="https://github.com/dreamrec/LivePilot/actions"><img src="https://img.shields.io/github/actions/workflow/status/dreamrec/LivePilot/ci.yml?style=flat-square&label=CI" alt="CI"></a>
|
|
@@ -90,7 +91,7 @@ All three feed into 178 deterministic tools that execute on Ableton's main threa
|
|
|
90
91
|
| Tracks | 17 | create MIDI/audio/return, delete, duplicate, arm, mute, solo, color, freeze, flatten |
|
|
91
92
|
| Clips | 11 | create, delete, duplicate, fire, stop, loop, launch mode, warp mode, quantize |
|
|
92
93
|
| Notes | 8 | add/get/remove/modify MIDI notes, transpose, duplicate, per-note probability |
|
|
93
|
-
| Devices |
|
|
94
|
+
| Devices | 15 | load by name or URI, get/set parameters, batch edit, racks, chains, presets, plugin deep control |
|
|
94
95
|
| Scenes | 12 | create, delete, duplicate, fire, name, color, tempo, scene matrix |
|
|
95
96
|
| Browser | 4 | search library, browse tree, load items, filter by category |
|
|
96
97
|
| Mixing | 11 | volume, pan, sends, routing, meters, return tracks, master, full mix snapshot |
|
|
@@ -98,13 +99,13 @@ All three feed into 178 deterministic tools that execute on Ableton's main threa
|
|
|
98
99
|
|
|
99
100
|
<br>
|
|
100
101
|
|
|
101
|
-
### Perception —
|
|
102
|
+
### Perception — 29 tools `[M4L]`
|
|
102
103
|
|
|
103
104
|
The M4L Analyzer sits on the master track. UDP 9880 carries spectral data
|
|
104
105
|
from Max to the server. OSC 9881 sends commands back.
|
|
105
106
|
|
|
106
107
|
> [!TIP]
|
|
107
|
-
> All
|
|
108
|
+
> All 149 core tools work without the analyzer — it adds 29 more and closes the feedback loop.
|
|
108
109
|
|
|
109
110
|
```
|
|
110
111
|
SPECTRAL ─────── 8-band frequency decomposition (sub → air)
|
package/bin/livepilot.js
CHANGED
|
@@ -95,7 +95,7 @@ function ensureVenv(systemPython, prefixArgs) {
|
|
|
95
95
|
// Check if venv already exists and has our deps
|
|
96
96
|
if (fs.existsSync(venvPy)) {
|
|
97
97
|
try {
|
|
98
|
-
execFileSync(venvPy, ["-c", "import fastmcp; import midiutil; import pretty_midi; import numpy; import pyloudnorm; import soundfile; import scipy"], {
|
|
98
|
+
execFileSync(venvPy, ["-c", "import fastmcp; import midiutil; import pretty_midi; import numpy; import pyloudnorm; import soundfile; import scipy; import mutagen"], {
|
|
99
99
|
encoding: "utf-8",
|
|
100
100
|
timeout: 10000,
|
|
101
101
|
stdio: "pipe",
|
|
@@ -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"
|
|
@@ -138,8 +138,8 @@ Never skip levels. The user's question determines the entry point, but always st
|
|
|
138
138
|
### Transport (12)
|
|
139
139
|
`get_session_info` · `set_tempo` · `set_time_signature` · `start_playback` · `stop_playback` · `continue_playback` · `toggle_metronome` · `set_session_loop` · `undo` · `redo` · `get_recent_actions` · `get_session_diagnostics`
|
|
140
140
|
|
|
141
|
-
### Tracks (
|
|
142
|
-
`get_track_info` · `create_midi_track` · `create_audio_track` · `create_return_track` · `delete_track` · `duplicate_track` · `set_track_name` · `set_track_color` · `set_track_mute` · `set_track_solo` · `set_track_arm` · `stop_track_clips` · `set_group_fold` · `set_track_input_monitoring`
|
|
141
|
+
### Tracks (17)
|
|
142
|
+
`get_track_info` · `create_midi_track` · `create_audio_track` · `create_return_track` · `delete_track` · `duplicate_track` · `set_track_name` · `set_track_color` · `set_track_mute` · `set_track_solo` · `set_track_arm` · `stop_track_clips` · `set_group_fold` · `set_track_input_monitoring` · `freeze_track` · `flatten_track` · `get_freeze_status`
|
|
143
143
|
|
|
144
144
|
### Clips (11)
|
|
145
145
|
`get_clip_info` · `create_clip` · `delete_clip` · `duplicate_clip` · `fire_clip` · `stop_clip` · `set_clip_name` · `set_clip_color` · `set_clip_loop` · `set_clip_launch` · `set_clip_warp_mode`
|
|
@@ -147,11 +147,11 @@ Never skip levels. The user's question determines the entry point, but always st
|
|
|
147
147
|
### Notes (8)
|
|
148
148
|
`add_notes` · `get_notes` · `remove_notes` · `remove_notes_by_id` · `modify_notes` · `duplicate_notes` · `transpose_notes` · `quantize_clip`
|
|
149
149
|
|
|
150
|
-
### Devices (
|
|
151
|
-
`get_device_info` · `get_device_parameters` · `set_device_parameter` · `batch_set_parameters` · `toggle_device` · `delete_device` · `load_device_by_uri` · `find_and_load_device` · `set_simpler_playback_mode` · `get_rack_chains` · `set_chain_volume` · `get_device_presets`
|
|
150
|
+
### Devices (15)
|
|
151
|
+
`get_device_info` · `get_device_parameters` · `set_device_parameter` · `batch_set_parameters` · `toggle_device` · `delete_device` · `load_device_by_uri` · `find_and_load_device` · `set_simpler_playback_mode` · `get_rack_chains` · `set_chain_volume` · `get_device_presets` · `get_plugin_parameters` · `map_plugin_parameter` · `get_plugin_presets`
|
|
152
152
|
|
|
153
|
-
### Scenes (
|
|
154
|
-
`get_scenes_info` · `create_scene` · `delete_scene` · `duplicate_scene` · `fire_scene` · `set_scene_name` · `set_scene_color` · `set_scene_tempo`
|
|
153
|
+
### Scenes (12)
|
|
154
|
+
`get_scenes_info` · `create_scene` · `delete_scene` · `duplicate_scene` · `fire_scene` · `set_scene_name` · `set_scene_color` · `set_scene_tempo` · `get_scene_matrix` · `fire_scene_clips` · `stop_all_clips` · `get_playing_clips`
|
|
155
155
|
|
|
156
156
|
### Mixing (11)
|
|
157
157
|
`set_track_volume` · `set_track_pan` · `set_track_send` · `get_return_tracks` · `get_master_track` · `set_master_volume` · `get_track_routing` · `set_track_routing` · `get_track_meters` · `get_master_meters` · `get_mix_snapshot`
|
|
@@ -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
|
|
|
@@ -29,12 +29,12 @@ Run this checklist EVERY time the user says "update everything", "push", "releas
|
|
|
29
29
|
## 2. Tool Count (must ALL match)
|
|
30
30
|
|
|
31
31
|
Current: **178 tools across 17 domains**.
|
|
32
|
-
Core (no M4L): **
|
|
32
|
+
Core (no M4L): **149**. Analyzer (M4L): **29**. Perception (offline): **4**.
|
|
33
33
|
|
|
34
34
|
Verify: `grep -rc "@mcp.tool" mcp_server/tools/ | grep -v ":0" | awk -F: '{sum+=$2} END{print sum}'`
|
|
35
35
|
|
|
36
36
|
Files that reference tool count:
|
|
37
|
-
- [ ] `README.md` — header, PERCEPTION section ("
|
|
37
|
+
- [ ] `README.md` — header, PERCEPTION section ("149 core...29 analyzer"), Analyzer table header "(29)", Perception table header "(4)"
|
|
38
38
|
- [ ] `package.json` → `"description"` (178 tools, 17 domains)
|
|
39
39
|
- [ ] `server.json` → `"description"`
|
|
40
40
|
- [ ] `livepilot/.Codex-plugin/plugin.json` → `"description"` (primary Codex manifest)
|
|
@@ -44,10 +44,10 @@ Files that reference tool count:
|
|
|
44
44
|
- [ ] `livepilot/skills/livepilot-core/SKILL.md` — "178 tools across 17 domains", Analyzer (29), Perception (4)
|
|
45
45
|
- [ ] `livepilot/skills/livepilot-core/references/overview.md` — "178 tools across 17 domains"
|
|
46
46
|
- [ ] `docs/manual/index.md` — domain table: Analyzer (29), Perception (4)
|
|
47
|
-
- [ ] `docs/manual/getting-started.md` — "
|
|
47
|
+
- [ ] `docs/manual/getting-started.md` — "149 core tools...29 analyzer"
|
|
48
48
|
- [ ] `docs/manual/tool-reference.md` — all domains present with correct counts
|
|
49
49
|
- [ ] `docs/TOOL_REFERENCE.md` — all domains present
|
|
50
|
-
- [ ] `docs/M4L_BRIDGE.md` — "
|
|
50
|
+
- [ ] `docs/M4L_BRIDGE.md` — "149 core tools...29 analyzer"
|
|
51
51
|
- [ ] `docs/social-banner.html`
|
|
52
52
|
- [ ] `mcp_server/tools/analyzer.py` → module docstring
|
|
53
53
|
- [ ] `tests/test_tools_contract.py` → expected total count
|
|
Binary file
|
|
@@ -84,7 +84,7 @@ function anything() {
|
|
|
84
84
|
function dispatch(cmd, args) {
|
|
85
85
|
switch(cmd) {
|
|
86
86
|
case "ping":
|
|
87
|
-
send_response({"ok": true, "version": "1.9.
|
|
87
|
+
send_response({"ok": true, "version": "1.9.13"});
|
|
88
88
|
break;
|
|
89
89
|
case "get_params":
|
|
90
90
|
cmd_get_params(args);
|
|
@@ -150,7 +150,7 @@ function dispatch(cmd, args) {
|
|
|
150
150
|
cmd_capture_stop();
|
|
151
151
|
break;
|
|
152
152
|
case "check_flucoma":
|
|
153
|
-
|
|
153
|
+
cmd_check_flucoma();
|
|
154
154
|
break;
|
|
155
155
|
// ── Phase 2: Clip & Display ──
|
|
156
156
|
case "scrub_clip":
|
|
@@ -458,6 +458,22 @@ function cmd_get_chains_deep(args) {
|
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
function cmd_check_flucoma() {
|
|
462
|
+
// Check if FluCoMa externals are installed.
|
|
463
|
+
// Max JS cannot reliably probe the object search path at runtime,
|
|
464
|
+
// so we check if the FluCoMa package folder exists on disk.
|
|
465
|
+
try {
|
|
466
|
+
var pkg_path = max.appsupportpath + "/Packages/FluidCorpusManipulation";
|
|
467
|
+
var f = new Folder(pkg_path);
|
|
468
|
+
var available = !f.end; // end === true means folder not found
|
|
469
|
+
f.close();
|
|
470
|
+
send_response({"flucoma_available": available});
|
|
471
|
+
} catch (e) {
|
|
472
|
+
// Can't probe — report unknown rather than lying
|
|
473
|
+
send_response({"flucoma_available": false, "probe_error": String(e)});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
461
477
|
function cmd_get_track_cpu(args) {
|
|
462
478
|
try {
|
|
463
479
|
var results = [];
|
|
@@ -511,6 +527,25 @@ function cmd_get_selected() {
|
|
|
511
527
|
break;
|
|
512
528
|
}
|
|
513
529
|
}
|
|
530
|
+
// Check return tracks if not found in main tracks
|
|
531
|
+
if (result.selected_track === -1) {
|
|
532
|
+
cursor_a.goto("live_set");
|
|
533
|
+
var rtc = cursor_a.getcount("return_tracks");
|
|
534
|
+
for (var j = 0; j < rtc; j++) {
|
|
535
|
+
cursor_a.goto("live_set return_tracks " + j);
|
|
536
|
+
if (cursor_a.get("name").toString() === result.selected_track_name) {
|
|
537
|
+
result.selected_track = -(j + 1); // -1, -2, ... convention
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Check master track if still not found
|
|
543
|
+
if (result.selected_track === -1) {
|
|
544
|
+
cursor_a.goto("live_set master_track");
|
|
545
|
+
if (cursor_a.get("name").toString() === result.selected_track_name) {
|
|
546
|
+
result.selected_track = -1000; // master convention
|
|
547
|
+
}
|
|
548
|
+
}
|
|
514
549
|
} catch(e) {}
|
|
515
550
|
|
|
516
551
|
// Selected scene
|
|
@@ -1067,6 +1102,10 @@ function cmd_stop_scrub(args) {
|
|
|
1067
1102
|
var path = build_track_path(track_idx) + " clip_slots " + clip_idx + " clip";
|
|
1068
1103
|
|
|
1069
1104
|
cursor_a.goto(path);
|
|
1105
|
+
if (cursor_a.id === 0) {
|
|
1106
|
+
send_response({"error": "No clip at track " + track_idx + " slot " + clip_idx});
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1070
1109
|
try {
|
|
1071
1110
|
cursor_a.call("stop_scrub");
|
|
1072
1111
|
send_response({"ok": true});
|
|
@@ -1165,6 +1204,15 @@ function cmd_capture_audio(args) {
|
|
|
1165
1204
|
var duration_ms = parseInt(args[0]) || 10000;
|
|
1166
1205
|
var requested_name = args[1] ? args[1].toString().trim() : "";
|
|
1167
1206
|
|
|
1207
|
+
// Sanitize filename — strip any directory components (defense-in-depth)
|
|
1208
|
+
if (requested_name.length > 0) {
|
|
1209
|
+
requested_name = _safe_filename(requested_name);
|
|
1210
|
+
if (!requested_name || requested_name.length === 0) {
|
|
1211
|
+
send_response({"error": "Invalid capture filename (path traversal blocked)"});
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1168
1216
|
// Generate a timestamped filename if none provided
|
|
1169
1217
|
var d = new Date();
|
|
1170
1218
|
var ts = d.getFullYear() + "_"
|
|
@@ -1172,7 +1220,7 @@ function cmd_capture_audio(args) {
|
|
|
1172
1220
|
+ pad2(d.getDate()) + "_"
|
|
1173
1221
|
+ pad2(d.getHours()) + pad2(d.getMinutes()) + pad2(d.getSeconds());
|
|
1174
1222
|
capture_filename = requested_name.length > 0 ? requested_name : ("capture_" + ts + ".wav");
|
|
1175
|
-
capture_file_path = _join_path(
|
|
1223
|
+
capture_file_path = _join_path(_get_captures_dir(), capture_filename);
|
|
1176
1224
|
|
|
1177
1225
|
// Calculate sample count from duration and current sample rate
|
|
1178
1226
|
var num_samples = Math.ceil((duration_ms / 1000.0) * current_sample_rate);
|
|
@@ -1438,6 +1486,22 @@ function build_device_path(track_idx, device_idx) {
|
|
|
1438
1486
|
}
|
|
1439
1487
|
}
|
|
1440
1488
|
|
|
1489
|
+
function _get_captures_dir() {
|
|
1490
|
+
// Stable captures directory: ~/Documents/LivePilot/captures/
|
|
1491
|
+
// max.appsupportpath = "/Users/<name>/Library/Application Support/Cycling '74"
|
|
1492
|
+
// Walk up to get home directory
|
|
1493
|
+
try {
|
|
1494
|
+
var support = max.appsupportpath;
|
|
1495
|
+
var parts = support.split("/");
|
|
1496
|
+
// /Users/<name>/Library/... → first 3 parts = /Users/<name>
|
|
1497
|
+
var home = parts.slice(0, 3).join("/");
|
|
1498
|
+
return home + "/Documents/LivePilot/captures/";
|
|
1499
|
+
} catch (e) {
|
|
1500
|
+
// Fallback to patcher directory if home detection fails
|
|
1501
|
+
return _get_patcher_dir();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1441
1505
|
function _get_patcher_dir() {
|
|
1442
1506
|
try {
|
|
1443
1507
|
var filepath = this.patcher && this.patcher.filepath ? this.patcher.filepath.toString() : "";
|
|
@@ -1450,6 +1514,16 @@ function _get_patcher_dir() {
|
|
|
1450
1514
|
}
|
|
1451
1515
|
}
|
|
1452
1516
|
|
|
1517
|
+
function _safe_filename(name) {
|
|
1518
|
+
// Strip directory components and reject traversal attempts.
|
|
1519
|
+
// This is defense-in-depth — Python should sanitize first.
|
|
1520
|
+
if (!name || name.length === 0) return name;
|
|
1521
|
+
var slash = Math.max(name.lastIndexOf("/"), name.lastIndexOf("\\"));
|
|
1522
|
+
if (slash >= 0) name = name.substring(slash + 1);
|
|
1523
|
+
if (name === "." || name === ".." || name.length === 0) return "";
|
|
1524
|
+
return name;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1453
1527
|
function _join_path(dir, file) {
|
|
1454
1528
|
if (!dir) return file;
|
|
1455
1529
|
var last = dir.charAt(dir.length - 1);
|
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"
|
package/mcp_server/connection.py
CHANGED
|
@@ -213,7 +213,7 @@ class AbletonConnection:
|
|
|
213
213
|
raise AbletonConnectionError(f"Failed to send command: {exc}") from exc
|
|
214
214
|
|
|
215
215
|
# Read until newline, preserving any trailing bytes in _recv_buf
|
|
216
|
-
buf =
|
|
216
|
+
buf = self._recv_buf
|
|
217
217
|
try:
|
|
218
218
|
while b"\n" not in buf:
|
|
219
219
|
chunk = self._socket.recv(4096)
|
|
@@ -222,6 +222,10 @@ class AbletonConnection:
|
|
|
222
222
|
self.disconnect()
|
|
223
223
|
raise AbletonConnectionError("Connection closed by Ableton")
|
|
224
224
|
buf += chunk
|
|
225
|
+
if len(buf) > 10 * 1024 * 1024: # 10 MB
|
|
226
|
+
self._recv_buf = b""
|
|
227
|
+
self.disconnect()
|
|
228
|
+
raise AbletonConnectionError("Response too large (>10 MB)")
|
|
225
229
|
except socket.timeout as exc:
|
|
226
230
|
self._recv_buf = buf
|
|
227
231
|
self.disconnect()
|
package/mcp_server/curves.py
CHANGED
|
@@ -366,11 +366,15 @@ def _brownian(duration: float, density: int, start: float = 0.5,
|
|
|
366
366
|
step = drift / density + volatility * _det_random(i, seed)
|
|
367
367
|
value += step
|
|
368
368
|
# Soft boundary reflection (bounce off 0/1 until within range)
|
|
369
|
-
|
|
369
|
+
for _ in range(100):
|
|
370
|
+
if 0.0 <= value <= 1.0:
|
|
371
|
+
break
|
|
370
372
|
if value > 1.0:
|
|
371
373
|
value = 2.0 - value
|
|
372
374
|
if value < 0.0:
|
|
373
375
|
value = -value
|
|
376
|
+
else:
|
|
377
|
+
value = max(0.0, min(1.0, value))
|
|
374
378
|
return points
|
|
375
379
|
|
|
376
380
|
|
package/mcp_server/m4l_bridge.py
CHANGED
|
@@ -141,6 +141,7 @@ class SpectralReceiver(asyncio.DatagramProtocol):
|
|
|
141
141
|
def __init__(self, cache: SpectralCache):
|
|
142
142
|
self.cache = cache
|
|
143
143
|
self._chunks: dict[str, dict] = {} # Reassembly buffer for chunked responses
|
|
144
|
+
self._chunk_times: dict[str, float] = {} # Monotonic timestamp per chunk sequence
|
|
144
145
|
self._chunk_id = 0
|
|
145
146
|
self._response_callback: Optional[asyncio.Future] = None
|
|
146
147
|
self._capture_future: Optional[asyncio.Future] = None
|
|
@@ -284,6 +285,7 @@ class SpectralReceiver(asyncio.DatagramProtocol):
|
|
|
284
285
|
key = str(self._chunk_id)
|
|
285
286
|
if key not in self._chunks:
|
|
286
287
|
self._chunks[key] = {"parts": {}, "total": total}
|
|
288
|
+
self._chunk_times[key] = time.monotonic()
|
|
287
289
|
|
|
288
290
|
self._chunks[key]["parts"][index] = encoded
|
|
289
291
|
|
|
@@ -293,8 +295,16 @@ class SpectralReceiver(asyncio.DatagramProtocol):
|
|
|
293
295
|
for i in range(total):
|
|
294
296
|
full += self._chunks[key]["parts"][i]
|
|
295
297
|
del self._chunks[key]
|
|
298
|
+
self._chunk_times.pop(key, None)
|
|
296
299
|
self._handle_response(full)
|
|
297
300
|
|
|
301
|
+
# Evict incomplete chunk sequences older than 30 seconds
|
|
302
|
+
now = time.monotonic()
|
|
303
|
+
stale = [k for k, t in self._chunk_times.items() if now - t > 30.0]
|
|
304
|
+
for k in stale:
|
|
305
|
+
self._chunks.pop(k, None)
|
|
306
|
+
self._chunk_times.pop(k, None)
|
|
307
|
+
|
|
298
308
|
def _handle_capture_complete(self, encoded: str) -> None:
|
|
299
309
|
"""Decode a /capture_complete OSC message and resolve _capture_future."""
|
|
300
310
|
try:
|
|
@@ -359,30 +369,37 @@ class M4LBridge:
|
|
|
359
369
|
if not self.cache.is_connected:
|
|
360
370
|
return {"error": "LivePilot Analyzer not connected. Drop it on the master track."}
|
|
361
371
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
self.receiver._capture_future.
|
|
372
|
+
async with self._cmd_lock:
|
|
373
|
+
# Cancel any stale capture future before creating a new one
|
|
374
|
+
if self.receiver and self.receiver._capture_future and not self.receiver._capture_future.done():
|
|
375
|
+
self.receiver._capture_future.cancel()
|
|
365
376
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
377
|
+
loop = asyncio.get_running_loop()
|
|
378
|
+
future = loop.create_future()
|
|
379
|
+
if self.receiver:
|
|
380
|
+
self.receiver.set_capture_future(future)
|
|
370
381
|
|
|
371
|
-
|
|
372
|
-
|
|
382
|
+
osc_data = self._build_osc(command, args)
|
|
383
|
+
self._sock.sendto(osc_data, self._m4l_addr)
|
|
373
384
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
385
|
+
try:
|
|
386
|
+
result = await asyncio.wait_for(future, timeout=timeout)
|
|
387
|
+
return result
|
|
388
|
+
except asyncio.TimeoutError:
|
|
389
|
+
# Clean up the dangling future
|
|
390
|
+
if self.receiver:
|
|
391
|
+
self.receiver._capture_future = None
|
|
392
|
+
return {"error": "M4L capture timeout — device may be busy or removed"}
|
|
382
393
|
|
|
383
|
-
def cancel_capture_future(self) -> None:
|
|
384
|
-
"""Cancel any in-progress capture future (called by capture_stop).
|
|
385
|
-
|
|
394
|
+
async def cancel_capture_future(self) -> None:
|
|
395
|
+
"""Cancel any in-progress capture future (called by capture_stop).
|
|
396
|
+
|
|
397
|
+
Does NOT acquire _cmd_lock — send_capture holds it during recording.
|
|
398
|
+
Cancelling the future causes send_capture's wait_for to raise
|
|
399
|
+
CancelledError, which releases the lock naturally.
|
|
400
|
+
"""
|
|
401
|
+
if self.receiver and self.receiver._capture_future \
|
|
402
|
+
and not self.receiver._capture_future.done():
|
|
386
403
|
self.receiver._capture_future.cancel()
|
|
387
404
|
self.receiver._capture_future = None
|
|
388
405
|
|