livepilot 1.1.1 → 1.1.2
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 +9 -0
- package/README.md +1 -1
- package/installer/paths.js +2 -2
- package/mcp_server/tools/arrangement.py +10 -6
- package/package.json +2 -2
- package/plugin/plugin.json +2 -2
- package/remote_script/LivePilot/arrangement.py +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.2 — 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- Added missing `clip_index` validation to 4 arrangement MCP tools
|
|
7
|
+
- Standardized `json` import (removed `_json` alias)
|
|
8
|
+
- Added index range to arrangement clip error messages for consistency
|
|
9
|
+
- Made installer version-agnostic (`/^Live \d+/` regex instead of hardcoded "Live 12")
|
|
10
|
+
- Standardized project description across all config files
|
|
11
|
+
|
|
3
12
|
## 1.1.1 — 2026-03-17
|
|
4
13
|
|
|
5
14
|
### Fixes
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://github.com/dreamrec/LivePilot/actions/workflows/ci.yml)
|
|
12
12
|
[](https://github.com/dreamrec/LivePilot/stargazers)
|
|
13
13
|
|
|
14
|
-
**AI copilot for Ableton Live 12** — 91 MCP tools for
|
|
14
|
+
**AI copilot for Ableton Live 12** — 91 MCP tools for music production, sound design, and mixing.
|
|
15
15
|
|
|
16
16
|
Control your entire Ableton session through natural language. Create tracks, program MIDI, load instruments, tweak parameters, arrange songs, and mix — all without leaving the keyboard.
|
|
17
17
|
|
package/installer/paths.js
CHANGED
|
@@ -26,7 +26,7 @@ function findAbletonPaths() {
|
|
|
26
26
|
try {
|
|
27
27
|
const entries = fs.readdirSync(prefsDir);
|
|
28
28
|
for (const entry of entries) {
|
|
29
|
-
if (
|
|
29
|
+
if (/^Live \d+/.test(entry)) {
|
|
30
30
|
candidates.push({
|
|
31
31
|
path: path.join(prefsDir, entry, "User Remote Scripts"),
|
|
32
32
|
description: `Ableton Preferences (${entry})`,
|
|
@@ -53,7 +53,7 @@ function findAbletonPaths() {
|
|
|
53
53
|
try {
|
|
54
54
|
const entries = fs.readdirSync(abletonAppData);
|
|
55
55
|
for (const entry of entries) {
|
|
56
|
-
if (
|
|
56
|
+
if (/^Live \d+/.test(entry)) {
|
|
57
57
|
candidates.push({
|
|
58
58
|
path: path.join(abletonAppData, entry, "Preferences", "User Remote Scripts"),
|
|
59
59
|
description: `Ableton AppData (${entry})`,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
19 tools matching the Remote Script arrangement domain.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import json
|
|
6
|
+
import json
|
|
7
7
|
|
|
8
8
|
from fastmcp import Context
|
|
9
9
|
|
|
@@ -147,8 +147,9 @@ def add_arrangement_notes(
|
|
|
147
147
|
start_time in notes is relative to the clip start, not the song timeline.
|
|
148
148
|
"""
|
|
149
149
|
_validate_track_index(track_index)
|
|
150
|
+
_validate_clip_index(clip_index)
|
|
150
151
|
if isinstance(notes, str):
|
|
151
|
-
notes =
|
|
152
|
+
notes = json.loads(notes)
|
|
152
153
|
return _get_ableton(ctx).send_command("add_arrangement_notes", {
|
|
153
154
|
"track_index": track_index,
|
|
154
155
|
"clip_index": clip_index,
|
|
@@ -181,6 +182,7 @@ def set_arrangement_automation(
|
|
|
181
182
|
For parameter_type="send": send_index required (0=A, 1=B, ...).
|
|
182
183
|
"""
|
|
183
184
|
_validate_track_index(track_index)
|
|
185
|
+
_validate_clip_index(clip_index)
|
|
184
186
|
if parameter_type not in ("device", "volume", "panning", "send"):
|
|
185
187
|
raise ValueError("parameter_type must be 'device', 'volume', 'panning', or 'send'")
|
|
186
188
|
if parameter_type == "device":
|
|
@@ -189,7 +191,7 @@ def set_arrangement_automation(
|
|
|
189
191
|
if parameter_type == "send" and send_index is None:
|
|
190
192
|
raise ValueError("send_index required for parameter_type='send'")
|
|
191
193
|
if isinstance(points, str):
|
|
192
|
-
points =
|
|
194
|
+
points = json.loads(points)
|
|
193
195
|
if not points:
|
|
194
196
|
raise ValueError("points list cannot be empty")
|
|
195
197
|
params: dict = {
|
|
@@ -224,6 +226,7 @@ def transpose_arrangement_notes(
|
|
|
224
226
|
time_span: length of note range in beats (defaults to full clip)
|
|
225
227
|
"""
|
|
226
228
|
_validate_track_index(track_index)
|
|
229
|
+
_validate_clip_index(clip_index)
|
|
227
230
|
if not -127 <= semitones <= 127:
|
|
228
231
|
raise ValueError("semitones must be between -127 and 127")
|
|
229
232
|
params: dict = {
|
|
@@ -248,6 +251,7 @@ def set_arrangement_clip_name(
|
|
|
248
251
|
) -> dict:
|
|
249
252
|
"""Rename an arrangement clip by its index in the track's arrangement_clips list."""
|
|
250
253
|
_validate_track_index(track_index)
|
|
254
|
+
_validate_clip_index(clip_index)
|
|
251
255
|
if not name.strip():
|
|
252
256
|
raise ValueError("name cannot be empty")
|
|
253
257
|
return _get_ableton(ctx).send_command("set_arrangement_clip_name", {
|
|
@@ -333,7 +337,7 @@ def remove_arrangement_notes_by_id(
|
|
|
333
337
|
_validate_track_index(track_index)
|
|
334
338
|
_validate_clip_index(clip_index)
|
|
335
339
|
if isinstance(note_ids, str):
|
|
336
|
-
note_ids =
|
|
340
|
+
note_ids = json.loads(note_ids)
|
|
337
341
|
if not note_ids:
|
|
338
342
|
raise ValueError("note_ids list cannot be empty")
|
|
339
343
|
return _get_ableton(ctx).send_command("remove_arrangement_notes_by_id", {
|
|
@@ -355,7 +359,7 @@ def modify_arrangement_notes(
|
|
|
355
359
|
_validate_track_index(track_index)
|
|
356
360
|
_validate_clip_index(clip_index)
|
|
357
361
|
if isinstance(modifications, str):
|
|
358
|
-
modifications =
|
|
362
|
+
modifications = json.loads(modifications)
|
|
359
363
|
if not modifications:
|
|
360
364
|
raise ValueError("modifications list cannot be empty")
|
|
361
365
|
for mod in modifications:
|
|
@@ -388,7 +392,7 @@ def duplicate_arrangement_notes(
|
|
|
388
392
|
_validate_track_index(track_index)
|
|
389
393
|
_validate_clip_index(clip_index)
|
|
390
394
|
if isinstance(note_ids, str):
|
|
391
|
-
note_ids =
|
|
395
|
+
note_ids = json.loads(note_ids)
|
|
392
396
|
if not note_ids:
|
|
393
397
|
raise ValueError("note_ids list cannot be empty")
|
|
394
398
|
return _get_ableton(ctx).send_command("duplicate_arrangement_notes", {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "AI copilot for Ableton Live 12 — 91 MCP tools for production, sound design, and mixing",
|
|
5
|
+
"description": "AI copilot for Ableton Live 12 — 91 MCP tools for music production, sound design, and mixing",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"type": "commonjs",
|
package/plugin/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "AI copilot for Ableton Live 12 — 91 tools for production, sound design, and mixing",
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"description": "AI copilot for Ableton Live 12 — 91 MCP tools for music production, sound design, and mixing",
|
|
5
5
|
"author": "Pilot Studio",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills/livepilot-core"
|
|
@@ -583,7 +583,10 @@ def set_arrangement_clip_name(song, params):
|
|
|
583
583
|
track = get_track(song, track_index)
|
|
584
584
|
arr_clips = list(track.arrangement_clips)
|
|
585
585
|
if clip_index < 0 or clip_index >= len(arr_clips):
|
|
586
|
-
raise IndexError(
|
|
586
|
+
raise IndexError(
|
|
587
|
+
"Arrangement clip index %d out of range (0..%d)"
|
|
588
|
+
% (clip_index, len(arr_clips) - 1)
|
|
589
|
+
)
|
|
587
590
|
arr_clips[clip_index].name = name
|
|
588
591
|
return {"track_index": track_index, "clip_index": clip_index, "name": name}
|
|
589
592
|
|