livepilot 1.23.2 → 1.23.4
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 +124 -0
- package/README.md +108 -10
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +39 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/cross_pack_chain.py +658 -0
- package/mcp_server/atlas/demo_story.py +700 -0
- package/mcp_server/atlas/extract_chain.py +786 -0
- package/mcp_server/atlas/macro_fingerprint.py +554 -0
- package/mcp_server/atlas/overlays.py +95 -3
- package/mcp_server/atlas/pack_aware_compose.py +1255 -0
- package/mcp_server/atlas/preset_resolver.py +238 -0
- package/mcp_server/atlas/tools.py +1001 -31
- package/mcp_server/atlas/transplant.py +1177 -0
- package/mcp_server/mix_engine/state_builder.py +44 -1
- package/mcp_server/runtime/capability_state.py +34 -3
- package/mcp_server/runtime/remote_commands.py +10 -0
- package/mcp_server/server.py +45 -24
- package/mcp_server/tools/agent_os.py +33 -9
- package/mcp_server/tools/analyzer.py +84 -23
- package/mcp_server/tools/browser.py +20 -1
- package/mcp_server/tools/devices.py +78 -11
- package/mcp_server/tools/perception.py +5 -1
- package/mcp_server/tools/tracks.py +39 -2
- package/mcp_server/user_corpus/__init__.py +48 -0
- package/mcp_server/user_corpus/manifest.py +142 -0
- package/mcp_server/user_corpus/plugin_engine/__init__.py +39 -0
- package/mcp_server/user_corpus/plugin_engine/detector.py +579 -0
- package/mcp_server/user_corpus/plugin_engine/manual.py +347 -0
- package/mcp_server/user_corpus/plugin_engine/research.py +247 -0
- package/mcp_server/user_corpus/runner.py +261 -0
- package/mcp_server/user_corpus/scanner.py +115 -0
- package/mcp_server/user_corpus/scanners/__init__.py +18 -0
- package/mcp_server/user_corpus/scanners/adg.py +79 -0
- package/mcp_server/user_corpus/scanners/als.py +144 -0
- package/mcp_server/user_corpus/scanners/amxd.py +374 -0
- package/mcp_server/user_corpus/scanners/plugin_preset.py +202 -0
- package/mcp_server/user_corpus/tools.py +904 -0
- package/mcp_server/user_corpus/wizard.py +224 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/browser.py +7 -2
- package/remote_script/LivePilot/devices.py +9 -0
- package/remote_script/LivePilot/simpler_sample.py +98 -0
- package/requirements.txt +3 -3
- package/server.json +2 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""First-run setup wizard — detects sensible scan candidates on the user's
|
|
2
|
+
filesystem and returns approval prompts the agent (in Claude Code) drives
|
|
3
|
+
through conversation.
|
|
4
|
+
|
|
5
|
+
The wizard does NOT scan anything. It surveys, returns candidates with
|
|
6
|
+
file counts, and lets the calling agent confirm each with the user before
|
|
7
|
+
adding to the manifest.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import platform
|
|
13
|
+
from dataclasses import dataclass, asdict
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Iterable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ─── Candidate categories the wizard offers ─────────────────────────────────
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class WizardCandidate:
|
|
23
|
+
"""One scannable folder the wizard surfaces for approval."""
|
|
24
|
+
category: str # "user_library_racks", "max_devices", "plugins", ...
|
|
25
|
+
suggested_id: str # e.g. "user-library-racks"
|
|
26
|
+
type: str # scanner type_id
|
|
27
|
+
path: str
|
|
28
|
+
file_count: int # estimated files (capped during enumeration)
|
|
29
|
+
sample_filenames: list[str]
|
|
30
|
+
description: str # 1-2 sentence prompt the agent reads to the user
|
|
31
|
+
recommended_default: bool # whether to auto-confirm (false = always ask)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def survey_filesystem() -> list[WizardCandidate]:
|
|
35
|
+
"""Inspect the OS-standard locations + return candidates for approval.
|
|
36
|
+
|
|
37
|
+
Does not write anything. Caller (skill/agent) walks each candidate and asks
|
|
38
|
+
the user "scan this? y/n", then calls corpus_add_source for each yes.
|
|
39
|
+
"""
|
|
40
|
+
candidates: list[WizardCandidate] = []
|
|
41
|
+
if platform.system() == "Darwin":
|
|
42
|
+
candidates.extend(_macos_candidates())
|
|
43
|
+
elif platform.system() == "Windows":
|
|
44
|
+
candidates.extend(_windows_candidates())
|
|
45
|
+
else:
|
|
46
|
+
candidates.extend(_linux_candidates())
|
|
47
|
+
return [c for c in candidates if c.file_count > 0]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _count_files(root: Path, extensions: Iterable[str], cap: int = 5000) -> tuple[int, list[str]]:
|
|
51
|
+
"""Count files matching any of the extensions under root. Return (count, samples).
|
|
52
|
+
|
|
53
|
+
Cap prevents pathological scans during the survey. Caller should still rescan
|
|
54
|
+
for real (the wizard is just for sizing + showing the user what's there).
|
|
55
|
+
"""
|
|
56
|
+
if not root.exists():
|
|
57
|
+
return 0, []
|
|
58
|
+
exts = tuple(e.lower() for e in extensions)
|
|
59
|
+
count = 0
|
|
60
|
+
samples: list[str] = []
|
|
61
|
+
try:
|
|
62
|
+
for p in root.rglob("*"):
|
|
63
|
+
try:
|
|
64
|
+
if not p.is_file() and not p.is_dir():
|
|
65
|
+
continue
|
|
66
|
+
# Bundle-style "files" (.amxd, .vst3) appear as dirs OR files
|
|
67
|
+
# depending on OS — count both
|
|
68
|
+
if p.suffix.lower() in exts:
|
|
69
|
+
count += 1
|
|
70
|
+
if len(samples) < 5:
|
|
71
|
+
samples.append(p.name)
|
|
72
|
+
if count >= cap:
|
|
73
|
+
break
|
|
74
|
+
except (PermissionError, OSError):
|
|
75
|
+
continue
|
|
76
|
+
except (PermissionError, OSError):
|
|
77
|
+
pass
|
|
78
|
+
return count, samples
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _macos_candidates() -> list[WizardCandidate]:
|
|
82
|
+
home = Path.home()
|
|
83
|
+
out: list[WizardCandidate] = []
|
|
84
|
+
|
|
85
|
+
# 1. User Library racks (.adg / .adv)
|
|
86
|
+
user_lib = home / "Music/Ableton/User Library/Presets"
|
|
87
|
+
if user_lib.exists():
|
|
88
|
+
n, samples = _count_files(user_lib, [".adg", ".adv"])
|
|
89
|
+
if n > 0:
|
|
90
|
+
out.append(WizardCandidate(
|
|
91
|
+
category="user_library_racks", suggested_id="user-library-racks",
|
|
92
|
+
type="adg", path=str(user_lib), file_count=n, sample_filenames=samples,
|
|
93
|
+
description=(
|
|
94
|
+
f"Ableton User Library — {n} rack/effect presets (.adg/.adv). "
|
|
95
|
+
"Indexes every saved chain you've made + third-party racks under "
|
|
96
|
+
"your User Library. Good first scan."
|
|
97
|
+
),
|
|
98
|
+
recommended_default=True,
|
|
99
|
+
))
|
|
100
|
+
|
|
101
|
+
# 2. Max for Live devices (.amxd) — multiple plausible locations
|
|
102
|
+
for label, p in (
|
|
103
|
+
("max_for_live_devices", home / "Documents/Max 9/Max for Live Devices"),
|
|
104
|
+
("max_for_live_devices_v8", home / "Documents/Max 8/Max for Live Devices"),
|
|
105
|
+
("user_library_m4l", home / "Music/Ableton/User Library/MAX MONTY/m4l_2024"),
|
|
106
|
+
("user_library_m4l_alt", home / "Music/Ableton/User Library/Presets/Audio Effects/Max Audio Effect"),
|
|
107
|
+
):
|
|
108
|
+
if p.exists():
|
|
109
|
+
n, samples = _count_files(p, [".amxd"])
|
|
110
|
+
if n > 0:
|
|
111
|
+
out.append(WizardCandidate(
|
|
112
|
+
category="max_devices", suggested_id=label.replace("_", "-"),
|
|
113
|
+
type="amxd", path=str(p), file_count=n, sample_filenames=samples,
|
|
114
|
+
description=(
|
|
115
|
+
f"Max for Live devices at {p.name} — {n} .amxd files. "
|
|
116
|
+
"Captures device type (audio/instrument/midi), Max version, "
|
|
117
|
+
"and any Live-exposed parameters."
|
|
118
|
+
),
|
|
119
|
+
recommended_default=True,
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
# 3. Plugin presets (.aupreset / .vstpreset / .fxp / .nksf)
|
|
123
|
+
for label, p in (
|
|
124
|
+
("au_presets", home / "Library/Audio/Presets"),
|
|
125
|
+
("vst3_presets", home / "Library/Audio/VST3 Presets"),
|
|
126
|
+
):
|
|
127
|
+
if p.exists():
|
|
128
|
+
n, samples = _count_files(p, [".aupreset", ".vstpreset", ".fxp", ".fxb", ".nksf"])
|
|
129
|
+
if n > 0:
|
|
130
|
+
out.append(WizardCandidate(
|
|
131
|
+
category="plugin_presets", suggested_id=label.replace("_", "-"),
|
|
132
|
+
type="plugin-preset", path=str(p), file_count=n, sample_filenames=samples,
|
|
133
|
+
description=(
|
|
134
|
+
f"Plugin presets at {p.name} — {n} preset files. Captures "
|
|
135
|
+
"plugin name + vendor + format. Param values are opaque per-plugin "
|
|
136
|
+
"binary (same as PluginDevice in .als)."
|
|
137
|
+
),
|
|
138
|
+
recommended_default=False, # often noisy — opt-in
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
# 4. Sample library (audio files) — .wav/.aif/.flac
|
|
142
|
+
# Note: corpus has no built-in sample scanner yet; these are advisory.
|
|
143
|
+
for label, p in (
|
|
144
|
+
("apple_loops", Path("/Library/Audio/Apple Loops")),
|
|
145
|
+
("user_apple_loops", home / "Library/Audio/Apple Loops"),
|
|
146
|
+
("user_samples", home / "Music/Samples"),
|
|
147
|
+
):
|
|
148
|
+
if p.exists():
|
|
149
|
+
n, _ = _count_files(p, [".wav", ".aif", ".aiff", ".flac"])
|
|
150
|
+
if n > 0:
|
|
151
|
+
out.append(WizardCandidate(
|
|
152
|
+
category="samples_advisory", suggested_id=label.replace("_", "-"),
|
|
153
|
+
type="sample", # NOTE: scanner not yet implemented
|
|
154
|
+
path=str(p), file_count=n, sample_filenames=[],
|
|
155
|
+
description=(
|
|
156
|
+
f"Sample library at {p.name} — {n} audio files. "
|
|
157
|
+
"(Sample scanner is not yet implemented — this is a survey "
|
|
158
|
+
"preview only. Skip for now or wait for the next build.)"
|
|
159
|
+
),
|
|
160
|
+
recommended_default=False,
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
# 5. Plugins are NOT a corpus_add_source — they're handled by
|
|
164
|
+
# corpus_detect_plugins. Surface as a separate "want to detect plugins?" step.
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _windows_candidates() -> list[WizardCandidate]:
|
|
169
|
+
home = Path.home()
|
|
170
|
+
out: list[WizardCandidate] = []
|
|
171
|
+
user_lib = home / "Documents" / "Ableton" / "User Library" / "Presets"
|
|
172
|
+
if user_lib.exists():
|
|
173
|
+
n, samples = _count_files(user_lib, [".adg", ".adv"])
|
|
174
|
+
if n > 0:
|
|
175
|
+
out.append(WizardCandidate(
|
|
176
|
+
category="user_library_racks", suggested_id="user-library-racks",
|
|
177
|
+
type="adg", path=str(user_lib), file_count=n, sample_filenames=samples,
|
|
178
|
+
description=f"Ableton User Library racks — {n} .adg/.adv presets.",
|
|
179
|
+
recommended_default=True,
|
|
180
|
+
))
|
|
181
|
+
return out
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _linux_candidates() -> list[WizardCandidate]:
|
|
185
|
+
return [] # Ableton Live isn't supported on Linux; corpus is mostly empty there
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ─── Aggregate decision packet ──────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_setup_proposal() -> dict:
|
|
192
|
+
"""Return the full first-run setup proposal: candidates + plugin-detection prompt.
|
|
193
|
+
|
|
194
|
+
Caller (skill/agent) walks each item, confirms with user, then dispatches
|
|
195
|
+
corpus_add_source / corpus_detect_plugins as approved.
|
|
196
|
+
"""
|
|
197
|
+
candidates = survey_filesystem()
|
|
198
|
+
return {
|
|
199
|
+
"candidates": [asdict(c) for c in candidates],
|
|
200
|
+
"candidate_count": len(candidates),
|
|
201
|
+
"categories": sorted({c.category for c in candidates}),
|
|
202
|
+
"plugin_detection_offer": {
|
|
203
|
+
"prompt": (
|
|
204
|
+
"Also detect installed VST3/AU/VST2/AAX plugins via "
|
|
205
|
+
"corpus_detect_plugins? This walks the OS-standard plugin "
|
|
206
|
+
"folders, parses each bundle's identity metadata, and writes "
|
|
207
|
+
"_inventory.json. Independent of the file scans above."
|
|
208
|
+
),
|
|
209
|
+
"tool": "corpus_detect_plugins",
|
|
210
|
+
"recommended_default": True,
|
|
211
|
+
},
|
|
212
|
+
"instructions": (
|
|
213
|
+
"Walk the user through each candidate one at a time. For each, "
|
|
214
|
+
"summarize file_count + path + description, then ASK 'add this?' "
|
|
215
|
+
"and only call corpus_add_source on yes. After all candidates are "
|
|
216
|
+
"decided, ask about plugin_detection_offer separately. Finally "
|
|
217
|
+
"call corpus_scan() to index everything they approved."
|
|
218
|
+
),
|
|
219
|
+
"do_not_scan": [
|
|
220
|
+
"Personal .als project folders unless the user explicitly points at one — "
|
|
221
|
+
"they're sensitive content that the user should opt into per-folder."
|
|
222
|
+
],
|
|
223
|
+
"schema_version": 1,
|
|
224
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.4",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 \u2014 453 tools, 54 domains, 44 semantic moves. Device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, 12 creative intelligence engines",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -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.23.
|
|
8
|
+
__version__ = "1.23.4"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
|
@@ -48,8 +48,13 @@ def _navigate_path(browser, path):
|
|
|
48
48
|
if not parts:
|
|
49
49
|
raise ValueError("Path cannot be empty")
|
|
50
50
|
|
|
51
|
-
# First part must be a category name
|
|
52
|
-
|
|
51
|
+
# First part must be a category name (normalise common aliases first)
|
|
52
|
+
_path_aliases = {
|
|
53
|
+
"effects": "audio_effects", "fx": "audio_effects",
|
|
54
|
+
"audio_fx": "audio_effects", "audiofx": "audio_effects",
|
|
55
|
+
"midi_fx": "midi_effects", "midifx": "midi_effects",
|
|
56
|
+
}
|
|
57
|
+
first = _path_aliases.get(parts[0].lower(), parts[0].lower())
|
|
53
58
|
if first not in categories:
|
|
54
59
|
raise ValueError(
|
|
55
60
|
"Unknown category '%s'. Available: %s"
|
|
@@ -618,11 +618,20 @@ def insert_rack_chain(song, params):
|
|
|
618
618
|
song.end_undo_step()
|
|
619
619
|
|
|
620
620
|
chain_count = len(list(device.chains))
|
|
621
|
+
# BUG-2026-04-25: callers (notably add_drum_rack_pad) expect
|
|
622
|
+
# `chain_index` in the response so they can target the new chain in
|
|
623
|
+
# subsequent set_drum_chain_note + insert_device calls. Without it,
|
|
624
|
+
# the caller falls back to chain_index=0 and overwrites/clobbers the
|
|
625
|
+
# first chain. For position=-1 (append), the new chain lives at
|
|
626
|
+
# chain_count - 1; for explicit position N (0-indexed), the new
|
|
627
|
+
# chain lives at N (insert_chain shifts later chains right).
|
|
628
|
+
new_chain_index = position if 0 <= position < chain_count else chain_count - 1
|
|
621
629
|
result = {
|
|
622
630
|
"inserted": True,
|
|
623
631
|
"track_index": track_index,
|
|
624
632
|
"device_index": device_index,
|
|
625
633
|
"chain_count": chain_count,
|
|
634
|
+
"chain_index": new_chain_index,
|
|
626
635
|
}
|
|
627
636
|
if assigned_note is not None:
|
|
628
637
|
result["assigned_pad_note"] = assigned_note
|
|
@@ -184,3 +184,101 @@ def replace_sample_native(song, params):
|
|
|
184
184
|
"method": "native_12_4",
|
|
185
185
|
"live_version": version_string(),
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@register("get_simpler_file_path")
|
|
190
|
+
def get_simpler_file_path(song, params):
|
|
191
|
+
"""Read the absolute file path of a Simpler's currently-loaded sample.
|
|
192
|
+
|
|
193
|
+
Closes the v1.12 follow-up that left ``classify_simpler_slices`` unable
|
|
194
|
+
to auto-resolve the WAV path: previously the call routed through the
|
|
195
|
+
M4L bridge ``get_simpler_file_path`` case (added in v1.23.3 JS) but
|
|
196
|
+
Live's M4L UDP response correlation produced wrong-data on the second
|
|
197
|
+
successive bridge call (a chunked-response edge case under
|
|
198
|
+
investigation in the bridge wire protocol). The Remote Script reads
|
|
199
|
+
``device.sample.file_path`` directly via Python LOM — no UDP, no Max
|
|
200
|
+
JS, no chunk reassembly, no ambiguity.
|
|
201
|
+
|
|
202
|
+
params dict keys:
|
|
203
|
+
track_index (int): 0-based index into song.tracks.
|
|
204
|
+
device_index (int): 0-based index into track.devices.
|
|
205
|
+
chain_index (int|None): optional, for nested Drum Rack chains.
|
|
206
|
+
nested_device_index (int|None): optional, position inside the chain.
|
|
207
|
+
|
|
208
|
+
Returns on success:
|
|
209
|
+
file_path (str): absolute filesystem path of the loaded sample.
|
|
210
|
+
track_index, device_index, chain_index, nested_device_index: echoed.
|
|
211
|
+
name (str): Simpler device name (typically the sample filename).
|
|
212
|
+
|
|
213
|
+
Returns on error:
|
|
214
|
+
error (str): human-readable message.
|
|
215
|
+
code (str): STATE_ERROR | INDEX_ERROR | INVALID_PARAM.
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
track_index = int(params["track_index"])
|
|
219
|
+
device_index = int(params["device_index"])
|
|
220
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
221
|
+
return {
|
|
222
|
+
"error": "Invalid params: " + str(exc),
|
|
223
|
+
"code": "INVALID_PARAM",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
chain_index = params.get("chain_index")
|
|
227
|
+
nested_device_index = params.get("nested_device_index")
|
|
228
|
+
if chain_index is not None:
|
|
229
|
+
try:
|
|
230
|
+
chain_index = int(chain_index)
|
|
231
|
+
except (TypeError, ValueError):
|
|
232
|
+
return {
|
|
233
|
+
"error": "chain_index must be an integer if provided",
|
|
234
|
+
"code": "INVALID_PARAM",
|
|
235
|
+
}
|
|
236
|
+
if nested_device_index is not None:
|
|
237
|
+
try:
|
|
238
|
+
nested_device_index = int(nested_device_index)
|
|
239
|
+
except (TypeError, ValueError):
|
|
240
|
+
return {
|
|
241
|
+
"error": "nested_device_index must be an integer if provided",
|
|
242
|
+
"code": "INVALID_PARAM",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
device, err = _resolve_simpler_device(
|
|
246
|
+
song, track_index, device_index, chain_index, nested_device_index,
|
|
247
|
+
)
|
|
248
|
+
if err is not None:
|
|
249
|
+
return err
|
|
250
|
+
|
|
251
|
+
class_name = getattr(device, "class_name", "")
|
|
252
|
+
if class_name != "OriginalSimpler":
|
|
253
|
+
return {
|
|
254
|
+
"error": "Device at resolved path is " + class_name + ", not Simpler",
|
|
255
|
+
"code": "INVALID_PARAM",
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
sample = getattr(device, "sample", None)
|
|
259
|
+
if sample is None:
|
|
260
|
+
return {
|
|
261
|
+
"error": "Simpler has no sample loaded (device.sample is None)",
|
|
262
|
+
"code": "STATE_ERROR",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
file_path = getattr(sample, "file_path", None)
|
|
266
|
+
if not file_path:
|
|
267
|
+
return {
|
|
268
|
+
"error": (
|
|
269
|
+
"Simpler.sample.file_path is empty — sample may be embedded "
|
|
270
|
+
"in the Set or otherwise lacks a filesystem path."
|
|
271
|
+
),
|
|
272
|
+
"code": "STATE_ERROR",
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
name = getattr(device, "name", "")
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"file_path": str(file_path),
|
|
279
|
+
"track_index": track_index,
|
|
280
|
+
"device_index": device_index,
|
|
281
|
+
"chain_index": chain_index,
|
|
282
|
+
"nested_device_index": nested_device_index,
|
|
283
|
+
"name": str(name),
|
|
284
|
+
}
|
package/requirements.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# LivePilot MCP Server dependencies
|
|
2
|
-
numpy>=
|
|
3
|
-
fastmcp>=3.
|
|
2
|
+
numpy>=2.4.4
|
|
3
|
+
fastmcp>=3.2.4,<3.3.0 # pinned upper bound — _get_all_tools() accesses private internals
|
|
4
4
|
midiutil>=1.2.1
|
|
5
5
|
pretty_midi>=0.2.11
|
|
6
6
|
# v1.8 Perception Layer (offline analysis)
|
|
@@ -12,7 +12,7 @@ mutagen>=1.47.0
|
|
|
12
12
|
# Without these, SpliceGRPCClient silently disables itself and search_samples
|
|
13
13
|
# falls back to the SQLite sounds.db which only returns locally downloaded
|
|
14
14
|
# samples (see docs/2026-04-14-bugs-discovered.md — P0-2).
|
|
15
|
-
grpcio>=1.
|
|
15
|
+
grpcio>=1.80.0
|
|
16
16
|
protobuf>=7.34.1
|
|
17
17
|
|
|
18
18
|
# Development / testing (not required for runtime)
|
package/server.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "453-tool agentic MCP production system for Ableton Live 12 \u2014 53 domains, 44 semantic moves, device atlas (5264 devices), Splice intelligence (gRPC + GraphQL), 9-band spectral perception auto-loaded, Creative Director skill, technique memory, 12 creative engines",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.23.
|
|
9
|
+
"version": "1.23.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|