livepilot 1.14.1 → 1.15.0-beta
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/CHANGELOG.md +43 -1
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/runtime/live_version.py +4 -2
- package/mcp_server/runtime/remote_commands.py +2 -0
- package/mcp_server/tools/analyzer.py +113 -5
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +2 -1
- package/remote_script/LivePilot/simpler_sample.py +98 -0
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.15.0-beta — Live 12.4 replace_sample native (April 21 2026)
|
|
4
|
+
|
|
5
|
+
First Live 12.4 beta support release. Adds a native fast path for
|
|
6
|
+
SimplerDevice.replace_sample(path) while preserving 100% backward
|
|
7
|
+
compatibility for 12.0-12.3.x users.
|
|
8
|
+
|
|
9
|
+
**Tool count**: unchanged at 403.
|
|
10
|
+
**Domain count**: unchanged at 52.
|
|
11
|
+
**Tests**: 2503 passed, 1 skipped, 0 regressions (+13 new from this release).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **Live 12.4 support (beta):** `SimplerDevice.replace_sample(path)` native
|
|
15
|
+
LOM path is now used automatically on Live 12.4+. Handles empty Simplers —
|
|
16
|
+
fixes the long-standing workaround documented in
|
|
17
|
+
`feedback_load_browser_item_is_source_of_truth.md`.
|
|
18
|
+
- New capability tier `"collaborative"` (Live 12.4+) exposed via
|
|
19
|
+
`LiveVersionCapabilities.capability_tier` and `.has_replace_sample_native`.
|
|
20
|
+
- Remote Script: new `replace_sample_native` handler.
|
|
21
|
+
- MCP server: new `_live_caps(ctx)` helper with lazy version-capability
|
|
22
|
+
caching on the lifespan context.
|
|
23
|
+
- Registered `replace_sample_native` in `mcp_server/runtime/remote_commands.py`
|
|
24
|
+
REMOTE_COMMANDS (required by the boundary audit contract).
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `replace_simpler_sample` and `load_sample_to_simpler` now route to the
|
|
28
|
+
native path when available and fall back to the M4L-bridge path
|
|
29
|
+
otherwise. Tool signatures, argument names, and return shapes unchanged.
|
|
30
|
+
|
|
31
|
+
### Backward Compatibility
|
|
32
|
+
- Live 12.0–12.3.x: zero behavior change. All routing still goes through
|
|
33
|
+
the M4L bridge.
|
|
34
|
+
- Live 12.4+: native path preferred; bridge used only on fallback.
|
|
35
|
+
|
|
36
|
+
### Verification status
|
|
37
|
+
- Full test suite: 2503 passed, 0 failures.
|
|
38
|
+
- Backward compat on Live 12.4: verified in-session — `replace_simpler_sample`
|
|
39
|
+
and `load_sample_to_simpler` both work via the bridge path on 12.4 (legacy
|
|
40
|
+
flow intact).
|
|
41
|
+
- Native E2E on Live 12.4 empty-Simpler case: deferred until the plugin
|
|
42
|
+
swap activates the worktree's MCP code. Unit tests prove the routing
|
|
43
|
+
logic and the native handler wiring.
|
|
44
|
+
|
|
3
45
|
## 1.14.1 — reload_handlers workflow + device/mixing fixes (April 21 2026)
|
|
4
46
|
|
|
5
47
|
Patch release that lands the post-1.14.0 audit work: one new diagnostics
|
|
@@ -13,7 +55,7 @@ tool, three bug fixes, and a new plugin-sync verification script.
|
|
|
13
55
|
|
|
14
56
|
- Replaces the manual "toggle Control Surface in Live → Preferences →
|
|
15
57
|
Link/MIDI" step that every Remote Script edit required. New workflow:
|
|
16
|
-
after `
|
|
58
|
+
after `npx livepilot --install`, call `reload_handlers` via the MCP
|
|
17
59
|
tool. The Remote Script side uses `pkgutil` + `importlib.reload()` to
|
|
18
60
|
re-fire all `@register` decorators in place in <1s, without dropping
|
|
19
61
|
the MCP TCP connection on port 9878.
|
|
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.
|
|
2
|
+
__version__ = "1.15.0-beta"
|
|
@@ -79,8 +79,10 @@ class LiveVersionCapabilities:
|
|
|
79
79
|
|
|
80
80
|
@property
|
|
81
81
|
def capability_tier(self) -> str:
|
|
82
|
-
"""Human-readable tier: core | enhanced_arrangement | full_intelligence."""
|
|
83
|
-
if self._version_tuple >= (12,
|
|
82
|
+
"""Human-readable tier: core | enhanced_arrangement | full_intelligence | collaborative."""
|
|
83
|
+
if self._version_tuple >= (12, 4, 0):
|
|
84
|
+
return "collaborative"
|
|
85
|
+
elif self._version_tuple >= (12, 3, 0):
|
|
84
86
|
return "full_intelligence"
|
|
85
87
|
elif self._version_tuple >= (12, 1, 10):
|
|
86
88
|
return "enhanced_arrangement"
|
|
@@ -117,6 +117,8 @@ REMOTE_COMMANDS: frozenset[str] = frozenset({
|
|
|
117
117
|
"get_appointed_device",
|
|
118
118
|
# ping (built-in)
|
|
119
119
|
"ping",
|
|
120
|
+
# Live 12.4+ native Simpler sample replacement (Collaborative tier)
|
|
121
|
+
"replace_sample_native",
|
|
120
122
|
})
|
|
121
123
|
|
|
122
124
|
# M4L bridge commands — routed through TCP but handled by livepilot_bridge.js
|
|
@@ -64,6 +64,71 @@ def _enrich_slice_response(response: Optional[dict]) -> Optional[dict]:
|
|
|
64
64
|
return enriched
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def _live_caps(ctx):
|
|
68
|
+
"""Read (or lazily compute + cache) LiveVersionCapabilities on the context.
|
|
69
|
+
|
|
70
|
+
On first call, queries Ableton via get_session_info and caches the
|
|
71
|
+
result in ``ctx.lifespan_context["_live_caps"]``. Subsequent calls
|
|
72
|
+
short-circuit to the cache. If Ableton is unreachable, falls back to
|
|
73
|
+
12.0.0 (conservative — all new-version gates return False).
|
|
74
|
+
|
|
75
|
+
This mirrors the on-demand pattern in
|
|
76
|
+
mcp_server/runtime/capability_probe.py and avoids adding a startup
|
|
77
|
+
round-trip to the lifespan.
|
|
78
|
+
"""
|
|
79
|
+
from mcp_server.runtime.live_version import LiveVersionCapabilities
|
|
80
|
+
|
|
81
|
+
lsc = ctx.lifespan_context
|
|
82
|
+
cached = lsc.get("_live_caps")
|
|
83
|
+
if cached is not None:
|
|
84
|
+
return cached
|
|
85
|
+
|
|
86
|
+
version_str = "12.0.0"
|
|
87
|
+
ableton = lsc.get("ableton")
|
|
88
|
+
if ableton is not None:
|
|
89
|
+
try:
|
|
90
|
+
info = ableton.send_command("get_session_info") or {}
|
|
91
|
+
version_str = info.get("live_version", "12.0.0")
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
caps = LiveVersionCapabilities.from_version_string(version_str)
|
|
96
|
+
lsc["_live_caps"] = caps
|
|
97
|
+
return caps
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def _try_native_replace_sample(ctx, track_index: int, device_index: int,
|
|
101
|
+
file_path: str):
|
|
102
|
+
"""Attempt the Live 12.4+ native SimplerDevice.replace_sample path.
|
|
103
|
+
|
|
104
|
+
Returns the remote-script response dict on success, or None if the
|
|
105
|
+
native path is unavailable (pre-12.4) or failed (caller should fall
|
|
106
|
+
back to the M4L-bridge path).
|
|
107
|
+
|
|
108
|
+
A native "failure" is any of: gate closed, dispatch exception, non-dict
|
|
109
|
+
response, error field present, or missing sample_loaded flag.
|
|
110
|
+
"""
|
|
111
|
+
caps = _live_caps(ctx)
|
|
112
|
+
if not caps.has_replace_sample_native:
|
|
113
|
+
return None
|
|
114
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
115
|
+
try:
|
|
116
|
+
resp = ableton.send_command("replace_sample_native", {
|
|
117
|
+
"track_index": track_index,
|
|
118
|
+
"device_index": device_index,
|
|
119
|
+
"file_path": file_path,
|
|
120
|
+
})
|
|
121
|
+
except Exception:
|
|
122
|
+
return None
|
|
123
|
+
if not isinstance(resp, dict):
|
|
124
|
+
return None
|
|
125
|
+
if "error" in resp:
|
|
126
|
+
return None
|
|
127
|
+
if not resp.get("sample_loaded"):
|
|
128
|
+
return None
|
|
129
|
+
return resp
|
|
130
|
+
|
|
131
|
+
|
|
67
132
|
@mcp.tool()
|
|
68
133
|
async def reconnect_bridge(ctx: Context) -> dict:
|
|
69
134
|
"""Attempt to reconnect the M4L UDP bridge (port 9880).
|
|
@@ -350,12 +415,27 @@ async def replace_simpler_sample(
|
|
|
350
415
|
_require_analyzer(cache)
|
|
351
416
|
bridge = _get_m4l(ctx)
|
|
352
417
|
ableton = ctx.lifespan_context["ableton"]
|
|
418
|
+
|
|
419
|
+
# Live 12.4+: prefer the native SimplerDevice.replace_sample path.
|
|
420
|
+
native = await _try_native_replace_sample(
|
|
421
|
+
ctx, track_index, device_index, file_path
|
|
422
|
+
)
|
|
423
|
+
if native is not None:
|
|
424
|
+
hygiene = await _simpler_post_load_hygiene(
|
|
425
|
+
bridge, ableton, track_index, device_index, file_path
|
|
426
|
+
)
|
|
427
|
+
if not hygiene.get("verified"):
|
|
428
|
+
return hygiene
|
|
429
|
+
result = dict(native)
|
|
430
|
+
result.update(hygiene)
|
|
431
|
+
result["method"] = "native_12_4" # preserved in case hygiene ever adds its own key
|
|
432
|
+
return result
|
|
433
|
+
|
|
434
|
+
# Pre-12.4 fallback: M4L bridge path (unchanged behavior).
|
|
353
435
|
result = await bridge.send_command(
|
|
354
436
|
"replace_simpler_sample", track_index, device_index, file_path
|
|
355
437
|
)
|
|
356
438
|
|
|
357
|
-
# Validate the response — the bridge may report success even when the
|
|
358
|
-
# sample silently failed to load (e.g., empty Simpler, bad path)
|
|
359
439
|
if "error" in result:
|
|
360
440
|
return result
|
|
361
441
|
if not result.get("sample_loaded"):
|
|
@@ -364,8 +444,6 @@ async def replace_simpler_sample(
|
|
|
364
444
|
"has a sample loaded — replace_sample silently fails on empty Simplers."
|
|
365
445
|
}
|
|
366
446
|
|
|
367
|
-
# Verify by reading back the device name — guards against the silent
|
|
368
|
-
# failure mode where the bridge reports success but keeps the placeholder.
|
|
369
447
|
hygiene = await _simpler_post_load_hygiene(
|
|
370
448
|
bridge, ableton, track_index, device_index, file_path
|
|
371
449
|
)
|
|
@@ -405,9 +483,39 @@ async def load_sample_to_simpler(
|
|
|
405
483
|
cache = _get_spectral(ctx)
|
|
406
484
|
_require_analyzer(cache)
|
|
407
485
|
bridge = _get_m4l(ctx)
|
|
486
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
487
|
+
|
|
488
|
+
# Live 12.4+: create an empty Simpler via insert_device, then use the
|
|
489
|
+
# native replace_sample path. Skips the dummy-sample bootstrap entirely.
|
|
490
|
+
caps = _live_caps(ctx)
|
|
491
|
+
if caps.has_replace_sample_native:
|
|
492
|
+
try:
|
|
493
|
+
ins = ableton.send_command("insert_device", {
|
|
494
|
+
"track_index": track_index,
|
|
495
|
+
"device_name": "Simpler",
|
|
496
|
+
})
|
|
497
|
+
except Exception:
|
|
498
|
+
ins = None
|
|
499
|
+
if isinstance(ins, dict) and "error" not in ins:
|
|
500
|
+
actual_device_index = ins.get("device_index", device_index)
|
|
501
|
+
native = await _try_native_replace_sample(
|
|
502
|
+
ctx, track_index, actual_device_index, file_path
|
|
503
|
+
)
|
|
504
|
+
if native is not None:
|
|
505
|
+
hygiene = await _simpler_post_load_hygiene(
|
|
506
|
+
bridge, ableton, track_index, actual_device_index, file_path
|
|
507
|
+
)
|
|
508
|
+
if not hygiene.get("verified"):
|
|
509
|
+
return hygiene
|
|
510
|
+
result = dict(native)
|
|
511
|
+
result.update(hygiene)
|
|
512
|
+
result["method"] = "native_12_4"
|
|
513
|
+
result["device_index"] = actual_device_index
|
|
514
|
+
result["track_index"] = track_index
|
|
515
|
+
return result
|
|
516
|
+
# Fall through to the legacy bootstrap path below on any failure.
|
|
408
517
|
|
|
409
518
|
# Step 1: Load a sample from the browser to create Simpler with content
|
|
410
|
-
ableton = ctx.lifespan_context["ableton"]
|
|
411
519
|
try:
|
|
412
520
|
search = ableton.send_command("search_browser", {
|
|
413
521
|
"path": "samples",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0-beta",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
5
|
"description": "Agentic production system for Ableton Live 12 — 403 tools, 52 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
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.
|
|
8
|
+
__version__ = "1.15.0-beta"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
|
@@ -26,6 +26,7 @@ from . import follow_actions # noqa: F401 — registers follow action handle
|
|
|
26
26
|
from . import grooves # noqa: F401 — registers groove pool handlers (11+)
|
|
27
27
|
from . import take_lanes # noqa: F401 — registers take lane handlers (12.0+ read, 12.2+ write)
|
|
28
28
|
from . import clip_automation # noqa: F401 — registers clip automation handlers
|
|
29
|
+
from . import simpler_sample # noqa: F401 — registers replace_sample_native (12.4+)
|
|
29
30
|
from . import version_detect # noqa: F401 — version detection
|
|
30
31
|
|
|
31
32
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# remote_script/LivePilot/simpler_sample.py
|
|
2
|
+
"""
|
|
3
|
+
LivePilot — Simpler sample replacement via the native Live 12.4 LOM API.
|
|
4
|
+
|
|
5
|
+
Exposes a ``replace_sample_native`` command that calls
|
|
6
|
+
``SimplerDevice.replace_sample(absolute_path)`` directly on the main thread.
|
|
7
|
+
Unlike the M4L-bridge path, this handler works on empty Simplers (the whole
|
|
8
|
+
reason 12.4 added the native API) and does not require a Max for Live
|
|
9
|
+
device in the Set.
|
|
10
|
+
|
|
11
|
+
Version-gated on ``replace_sample_native`` (12.4.0+). On earlier versions
|
|
12
|
+
the handler returns a STATE_ERROR; callers (MCP tools) are expected to
|
|
13
|
+
fall back to the bridge path.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .router import register
|
|
17
|
+
from .version_detect import has_feature, version_string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register("replace_sample_native")
|
|
21
|
+
def replace_sample_native(song, params):
|
|
22
|
+
"""Replace the sample in a Simpler device using the Live 12.4+ native API.
|
|
23
|
+
|
|
24
|
+
params dict keys:
|
|
25
|
+
track_index (int): 0-based index into song.tracks.
|
|
26
|
+
device_index (int): 0-based index into the track's devices.
|
|
27
|
+
file_path (str): absolute path to the audio file to load.
|
|
28
|
+
|
|
29
|
+
Returns on success:
|
|
30
|
+
sample_loaded (bool): True.
|
|
31
|
+
track_index (int): echoed from input.
|
|
32
|
+
device_index (int): echoed from input.
|
|
33
|
+
method (str): "native_12_4".
|
|
34
|
+
live_version (str): detected Live version at call time.
|
|
35
|
+
|
|
36
|
+
Returns on error:
|
|
37
|
+
error (str): human-readable message.
|
|
38
|
+
code (str): STATE_ERROR | INDEX_ERROR | INVALID_PARAM | INTERNAL.
|
|
39
|
+
"""
|
|
40
|
+
if not has_feature("replace_sample_native"):
|
|
41
|
+
return {
|
|
42
|
+
"error": (
|
|
43
|
+
"replace_sample_native requires Live 12.4+. "
|
|
44
|
+
"Detected: " + version_string() + ". "
|
|
45
|
+
"Use the M4L-bridge replace_simpler_sample path instead."
|
|
46
|
+
),
|
|
47
|
+
"code": "STATE_ERROR",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
track_index = int(params["track_index"])
|
|
52
|
+
device_index = int(params["device_index"])
|
|
53
|
+
file_path = str(params["file_path"])
|
|
54
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
55
|
+
return {
|
|
56
|
+
"error": "Invalid params: " + str(exc),
|
|
57
|
+
"code": "INVALID_PARAM",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
tracks = list(song.tracks)
|
|
61
|
+
if track_index < 0 or track_index >= len(tracks):
|
|
62
|
+
return {
|
|
63
|
+
"error": "track_index " + str(track_index) + " out of range",
|
|
64
|
+
"code": "INDEX_ERROR",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
track = tracks[track_index]
|
|
68
|
+
devices = list(track.devices)
|
|
69
|
+
if device_index < 0 or device_index >= len(devices):
|
|
70
|
+
return {
|
|
71
|
+
"error": "device_index " + str(device_index) + " out of range",
|
|
72
|
+
"code": "INDEX_ERROR",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
device = devices[device_index]
|
|
76
|
+
class_name = getattr(device, "class_name", "")
|
|
77
|
+
if class_name != "SimplerDevice":
|
|
78
|
+
return {
|
|
79
|
+
"error": "Device at [" + str(track_index) + "][" + str(device_index) + "] is "
|
|
80
|
+
+ class_name + ", not Simpler",
|
|
81
|
+
"code": "INVALID_PARAM",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
device.replace_sample(file_path)
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
return {
|
|
88
|
+
"error": "SimplerDevice.replace_sample failed: " + str(exc),
|
|
89
|
+
"code": "INTERNAL",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"sample_loaded": True,
|
|
94
|
+
"track_index": track_index,
|
|
95
|
+
"device_index": device_index,
|
|
96
|
+
"method": "native_12_4",
|
|
97
|
+
"live_version": version_string(),
|
|
98
|
+
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.15.0-beta",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.15.0-beta",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|