livepilot 1.8.4 → 1.9.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
3
  "name": "dreamrec-LivePilot",
4
- "description": "Agentic MCP production system for Ableton Live 12 — 168 tools, 17 domains",
4
+ "description": "Agentic MCP production system for Ableton Live 12 — 178 tools, 17 domains",
5
5
  "owner": {
6
6
  "name": "dreamrec",
7
7
  "email": "dreamrec@users.noreply.github.com"
@@ -9,8 +9,8 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "livepilot",
12
- "description": "Agentic production system for Ableton Live 12 — 168 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
13
- "version": "1.8.4",
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.1",
14
14
  "author": {
15
15
  "name": "Pilot Studio"
16
16
  },
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # LivePilot v1.8.4 — Ableton Live 12
1
+ # LivePilot v1.9.1 — Ableton Live 12
2
2
 
3
3
  ## Project
4
4
  - **Repo:** This directory (LivePilot)
@@ -22,7 +22,7 @@
22
22
  ## Key Rules
23
23
  - ALL Live Object Model (LOM) calls must execute on Ableton's main thread via schedule_message queue
24
24
  - Live 12 minimum — use modern note API (add_new_notes, get_notes_extended, apply_note_modifications)
25
- - 168 tools across 17 domains: transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, analyzer, automation, theory, generative, harmony, midi_io, perception
25
+ - 178 tools across 17 domains: transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, analyzer, automation, theory, generative, harmony, midi_io, perception
26
26
  - JSON over TCP, newline-delimited, port 9878
27
27
  - Structured errors with codes: INDEX_ERROR, NOT_FOUND, INVALID_PARAM, STATE_ERROR, TIMEOUT, INTERNAL
28
28
 
@@ -43,4 +43,4 @@ When modifying .amxd attributes that Max editor won't persist (e.g., `openinpres
43
43
  4. Structure: 24-byte `ampf` header + `ptch` chunk + `mx@c` header + JSON patcher + frozen deps
44
44
 
45
45
  ## Tool Count
46
- Currently 168 tools. If adding/removing tools, update: README.md, package.json description, livepilot/.Codex-plugin/plugin.json, server.json, livepilot/skills/livepilot-core/SKILL.md, livepilot/skills/livepilot-core/references/overview.md, AGENTS.md, CHANGELOG.md, tests/test_tools_contract.py, docs/manual/index.md, docs/manual/tool-reference.md
46
+ Currently 178 tools. If adding/removing tools, update: README.md, package.json description, livepilot/.Codex-plugin/plugin.json, server.json, livepilot/skills/livepilot-core/SKILL.md, livepilot/skills/livepilot-core/references/overview.md, AGENTS.md, CHANGELOG.md, tests/test_tools_contract.py, docs/manual/index.md, docs/manual/tool-reference.md
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.1 — Rebuild .amxd with v1.9 Bridge (March 2026)
4
+
5
+ - Rebuild LivePilot_Analyzer.amxd in Max editor to freeze 3 new plugin parameter bridge commands
6
+ - Binary-patch `openinpresentation` for presentation mode
7
+
8
+ ## 1.9.0 — Scene Matrix, Freeze/Flatten, Plugin Deep Control (March 2026)
9
+
10
+ **10 new tools (168 → 178), 3 features shipped.**
11
+
12
+ ### Scene Matrix Operations (+4 tools)
13
+ - `get_scene_matrix` — full N×M clip grid with states (empty/stopped/playing/triggered/recording)
14
+ - `fire_scene_clips` — fire a scene with optional track filter for selective launching
15
+ - `stop_all_clips` — stop all playing clips in the session (panic button)
16
+ - `get_playing_clips` — return all currently playing or triggered clips
17
+
18
+ ### Track Freeze/Flatten (+3 tools)
19
+ - `freeze_track` — freeze a track (render devices to audio for CPU savings)
20
+ - `flatten_track` — flatten a frozen track (commit rendered audio permanently)
21
+ - `get_freeze_status` — check if a track is frozen
22
+
23
+ ### Plugin Parameter Mapping (+3 tools, M4L)
24
+ - `get_plugin_parameters` — get ALL VST/AU plugin parameters including unconfigured ones
25
+ - `map_plugin_parameter` — add a plugin parameter to Ableton's Configure list for automation
26
+ - `get_plugin_presets` — list a plugin's internal presets and banks
27
+
28
+ ### Infrastructure
29
+ - `SLOW_WRITE_COMMANDS` set for freeze_track (35s timeout vs 15s for normal writes)
30
+ - Removed "Coming" section from README — all roadmap features shipped or dropped
31
+
3
32
  ## 1.8.4 — Bug Fix Audit (March 2026)
4
33
 
5
34
  **5 bugs fixed (2 P1, 3 P2), verified live in Ableton.**
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  An agentic production system for Ableton Live 12.
11
11
 
12
- 168 tools. Device atlas. Spectral perception. Technique memory.
12
+ 178 tools. Device atlas. Spectral perception. Technique memory.
13
13
  Neo-Riemannian harmony. Euclidean rhythm. Species counterpoint.
14
14
 
15
15
 
@@ -37,7 +37,7 @@ Neo-Riemannian harmony. Euclidean rhythm. Species counterpoint.
37
37
  │ └───────────────────┼───────────────────┘ │
38
38
  │ ▼ │
39
39
  │ ┌─────────────────┐ │
40
- │ │ 168 MCP Tools │ │
40
+ │ │ 178 MCP Tools │ │
41
41
  │ │ 17 domains │ │
42
42
  │ └────────┬────────┘ │
43
43
  │ │ │
@@ -59,7 +59,7 @@ via a Max for Live device.
59
59
  The memory gives it history — a searchable library of production decisions
60
60
  that persists across sessions.
61
61
 
62
- All three feed into 168 deterministic tools that execute on Ableton's main thread
62
+ All three feed into 178 deterministic tools that execute on Ableton's main thread
63
63
  through the official Live Object Model API. Everything is reversible with undo.
64
64
 
65
65
  <br>
@@ -476,7 +476,7 @@ Check memory before creative decisions. Verify every mutation.
476
476
 
477
477
  ## Full Tool List
478
478
 
479
- 168 tools across 17 domains.
479
+ 178 tools across 17 domains.
480
480
 
481
481
  <br>
482
482
 
@@ -769,17 +769,6 @@ Check memory before creative decisions. Verify every mutation.
769
769
 
770
770
  ---
771
771
 
772
- ## Coming
773
-
774
- ```
775
- □ Plugin parameter mapping — VST/AU deep control
776
- □ Audio track freeze/flatten automation
777
- □ Clip launch scene matrix operations
778
- □ Multi-track arrangement templates
779
- ```
780
-
781
- <br>
782
-
783
772
  ---
784
773
 
785
774
  ## CLI
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.8.4",
4
- "description": "Agentic production system for Ableton Live 12 — 168 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
3
+ "version": "1.9.1",
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"
7
7
  }
@@ -1,17 +1,17 @@
1
1
  ---
2
2
  name: livepilot-core
3
- description: Core discipline for LivePilot — agentic production system for Ableton Live 12. 168 tools across 17 domains. Device atlas (280+ devices), M4L analyzer (spectrum/RMS/key detection), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O. Use whenever working with Ableton Live through MCP tools.
3
+ description: Core discipline for LivePilot — agentic production system for Ableton Live 12. 178 tools across 17 domains. Device atlas (280+ devices), M4L analyzer (spectrum/RMS/key detection), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O. Use whenever working with Ableton Live through MCP tools.
4
4
  ---
5
5
 
6
6
  # LivePilot Core — Ableton Live 12
7
7
 
8
- Agentic production system for Ableton Live 12. 168 tools across 17 domains, three layers:
8
+ Agentic production system for Ableton Live 12. 178 tools across 17 domains, three layers:
9
9
 
10
10
  - **Device Atlas** — A structured knowledge corpus of 280+ instruments, 139 drum kits, and 350+ impulse responses. Consult the atlas before loading any device. It contains real browser URIs, preset names, and sonic descriptions. Never guess a device name — look it up.
11
11
  - **M4L Analyzer** — Real-time audio analysis on the master bus (8-band spectrum, RMS/peak, key detection). Use it to verify mixing decisions, detect frequency problems, and find the key before writing harmonic content.
12
12
  - **Technique Memory** — Persistent storage for production decisions. Consult `memory_recall` before creative tasks to understand the user's taste. Save techniques when the user likes something. The memory shapes future decisions without constraining them.
13
13
 
14
- These layers sit on top of 168 deterministic tools across 17 domains: transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, analyzer, automation, theory, generative, harmony, MIDI I/O, and perception.
14
+ These layers sit on top of 178 deterministic tools across 17 domains: transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, analyzer, automation, theory, generative, harmony, MIDI I/O, and perception.
15
15
 
16
16
  ## Golden Rules
17
17
 
@@ -32,7 +32,7 @@ These layers sit on top of 168 deterministic tools across 17 domains: transport,
32
32
  Not all tools respond instantly. Know the tiers and act accordingly.
33
33
 
34
34
  ### Instant (<1s) — Use freely, no warning needed
35
- All 168 core tools (transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, automation, theory, generative, harmony, midi_io, perception) plus Layer A perception tools (spectral shape, timbral profile, mel spectrum, chroma, onsets, harmonic/percussive, novelty, momentary loudness). These are the reflex tools — call them anytime without hesitation.
35
+ All 178 core tools (transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, automation, theory, generative, harmony, midi_io, perception) plus Layer A perception tools (spectral shape, timbral profile, mel spectrum, chroma, onsets, harmonic/percussive, novelty, momentary loudness). These are the reflex tools — call them anytime without hesitation.
36
36
 
37
37
  ### Fast (1-5s) — Use freely, barely noticeable
38
38
  `analyze_loudness` · `analyze_dynamic_range` · `compare_loudness`
@@ -117,7 +117,7 @@ Never skip levels. The user's question determines the entry point, but always st
117
117
  - MIDI track with no instrument loaded
118
118
  - Notes programmed but clip not fired
119
119
 
120
- ## Tool Domains (168 total)
120
+ ## Tool Domains (178 total)
121
121
 
122
122
  ### Transport (12)
123
123
  `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`
@@ -374,7 +374,7 @@ Deep production knowledge lives in `references/`. Consult these when making crea
374
374
 
375
375
  | File | What's inside | When to consult |
376
376
  |------|--------------|-----------------|
377
- | `references/overview.md` | All 168 tools mapped with params, units, ranges | Quick lookup for any tool |
377
+ | `references/overview.md` | All 178 tools mapped with params, units, ranges | Quick lookup for any tool |
378
378
  | `references/midi-recipes.md` | Drum patterns by genre, chord voicings, scales, hi-hat techniques, humanization, polymetrics | Programming MIDI notes, building beats |
379
379
  | `references/sound-design.md` | Stock instruments/effects, parameter recipes for bass/pad/lead/pluck, device chain patterns | Loading and configuring devices |
380
380
  | `references/mixing-patterns.md` | Gain staging, parallel compression, sidechain, EQ by instrument, bus processing, stereo width | Setting volumes, panning, adding effects |
@@ -1,6 +1,6 @@
1
- # LivePilot v1.8.4 — Architecture & Tool Reference
1
+ # LivePilot v1.9.1 — Architecture & Tool Reference
2
2
 
3
- Agentic production system for Ableton Live 12. 168 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.
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
 
5
5
  ## Architecture
6
6
 
@@ -27,20 +27,20 @@ Run this checklist EVERY time the user says "update everything", "push", "releas
27
27
 
28
28
  ## 2. Tool Count (must ALL match)
29
29
 
30
- Current: **168 tools across 17 domains**.
30
+ Current: **178 tools across 17 domains**.
31
31
  Core (no M4L): **139**. Analyzer (M4L): **29**. Perception (offline): **4**.
32
32
 
33
33
  Verify: `grep -rc "@mcp.tool" mcp_server/tools/ | grep -v ":0" | awk -F: '{sum+=$2} END{print sum}'`
34
34
 
35
35
  Files that reference tool count:
36
36
  - [ ] `README.md` — header, PERCEPTION section ("139 core...29 analyzer"), Analyzer table header "(29)", Perception table header "(4)"
37
- - [ ] `package.json` → `"description"` (168 tools, 17 domains)
37
+ - [ ] `package.json` → `"description"` (178 tools, 17 domains)
38
38
  - [ ] `server.json` → `"description"`
39
39
  - [ ] `livepilot/.claude-plugin/plugin.json` → `"description"`
40
40
  - [ ] `.claude-plugin/marketplace.json` → `"description"`
41
- - [ ] `CLAUDE.md` → "168 tools across 17 domains"
42
- - [ ] `livepilot/skills/livepilot-core/SKILL.md` — "168 tools across 17 domains", Analyzer (29), Perception (4)
43
- - [ ] `livepilot/skills/livepilot-core/references/overview.md` — "168 tools across 17 domains"
41
+ - [ ] `CLAUDE.md` → "178 tools across 17 domains"
42
+ - [ ] `livepilot/skills/livepilot-core/SKILL.md` — "178 tools across 17 domains", Analyzer (29), Perception (4)
43
+ - [ ] `livepilot/skills/livepilot-core/references/overview.md` — "178 tools across 17 domains"
44
44
  - [ ] `docs/manual/index.md` — domain table: Analyzer (29), Perception (4)
45
45
  - [ ] `docs/manual/getting-started.md` — "139 core tools...29 analyzer"
46
46
  - [ ] `docs/manual/tool-reference.md` — all domains present with correct counts
@@ -86,7 +86,7 @@ Current: **17 domains**: transport, tracks, clips, notes, devices, scenes, mixin
86
86
 
87
87
  - [ ] `README.md` — features match current capabilities, "Coming" section is accurate
88
88
  - [ ] `docs/manual/getting-started.md` — install instructions current
89
- - [ ] `docs/manual/tool-reference.md` — all 17 domains listed, all 168 tools present
89
+ - [ ] `docs/manual/tool-reference.md` — all 17 domains listed, all 178 tools present
90
90
  - [ ] `docs/TOOL_REFERENCE.md` — all 17 domains present
91
91
  - [ ] `docs/M4L_BRIDGE.md` — architecture accurate, core tool count correct
92
92
 
Binary file
@@ -83,7 +83,7 @@ function anything() {
83
83
  function dispatch(cmd, args) {
84
84
  switch(cmd) {
85
85
  case "ping":
86
- send_response({"ok": true, "version": "1.8.4"});
86
+ send_response({"ok": true, "version": "1.9.1"});
87
87
  break;
88
88
  case "get_params":
89
89
  cmd_get_params(args);
@@ -161,6 +161,16 @@ function dispatch(cmd, args) {
161
161
  case "get_display_values":
162
162
  cmd_get_display_values(args);
163
163
  break;
164
+ // ── Plugin Parameters ──
165
+ case "get_plugin_params":
166
+ cmd_get_plugin_params(args);
167
+ break;
168
+ case "map_plugin_param":
169
+ cmd_map_plugin_param(args);
170
+ break;
171
+ case "get_plugin_presets":
172
+ cmd_get_plugin_presets(args);
173
+ break;
164
174
  default:
165
175
  send_response({"error": "Unknown command: " + cmd});
166
176
  }
@@ -1035,6 +1045,165 @@ function pad2(n) {
1035
1045
  return n < 10 ? "0" + n : "" + n;
1036
1046
  }
1037
1047
 
1048
+ // ── Plugin Parameters ──────────────────────────────────────────────────────
1049
+
1050
+ function cmd_get_plugin_params(args) {
1051
+ // Returns all parameters for a VST/AU plugin device
1052
+ var track_idx = parseInt(args[0]);
1053
+ var device_idx = parseInt(args[1]);
1054
+ var path = build_device_path(track_idx, device_idx);
1055
+
1056
+ cursor_a.goto(path);
1057
+ var class_name = cursor_a.get("class_name").toString();
1058
+
1059
+ // Check if this is a plugin device
1060
+ var is_plugin = (class_name === "PluginDevice" || class_name === "AuPluginDevice");
1061
+ if (!is_plugin) {
1062
+ send_response({
1063
+ "error": "Device is " + class_name + ", not a plugin (PluginDevice/AuPluginDevice)"
1064
+ });
1065
+ return;
1066
+ }
1067
+
1068
+ var device_name = cursor_a.get("name").toString();
1069
+ var param_count = cursor_a.getcount("parameters");
1070
+ var params = [];
1071
+ var current = 0;
1072
+ var batch_size = 4;
1073
+
1074
+ function read_batch() {
1075
+ var end = Math.min(current + batch_size, param_count);
1076
+ for (var i = current; i < end; i++) {
1077
+ cursor_b.goto(path + " parameters " + i);
1078
+ params.push({
1079
+ index: i,
1080
+ name: cursor_b.get("name").toString(),
1081
+ value: parseFloat(cursor_b.get("value")),
1082
+ min: parseFloat(cursor_b.get("min")),
1083
+ max: parseFloat(cursor_b.get("max")),
1084
+ default_value: parseFloat(cursor_b.get("default_value")),
1085
+ is_quantized: parseInt(cursor_b.get("is_quantized")) === 1,
1086
+ value_string: String(cursor_b.call("str_for_value", parseFloat(cursor_b.get("value"))))
1087
+ });
1088
+ }
1089
+ current = end;
1090
+
1091
+ if (current < param_count) {
1092
+ var next_task = new Task(read_batch);
1093
+ next_task.schedule(50);
1094
+ } else {
1095
+ send_response({
1096
+ "track": track_idx,
1097
+ "device": device_idx,
1098
+ "name": device_name,
1099
+ "class_name": class_name,
1100
+ "is_plugin": true,
1101
+ "parameter_count": param_count,
1102
+ "parameters": params
1103
+ });
1104
+ }
1105
+ }
1106
+
1107
+ if (param_count > 0) {
1108
+ read_batch();
1109
+ } else {
1110
+ send_response({
1111
+ "track": track_idx,
1112
+ "device": device_idx,
1113
+ "name": device_name,
1114
+ "class_name": class_name,
1115
+ "is_plugin": true,
1116
+ "parameter_count": 0,
1117
+ "parameters": []
1118
+ });
1119
+ }
1120
+ }
1121
+
1122
+ function cmd_map_plugin_param(args) {
1123
+ // Add a plugin parameter to Ableton's Configure list
1124
+ var track_idx = parseInt(args[0]);
1125
+ var device_idx = parseInt(args[1]);
1126
+ var param_idx = parseInt(args[2]);
1127
+ var path = build_device_path(track_idx, device_idx);
1128
+
1129
+ cursor_a.goto(path);
1130
+ var param_count = cursor_a.getcount("parameters");
1131
+ if (param_idx < 0 || param_idx >= param_count) {
1132
+ send_response({"error": "Parameter index " + param_idx + " out of range (0.." + (param_count - 1) + ")"});
1133
+ return;
1134
+ }
1135
+
1136
+ // Navigate to the parameter and read its name
1137
+ cursor_b.goto(path + " parameters " + param_idx);
1138
+ var param_name = cursor_b.get("name").toString();
1139
+
1140
+ // Select the parameter — this is how Ableton's Configure mode works
1141
+ // via LiveAPI. The parameter becomes visible in the device's macro panel.
1142
+ try {
1143
+ cursor_a.set("selected_parameter", param_idx);
1144
+ cursor_a.call("store_chosen_bank");
1145
+ send_response({
1146
+ "mapped": true,
1147
+ "parameter_index": param_idx,
1148
+ "parameter_name": param_name
1149
+ });
1150
+ } catch(e) {
1151
+ send_response({
1152
+ "error": "Failed to map parameter: " + e.message,
1153
+ "parameter_index": param_idx,
1154
+ "parameter_name": param_name
1155
+ });
1156
+ }
1157
+ }
1158
+
1159
+ function cmd_get_plugin_presets(args) {
1160
+ // List plugin's internal presets
1161
+ var track_idx = parseInt(args[0]);
1162
+ var device_idx = parseInt(args[1]);
1163
+ var path = build_device_path(track_idx, device_idx);
1164
+
1165
+ cursor_a.goto(path);
1166
+ var class_name = cursor_a.get("class_name").toString();
1167
+ var device_name = cursor_a.get("name").toString();
1168
+
1169
+ var is_plugin = (class_name === "PluginDevice" || class_name === "AuPluginDevice");
1170
+ if (!is_plugin) {
1171
+ send_response({
1172
+ "error": "Device is " + class_name + ", not a plugin"
1173
+ });
1174
+ return;
1175
+ }
1176
+
1177
+ // Read presets — the presets property returns an array of preset names
1178
+ var presets = [];
1179
+ try {
1180
+ var preset_count = cursor_a.getcount("presets");
1181
+ for (var i = 0; i < preset_count; i++) {
1182
+ cursor_b.goto(path + " presets " + i);
1183
+ presets.push({
1184
+ index: i,
1185
+ name: cursor_b.get("name").toString()
1186
+ });
1187
+ }
1188
+ } catch(e) {
1189
+ // Some plugins don't expose presets via LOM
1190
+ }
1191
+
1192
+ // Try to get selected preset index
1193
+ var selected = -1;
1194
+ try {
1195
+ selected = parseInt(cursor_a.get("selected_preset_index"));
1196
+ } catch(e) {}
1197
+
1198
+ send_response({
1199
+ "track": track_idx,
1200
+ "device": device_idx,
1201
+ "name": device_name,
1202
+ "presets": presets,
1203
+ "selected_preset": selected
1204
+ });
1205
+ }
1206
+
1038
1207
  // ── Helpers ────────────────────────────────────────────────────────────────
1039
1208
 
1040
1209
  function build_track_path(track_idx) {
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.8.4"
2
+ __version__ = "1.9.1"
@@ -1,6 +1,6 @@
1
- """Device MCP tools — parameters, racks, browser loading.
1
+ """Device MCP tools — parameters, racks, browser loading, plugin deep control.
2
2
 
3
- 12 tools matching the Remote Script devices domain.
3
+ 15 tools matching the Remote Script devices domain + M4L bridge.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -254,3 +254,96 @@ def get_device_presets(ctx: Context, device_name: str) -> dict:
254
254
  return _get_ableton(ctx).send_command("get_device_presets", {
255
255
  "device_name": device_name,
256
256
  })
257
+
258
+
259
+ # ── Plugin Deep Control (M4L Bridge) ────────────────────────────────────
260
+
261
+
262
+ def _get_m4l(ctx: Context):
263
+ """Get M4LBridge from lifespan context."""
264
+ bridge = ctx.lifespan_context.get("m4l")
265
+ if not bridge:
266
+ raise RuntimeError("M4L bridge not initialized")
267
+ return bridge
268
+
269
+
270
+ def _get_spectral(ctx: Context):
271
+ """Get SpectralCache from lifespan context."""
272
+ cache = ctx.lifespan_context.get("spectral")
273
+ if not cache:
274
+ raise RuntimeError("Spectral cache not initialized")
275
+ return cache
276
+
277
+
278
+ def _require_analyzer(cache) -> None:
279
+ if not cache.is_connected:
280
+ raise ValueError(
281
+ "LivePilot Analyzer not detected. "
282
+ "Drag 'LivePilot Analyzer' onto the master track."
283
+ )
284
+
285
+
286
+ @mcp.tool()
287
+ async def get_plugin_parameters(
288
+ ctx: Context,
289
+ track_index: int,
290
+ device_index: int,
291
+ ) -> dict:
292
+ """Get ALL parameters from a VST/AU plugin including unconfigured ones.
293
+
294
+ Returns every parameter the plugin exposes — not just the 128
295
+ that Ableton's Configure panel shows. Includes name, value, min,
296
+ max, default, and display string for each.
297
+ Only works on PluginDevice/AuPluginDevice types.
298
+ Requires LivePilot Analyzer on master track.
299
+ """
300
+ _validate_track_index(track_index)
301
+ _validate_device_index(device_index)
302
+ cache = _get_spectral(ctx)
303
+ _require_analyzer(cache)
304
+ bridge = _get_m4l(ctx)
305
+ return await bridge.send_command("get_plugin_params", track_index, device_index)
306
+
307
+
308
+ @mcp.tool()
309
+ async def map_plugin_parameter(
310
+ ctx: Context,
311
+ track_index: int,
312
+ device_index: int,
313
+ parameter_index: int,
314
+ ) -> dict:
315
+ """Add a plugin parameter to Ableton's Configure list for automation.
316
+
317
+ After mapping, the parameter becomes visible in the device's macro
318
+ panel and can be automated with set_device_parameter or
319
+ set_clip_automation like any native parameter.
320
+ Requires LivePilot Analyzer on master track.
321
+ """
322
+ _validate_track_index(track_index)
323
+ _validate_device_index(device_index)
324
+ if parameter_index < 0:
325
+ raise ValueError("parameter_index must be >= 0")
326
+ cache = _get_spectral(ctx)
327
+ _require_analyzer(cache)
328
+ bridge = _get_m4l(ctx)
329
+ return await bridge.send_command("map_plugin_param", track_index, device_index, parameter_index)
330
+
331
+
332
+ @mcp.tool()
333
+ async def get_plugin_presets(
334
+ ctx: Context,
335
+ track_index: int,
336
+ device_index: int,
337
+ ) -> dict:
338
+ """List a VST/AU plugin's internal presets and banks.
339
+
340
+ Returns preset names and the currently selected preset index.
341
+ Only works on PluginDevice/AuPluginDevice types.
342
+ Requires LivePilot Analyzer on master track.
343
+ """
344
+ _validate_track_index(track_index)
345
+ _validate_device_index(device_index)
346
+ cache = _get_spectral(ctx)
347
+ _require_analyzer(cache)
348
+ bridge = _get_m4l(ctx)
349
+ return await bridge.send_command("get_plugin_presets", track_index, device_index)
@@ -14,6 +14,7 @@ from typing import Any, Optional
14
14
 
15
15
  from fastmcp import Context
16
16
 
17
+ from ..connection import AbletonConnectionError
17
18
  from ..server import mcp
18
19
  from . import _theory_engine as theory
19
20
 
@@ -177,18 +178,25 @@ def import_midi_to_clip(
177
178
 
178
179
  if create_clip:
179
180
  # Check if clip already exists — only create if the slot is empty
181
+ slot_has_clip = False
180
182
  try:
181
183
  ableton.send_command("get_clip_info", {
182
184
  "track_index": track_index,
183
185
  "clip_index": clip_index,
184
186
  })
187
+ slot_has_clip = True
188
+ except AbletonConnectionError:
189
+ # Slot is empty — no clip to clear
190
+ pass
191
+
192
+ if slot_has_clip:
185
193
  # Clip exists — clear its notes before importing
186
194
  ableton.send_command("remove_notes", {
187
195
  "track_index": track_index,
188
196
  "clip_index": clip_index,
189
197
  })
190
- except Exception:
191
- # No clip in slot — create one
198
+ else:
199
+ # Empty slot — create a new clip
192
200
  ableton.send_command("create_clip", {
193
201
  "track_index": track_index,
194
202
  "clip_index": clip_index,
@@ -1,8 +1,11 @@
1
- """Scene MCP tools — list, create, delete, duplicate, fire, rename, color, tempo.
1
+ """Scene MCP tools — list, create, delete, duplicate, fire, rename, color, tempo, matrix.
2
2
 
3
- 8 tools matching the Remote Script scenes domain.
3
+ 12 tools matching the Remote Script scenes domain.
4
4
  """
5
5
 
6
+ import json
7
+ from typing import Any, Optional
8
+
6
9
  from fastmcp import Context
7
10
 
8
11
  from ..server import mcp
@@ -87,3 +90,63 @@ def set_scene_tempo(ctx: Context, scene_index: int, tempo: float) -> dict:
87
90
  "scene_index": scene_index,
88
91
  "tempo": tempo,
89
92
  })
93
+
94
+
95
+ # ── Scene Matrix Operations ─────────────────────────────────────────────
96
+
97
+
98
+ def _ensure_list(value: Any) -> list:
99
+ if isinstance(value, str):
100
+ return json.loads(value)
101
+ return value
102
+
103
+
104
+ @mcp.tool()
105
+ def get_scene_matrix(ctx: Context) -> dict:
106
+ """Get the full session clip grid: every track x every scene.
107
+
108
+ Returns clip states (empty/stopped/playing/triggered/recording),
109
+ clip names, and colors. Use this for a bird's-eye view of the
110
+ entire session before making clip launch decisions.
111
+ """
112
+ return _get_ableton(ctx).send_command("get_scene_matrix")
113
+
114
+
115
+ @mcp.tool()
116
+ def fire_scene_clips(
117
+ ctx: Context,
118
+ scene_index: int,
119
+ track_indices: Optional[Any] = None,
120
+ ) -> dict:
121
+ """Fire a scene, optionally filtering to specific tracks.
122
+
123
+ If track_indices is omitted, fires the entire scene (all tracks).
124
+ If provided (JSON array of ints), fires only those tracks' clip slots
125
+ from the scene — useful for launching drums + bass without triggering
126
+ the lead, or building up layers gradually.
127
+ """
128
+ _validate_scene_index(scene_index)
129
+ params: dict = {"scene_index": scene_index}
130
+ if track_indices is not None:
131
+ track_indices = _ensure_list(track_indices)
132
+ for ti in track_indices:
133
+ if int(ti) < 0:
134
+ raise ValueError("track_indices must all be >= 0")
135
+ params["track_indices"] = track_indices
136
+ return _get_ableton(ctx).send_command("fire_scene_clips", params)
137
+
138
+
139
+ @mcp.tool()
140
+ def stop_all_clips(ctx: Context) -> dict:
141
+ """Stop all playing clips in the session. Panic button."""
142
+ return _get_ableton(ctx).send_command("stop_all_clips")
143
+
144
+
145
+ @mcp.tool()
146
+ def get_playing_clips(ctx: Context) -> dict:
147
+ """Get all currently playing or triggered clips.
148
+
149
+ Returns track index/name, clip index/name, and whether each clip
150
+ is actively playing or just triggered (waiting for quantization).
151
+ """
152
+ return _get_ableton(ctx).send_command("get_playing_clips")
@@ -1,6 +1,6 @@
1
- """Track MCP tools — create, delete, rename, mute, solo, arm, group fold, monitor.
1
+ """Track MCP tools — create, delete, rename, mute, solo, arm, group fold, monitor, freeze, flatten.
2
2
 
3
- 14 tools matching the Remote Script tracks domain.
3
+ 17 tools matching the Remote Script tracks domain.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -173,3 +173,46 @@ def set_track_input_monitoring(ctx: Context, track_index: int, state: int) -> di
173
173
  "track_index": track_index,
174
174
  "state": state,
175
175
  })
176
+
177
+
178
+ # ── Freeze / Flatten ────────────────────────────────────────────────────
179
+
180
+
181
+ @mcp.tool()
182
+ def freeze_track(ctx: Context, track_index: int) -> dict:
183
+ """Freeze a track — render all devices to audio for CPU savings.
184
+
185
+ Freeze is async in Ableton: this initiates the render and returns
186
+ immediately. Poll get_freeze_status to check when it's done.
187
+ Freezing a track that's already frozen is a no-op.
188
+ """
189
+ _validate_track_index(track_index)
190
+ return _get_ableton(ctx).send_command("freeze_track", {
191
+ "track_index": track_index,
192
+ })
193
+
194
+
195
+ @mcp.tool()
196
+ def flatten_track(ctx: Context, track_index: int) -> dict:
197
+ """Flatten a frozen track — commit rendered audio permanently.
198
+
199
+ Destructive: replaces all devices with the rendered audio file.
200
+ The track must already be frozen. Use undo to revert.
201
+ """
202
+ _validate_track_index(track_index)
203
+ return _get_ableton(ctx).send_command("flatten_track", {
204
+ "track_index": track_index,
205
+ })
206
+
207
+
208
+ @mcp.tool()
209
+ def get_freeze_status(ctx: Context, track_index: int) -> dict:
210
+ """Check if a track is frozen.
211
+
212
+ Use after freeze_track to poll for completion, or before
213
+ flatten_track to verify the track is ready to flatten.
214
+ """
215
+ _validate_track_index(track_index)
216
+ return _get_ableton(ctx).send_command("get_freeze_status", {
217
+ "track_index": track_index,
218
+ })
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.8.4",
3
+ "version": "1.9.1",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 168 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
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",
7
7
  "license": "MIT",
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.8.4"
8
+ __version__ = "1.9.1"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from .server import LivePilotServer
@@ -107,6 +107,31 @@ def create_arrangement_clip(song, params):
107
107
 
108
108
  clip_count += 1
109
109
  pos += loop_length
110
+
111
+ # Trim the last clip's overshoot: if the last duplicate extends
112
+ # past end_pos, remove notes beyond the requested region and
113
+ # set loop_end so only the needed portion plays.
114
+ if clip_count > 0:
115
+ arr_clips = list(track.arrangement_clips)
116
+ for c in arr_clips:
117
+ clip_end = c.start_time + c.length
118
+ if c.start_time >= start_time and clip_end > end_pos + 0.01:
119
+ # This clip overshoots — trim its content
120
+ overshoot_start = end_pos - c.start_time
121
+ if overshoot_start > 0:
122
+ try:
123
+ c.looping = True
124
+ c.loop_start = 0.0
125
+ c.loop_end = overshoot_start
126
+ except (AttributeError, RuntimeError):
127
+ pass
128
+ # Remove notes beyond the trim point
129
+ try:
130
+ c.remove_notes_extended(
131
+ 0, 128, overshoot_start, c.length
132
+ )
133
+ except Exception:
134
+ pass
110
135
  finally:
111
136
  song.end_undo_step()
112
137
 
@@ -1,5 +1,5 @@
1
1
  """
2
- LivePilot - Scene domain handlers (8 commands).
2
+ LivePilot - Scene domain handlers (12 commands).
3
3
  """
4
4
 
5
5
  from .router import register
@@ -97,3 +97,112 @@ def set_scene_tempo(song, params):
97
97
  "index": scene_index,
98
98
  "tempo": scene.tempo,
99
99
  }
100
+
101
+
102
+ # ── Scene Matrix Operations ─────────────────────────────────────────────
103
+
104
+
105
+ @register("get_scene_matrix")
106
+ def get_scene_matrix(song, params):
107
+ """Return the full clip grid: tracks x scenes with clip states."""
108
+ tracks = list(song.tracks)
109
+ scenes = list(song.scenes)
110
+
111
+ track_headers = []
112
+ for i, t in enumerate(tracks):
113
+ track_headers.append({"index": i, "name": t.name})
114
+
115
+ scene_headers = []
116
+ for i, s in enumerate(scenes):
117
+ scene_headers.append({
118
+ "index": i,
119
+ "name": s.name,
120
+ "tempo": s.tempo if s.tempo > 0 else None,
121
+ })
122
+
123
+ matrix = []
124
+ for si, scene in enumerate(scenes):
125
+ row = []
126
+ for ti, track in enumerate(tracks):
127
+ slots = list(track.clip_slots)
128
+ if si >= len(slots):
129
+ row.append({"state": "missing"})
130
+ continue
131
+ slot = slots[si]
132
+ cell = {"state": "empty"}
133
+ if slot.has_clip and slot.clip:
134
+ clip = slot.clip
135
+ if clip.is_recording:
136
+ cell["state"] = "recording"
137
+ elif clip.is_playing:
138
+ cell["state"] = "playing"
139
+ elif clip.is_triggered:
140
+ cell["state"] = "triggered"
141
+ else:
142
+ cell["state"] = "stopped"
143
+ cell["name"] = clip.name
144
+ cell["color_index"] = clip.color_index
145
+ row.append(cell)
146
+ matrix.append(row)
147
+
148
+ return {
149
+ "tracks": track_headers,
150
+ "scenes": scene_headers,
151
+ "matrix": matrix,
152
+ }
153
+
154
+
155
+ @register("fire_scene_clips")
156
+ def fire_scene_clips(song, params):
157
+ """Fire a scene with optional track filter."""
158
+ scene_index = int(params["scene_index"])
159
+ track_indices = params.get("track_indices")
160
+
161
+ scene = get_scene(song, scene_index)
162
+
163
+ if track_indices is None:
164
+ # Fire entire scene
165
+ scene.fire()
166
+ return {"scene_index": scene_index, "fired": "all"}
167
+
168
+ # Fire specific tracks only
169
+ tracks = list(song.tracks)
170
+ fired = []
171
+ for ti in track_indices:
172
+ ti = int(ti)
173
+ if ti < 0 or ti >= len(tracks):
174
+ raise IndexError("Track index %d out of range (0..%d)" % (ti, len(tracks) - 1))
175
+ slots = list(tracks[ti].clip_slots)
176
+ if scene_index < len(slots):
177
+ slots[scene_index].fire()
178
+ fired.append(ti)
179
+
180
+ return {"scene_index": scene_index, "fired_tracks": fired}
181
+
182
+
183
+ @register("stop_all_clips")
184
+ def stop_all_clips(song, params):
185
+ """Stop all playing clips in the session."""
186
+ song.stop_all_clips()
187
+ return {"stopped": True}
188
+
189
+
190
+ @register("get_playing_clips")
191
+ def get_playing_clips(song, params):
192
+ """Return all currently playing or triggered clips."""
193
+ tracks = list(song.tracks)
194
+ clips = []
195
+ for ti, track in enumerate(tracks):
196
+ for si, slot in enumerate(track.clip_slots):
197
+ if slot.has_clip and slot.clip:
198
+ clip = slot.clip
199
+ if clip.is_playing or clip.is_triggered:
200
+ clips.append({
201
+ "track_index": ti,
202
+ "track_name": track.name,
203
+ "clip_index": si,
204
+ "clip_name": clip.name,
205
+ "is_playing": clip.is_playing,
206
+ "is_triggered": clip.is_triggered,
207
+ })
208
+ return {"clips": clips}
@@ -39,6 +39,9 @@ WRITE_COMMANDS = frozenset([
39
39
  # scenes
40
40
  "create_scene", "delete_scene", "duplicate_scene", "fire_scene",
41
41
  "set_scene_name", "set_scene_color", "set_scene_tempo",
42
+ "fire_scene_clips", "stop_all_clips",
43
+ # tracks (freeze/flatten)
44
+ "freeze_track", "flatten_track",
42
45
  # mixing
43
46
  "set_track_volume", "set_track_pan", "set_track_send",
44
47
  "set_master_volume", "set_track_routing",
@@ -57,6 +60,11 @@ WRITE_COMMANDS = frozenset([
57
60
  "clear_clip_automation",
58
61
  ])
59
62
 
63
+ # Commands that need longer timeouts (e.g., freeze renders audio)
64
+ SLOW_WRITE_COMMANDS = frozenset([
65
+ "freeze_track",
66
+ ])
67
+
60
68
 
61
69
  class LivePilotServer(object):
62
70
  """TCP server that bridges JSON commands to Ableton's main thread.
@@ -212,9 +220,14 @@ class LivePilotServer(object):
212
220
  request_id = command.get("id", "unknown")
213
221
  cmd_type = command.get("type", "")
214
222
 
215
- # Determine timeout based on read vs write
223
+ # Determine timeout based on read vs write vs slow write
216
224
  is_write = cmd_type in WRITE_COMMANDS
217
- timeout = 15 if is_write else 10
225
+ if cmd_type in SLOW_WRITE_COMMANDS:
226
+ timeout = 35
227
+ elif is_write:
228
+ timeout = 15
229
+ else:
230
+ timeout = 10
218
231
 
219
232
  # Per-command response queue
220
233
  response_queue = queue.Queue()
@@ -1,5 +1,5 @@
1
1
  """
2
- LivePilot - Track domain handlers (14 commands).
2
+ LivePilot - Track domain handlers (17 commands).
3
3
  """
4
4
 
5
5
  from .router import register
@@ -267,3 +267,73 @@ def set_track_input_monitoring(song, params):
267
267
  "index": track_index,
268
268
  "monitoring_state": track.current_monitoring_state,
269
269
  }
270
+
271
+
272
+ # ── Freeze / Flatten ────────────────────────────────────────────────────
273
+
274
+
275
+ @register("get_freeze_status")
276
+ def get_freeze_status(song, params):
277
+ """Return freeze state for a track."""
278
+ track_index = int(params["track_index"])
279
+ track = get_track(song, track_index)
280
+ return {
281
+ "track_index": track_index,
282
+ "is_frozen": track.is_frozen,
283
+ }
284
+
285
+
286
+ @register("freeze_track")
287
+ def freeze_track(song, params):
288
+ """Freeze a track — render all devices to audio for CPU savings.
289
+
290
+ Freeze is async in Ableton — this call initiates it and returns
291
+ immediately. Use get_freeze_status to poll for completion.
292
+ """
293
+ track_index = int(params["track_index"])
294
+ track = get_track(song, track_index)
295
+ if track.is_frozen:
296
+ return {
297
+ "track_index": track_index,
298
+ "is_frozen": True,
299
+ "note": "Track is already frozen",
300
+ }
301
+ # In Live 12, freeze is a track method accessible from ControlSurface
302
+ try:
303
+ track.freeze()
304
+ except AttributeError:
305
+ raise ValueError(
306
+ "freeze() not available on this track type. "
307
+ "Only MIDI and audio tracks with devices can be frozen."
308
+ )
309
+ return {
310
+ "track_index": track_index,
311
+ "freezing": True,
312
+ "note": "Freeze initiated. Poll get_freeze_status to check completion.",
313
+ }
314
+
315
+
316
+ @register("flatten_track")
317
+ def flatten_track(song, params):
318
+ """Flatten a frozen track — commits rendered audio permanently.
319
+
320
+ Destructive operation. The track must already be frozen.
321
+ Wrapped in undo step so it can be reverted.
322
+ """
323
+ track_index = int(params["track_index"])
324
+ track = get_track(song, track_index)
325
+ if not track.is_frozen:
326
+ raise ValueError(
327
+ "Track %d is not frozen. Freeze it first with freeze_track."
328
+ % track_index
329
+ )
330
+ song.begin_undo_step()
331
+ try:
332
+ # flatten() is a method on the track, not the song
333
+ track.flatten()
334
+ finally:
335
+ song.end_undo_step()
336
+ return {
337
+ "track_index": track_index,
338
+ "flattened": True,
339
+ }