livepilot 1.10.6 → 1.10.8

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.
Files changed (163) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/README.md +12 -10
  3. package/bin/livepilot.js +168 -30
  4. package/installer/install.js +117 -11
  5. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  6. package/m4l_device/livepilot_bridge.js +215 -3
  7. package/mcp_server/__init__.py +1 -1
  8. package/mcp_server/atlas/__init__.py +132 -33
  9. package/mcp_server/atlas/tools.py +56 -15
  10. package/mcp_server/composer/layer_planner.py +27 -0
  11. package/mcp_server/composer/prompt_parser.py +15 -6
  12. package/mcp_server/connection.py +11 -3
  13. package/mcp_server/corpus/__init__.py +14 -4
  14. package/mcp_server/creative_constraints/tools.py +206 -33
  15. package/mcp_server/experiment/engine.py +7 -9
  16. package/mcp_server/hook_hunter/analyzer.py +62 -9
  17. package/mcp_server/hook_hunter/tools.py +60 -9
  18. package/mcp_server/m4l_bridge.py +68 -12
  19. package/mcp_server/musical_intelligence/detectors.py +32 -0
  20. package/mcp_server/performance_engine/tools.py +112 -29
  21. package/mcp_server/preview_studio/engine.py +89 -8
  22. package/mcp_server/preview_studio/tools.py +22 -6
  23. package/mcp_server/project_brain/automation_graph.py +71 -19
  24. package/mcp_server/project_brain/builder.py +2 -0
  25. package/mcp_server/project_brain/tools.py +55 -5
  26. package/mcp_server/reference_engine/profile_builder.py +129 -3
  27. package/mcp_server/reference_engine/tools.py +47 -6
  28. package/mcp_server/runtime/execution_router.py +66 -2
  29. package/mcp_server/runtime/mcp_dispatch.py +75 -3
  30. package/mcp_server/runtime/remote_commands.py +10 -2
  31. package/mcp_server/sample_engine/analyzer.py +131 -4
  32. package/mcp_server/sample_engine/critics.py +29 -8
  33. package/mcp_server/sample_engine/models.py +42 -4
  34. package/mcp_server/sample_engine/tools.py +48 -14
  35. package/mcp_server/semantic_moves/__init__.py +1 -0
  36. package/mcp_server/semantic_moves/compiler.py +9 -1
  37. package/mcp_server/semantic_moves/device_creation_compilers.py +47 -0
  38. package/mcp_server/semantic_moves/mix_compilers.py +170 -0
  39. package/mcp_server/semantic_moves/mix_moves.py +1 -1
  40. package/mcp_server/semantic_moves/models.py +5 -0
  41. package/mcp_server/semantic_moves/sound_design_compilers.py +22 -59
  42. package/mcp_server/semantic_moves/tools.py +15 -4
  43. package/mcp_server/semantic_moves/transition_compilers.py +12 -19
  44. package/mcp_server/server.py +75 -5
  45. package/mcp_server/services/singletons.py +68 -0
  46. package/mcp_server/session_continuity/models.py +4 -0
  47. package/mcp_server/session_continuity/tracker.py +14 -1
  48. package/mcp_server/song_brain/builder.py +110 -12
  49. package/mcp_server/song_brain/tools.py +77 -13
  50. package/mcp_server/sound_design/tools.py +112 -1
  51. package/mcp_server/splice_client/client.py +29 -8
  52. package/mcp_server/stuckness_detector/detector.py +90 -0
  53. package/mcp_server/stuckness_detector/tools.py +41 -0
  54. package/mcp_server/tools/_agent_os_engine/critics.py +24 -0
  55. package/mcp_server/tools/_composition_engine/__init__.py +2 -2
  56. package/mcp_server/tools/_composition_engine/harmony.py +90 -0
  57. package/mcp_server/tools/_composition_engine/sections.py +47 -4
  58. package/mcp_server/tools/_harmony_engine.py +52 -8
  59. package/mcp_server/tools/_research_engine.py +98 -19
  60. package/mcp_server/tools/_theory_engine.py +138 -9
  61. package/mcp_server/tools/agent_os.py +20 -3
  62. package/mcp_server/tools/analyzer.py +105 -6
  63. package/mcp_server/tools/clips.py +46 -1
  64. package/mcp_server/tools/composition.py +66 -23
  65. package/mcp_server/tools/devices.py +22 -1
  66. package/mcp_server/tools/harmony.py +115 -14
  67. package/mcp_server/tools/midi_io.py +23 -1
  68. package/mcp_server/tools/mixing.py +35 -1
  69. package/mcp_server/tools/motif.py +49 -3
  70. package/mcp_server/tools/research.py +24 -0
  71. package/mcp_server/tools/theory.py +108 -16
  72. package/mcp_server/tools/tracks.py +1 -1
  73. package/mcp_server/tools/transport.py +1 -1
  74. package/mcp_server/transition_engine/critics.py +18 -11
  75. package/mcp_server/translation_engine/tools.py +8 -4
  76. package/package.json +25 -3
  77. package/remote_script/LivePilot/__init__.py +77 -2
  78. package/remote_script/LivePilot/arrangement.py +12 -2
  79. package/remote_script/LivePilot/browser.py +16 -6
  80. package/remote_script/LivePilot/clips.py +69 -0
  81. package/remote_script/LivePilot/devices.py +10 -5
  82. package/remote_script/LivePilot/mixing.py +117 -0
  83. package/remote_script/LivePilot/notes.py +13 -2
  84. package/remote_script/LivePilot/router.py +13 -1
  85. package/remote_script/LivePilot/server.py +51 -13
  86. package/remote_script/LivePilot/version_detect.py +7 -4
  87. package/server.json +20 -0
  88. package/.claude-plugin/marketplace.json +0 -21
  89. package/.mcpbignore +0 -57
  90. package/AGENTS.md +0 -46
  91. package/CODE_OF_CONDUCT.md +0 -27
  92. package/CONTRIBUTING.md +0 -131
  93. package/SECURITY.md +0 -48
  94. package/livepilot/.Codex-plugin/plugin.json +0 -8
  95. package/livepilot/.claude-plugin/plugin.json +0 -8
  96. package/livepilot/agents/livepilot-producer/AGENT.md +0 -313
  97. package/livepilot/commands/arrange.md +0 -47
  98. package/livepilot/commands/beat.md +0 -77
  99. package/livepilot/commands/evaluate.md +0 -49
  100. package/livepilot/commands/memory.md +0 -22
  101. package/livepilot/commands/mix.md +0 -44
  102. package/livepilot/commands/perform.md +0 -42
  103. package/livepilot/commands/session.md +0 -13
  104. package/livepilot/commands/sounddesign.md +0 -43
  105. package/livepilot/skills/livepilot-arrangement/SKILL.md +0 -155
  106. package/livepilot/skills/livepilot-composition-engine/SKILL.md +0 -107
  107. package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +0 -97
  108. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +0 -102
  109. package/livepilot/skills/livepilot-core/SKILL.md +0 -184
  110. package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +0 -831
  111. package/livepilot/skills/livepilot-core/references/automation-atlas.md +0 -272
  112. package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +0 -110
  113. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +0 -687
  114. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +0 -753
  115. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +0 -525
  116. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +0 -402
  117. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +0 -963
  118. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +0 -874
  119. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +0 -571
  120. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +0 -714
  121. package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +0 -953
  122. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +0 -34
  123. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +0 -204
  124. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +0 -173
  125. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +0 -211
  126. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +0 -188
  127. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +0 -162
  128. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +0 -229
  129. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +0 -243
  130. package/livepilot/skills/livepilot-core/references/m4l-devices.md +0 -352
  131. package/livepilot/skills/livepilot-core/references/memory-guide.md +0 -107
  132. package/livepilot/skills/livepilot-core/references/midi-recipes.md +0 -402
  133. package/livepilot/skills/livepilot-core/references/mixing-patterns.md +0 -578
  134. package/livepilot/skills/livepilot-core/references/overview.md +0 -290
  135. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +0 -724
  136. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +0 -140
  137. package/livepilot/skills/livepilot-core/references/sound-design.md +0 -393
  138. package/livepilot/skills/livepilot-devices/SKILL.md +0 -169
  139. package/livepilot/skills/livepilot-evaluation/SKILL.md +0 -156
  140. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +0 -118
  141. package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +0 -121
  142. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +0 -110
  143. package/livepilot/skills/livepilot-mix-engine/SKILL.md +0 -123
  144. package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +0 -143
  145. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +0 -105
  146. package/livepilot/skills/livepilot-mixing/SKILL.md +0 -157
  147. package/livepilot/skills/livepilot-notes/SKILL.md +0 -130
  148. package/livepilot/skills/livepilot-performance-engine/SKILL.md +0 -122
  149. package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +0 -98
  150. package/livepilot/skills/livepilot-release/SKILL.md +0 -130
  151. package/livepilot/skills/livepilot-sample-engine/SKILL.md +0 -105
  152. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +0 -87
  153. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +0 -51
  154. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +0 -131
  155. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +0 -168
  156. package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +0 -119
  157. package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +0 -118
  158. package/livepilot/skills/livepilot-wonder/SKILL.md +0 -79
  159. package/m4l_device/LivePilot_Analyzer.maxpat +0 -2705
  160. package/manifest.json +0 -91
  161. package/mcp_server/splice_client/protos/app_pb2.pyi +0 -1153
  162. package/scripts/generate_tool_catalog.py +0 -131
  163. package/scripts/sync_metadata.py +0 -132
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const fs = require("fs");
4
+ const os = require("os");
4
5
  const path = require("path");
5
6
  const { findAbletonPaths } = require("./paths");
6
7
 
@@ -10,6 +11,60 @@ const SOURCE_DIR = path.join(ROOT, "remote_script", "LivePilot");
10
11
  // Files / dirs to skip during copy
11
12
  const SKIP = new Set(["__pycache__", ".DS_Store"]);
12
13
 
14
+ // How many previous backups to keep on disk before auto-pruning (the upgrade
15
+ // path renames the old LivePilot dir to LivePilot.backup-<ts>/ so the user can
16
+ // recover a manual edit).
17
+ const BACKUP_RETENTION = 3;
18
+
19
+ /**
20
+ * Typed installer error. Wrappers (e.g. the --setup wizard) can catch this
21
+ * and decide whether to continue with later steps (recoverable) or abort the
22
+ * whole wizard (non-recoverable). The previous version called process.exit(1)
23
+ * mid-function, which silently short-circuited the setup wizard — callers
24
+ * had try/catch expecting exceptions, so later steps (bootstrap, M4L install,
25
+ * diagnostics) were skipped without warning.
26
+ */
27
+ class InstallerAbort extends Error {
28
+ constructor(message, { recoverable = false } = {}) {
29
+ super(message);
30
+ this.name = "InstallerAbort";
31
+ this.recoverable = recoverable;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Validate that a user-supplied install destination is somewhere safe.
37
+ * Refuses to write outside of the user's home directory unless it matches
38
+ * one of the known Ableton Remote Scripts paths. This closes the path-
39
+ * traversal hole from `LIVEPILOT_INSTALL_PATH=/etc ...`.
40
+ */
41
+ function _assertSafeInstallPath(resolvedPath, candidates) {
42
+ const home = os.homedir();
43
+ const allowedPrefixes = [
44
+ home,
45
+ // Systemwide Ableton install paths that live outside $HOME on some platforms
46
+ "/Applications/Ableton",
47
+ "C:\\ProgramData\\Ableton",
48
+ ];
49
+ // The detected Ableton candidate paths are always considered safe
50
+ for (const c of candidates) {
51
+ if (path.resolve(c.path).startsWith(path.resolve(resolvedPath))) {
52
+ return;
53
+ }
54
+ if (path.resolve(resolvedPath).startsWith(path.resolve(c.path))) {
55
+ return;
56
+ }
57
+ }
58
+ const safe = allowedPrefixes.some((p) => resolvedPath.startsWith(path.resolve(p)));
59
+ if (!safe) {
60
+ throw new InstallerAbort(
61
+ `LIVEPILOT_INSTALL_PATH=${resolvedPath} is outside permitted directories. ` +
62
+ `Refusing to install. Allowed roots: ${allowedPrefixes.join(", ")}`,
63
+ { recoverable: false }
64
+ );
65
+ }
66
+ }
67
+
13
68
  /**
14
69
  * Recursively copy a directory, skipping __pycache__ and .DS_Store.
15
70
  */
@@ -28,22 +83,51 @@ function copyDirSync(src, dest) {
28
83
  }
29
84
  }
30
85
 
86
+ /**
87
+ * Prune old LivePilot.backup-<ts>/ dirs, keeping the most recent N.
88
+ */
89
+ function _pruneBackups(parentDir) {
90
+ try {
91
+ const entries = fs.readdirSync(parentDir, { withFileTypes: true });
92
+ const backups = entries
93
+ .filter((e) => e.isDirectory() && /^LivePilot\.backup-\d+$/.test(e.name))
94
+ .map((e) => e.name)
95
+ .sort(); // lexicographic — timestamps are monotonic, so this is age order
96
+ while (backups.length > BACKUP_RETENTION) {
97
+ const old = backups.shift();
98
+ try {
99
+ fs.rmSync(path.join(parentDir, old), { recursive: true, force: true });
100
+ } catch {
101
+ // best effort — don't let cleanup failure break an install
102
+ }
103
+ }
104
+ } catch {
105
+ // best effort
106
+ }
107
+ }
108
+
31
109
  /**
32
110
  * Install the LivePilot Remote Script into Ableton's Remote Scripts folder.
111
+ *
112
+ * Throws InstallerAbort on recoverable failures (auto-detect missing) or
113
+ * non-recoverable ones (path-traversal attempt). Never calls process.exit.
114
+ * This lets the setup wizard continue with later steps on a recoverable
115
+ * failure.
33
116
  */
34
117
  function install() {
35
118
  const candidates = findAbletonPaths();
36
119
 
37
120
  if (candidates.length === 0) {
38
- console.log("Could not auto-detect an Ableton Live Remote Scripts directory.");
39
- console.log("");
40
- console.log("Manual install:");
41
- console.log(" 1. Open Ableton Live > Preferences > File/Folder");
42
- console.log(" 2. Find the User Remote Scripts folder path");
43
- console.log(" 3. Copy the 'remote_script/LivePilot' folder into that directory");
44
- console.log(" 4. Restart Ableton Live");
45
- console.log(" 5. In Preferences > Link/Tempo/MIDI, set a Control Surface to 'LivePilot'");
46
- process.exit(1);
121
+ throw new InstallerAbort(
122
+ "Could not auto-detect an Ableton Live Remote Scripts directory.\n\n" +
123
+ "Manual install:\n" +
124
+ " 1. Open Ableton Live > Preferences > File/Folder\n" +
125
+ " 2. Find the User Remote Scripts folder path\n" +
126
+ " 3. Copy the 'remote_script/LivePilot' folder into that directory\n" +
127
+ " 4. Restart Ableton Live\n" +
128
+ " 5. In Preferences > Link/Tempo/MIDI, set a Control Surface to 'LivePilot'",
129
+ { recoverable: true }
130
+ );
47
131
  }
48
132
 
49
133
  // If multiple candidates exist, let the user choose via --install-path
@@ -51,7 +135,9 @@ function install() {
51
135
  let target;
52
136
  const explicitPath = process.env.LIVEPILOT_INSTALL_PATH;
53
137
  if (explicitPath) {
54
- target = { path: explicitPath, description: "explicit (LIVEPILOT_INSTALL_PATH)" };
138
+ const resolved = path.resolve(explicitPath);
139
+ _assertSafeInstallPath(resolved, candidates);
140
+ target = { path: resolved, description: "explicit (LIVEPILOT_INSTALL_PATH)" };
55
141
  } else if (candidates.length > 1) {
56
142
  console.log("Multiple Ableton Remote Scripts directories detected:");
57
143
  candidates.forEach((c, i) => {
@@ -73,6 +159,25 @@ function install() {
73
159
  // Ensure target base exists
74
160
  fs.mkdirSync(targetBase, { recursive: true });
75
161
 
162
+ // Clear-then-copy upgrade path. Overlay-copying on top of an existing
163
+ // install leaves stale files when a module is removed/renamed upstream.
164
+ // Instead, rename the previous install to a timestamped backup, copy
165
+ // fresh, then prune old backups. The rename (not delete) preserves any
166
+ // local edits the user may have made.
167
+ if (fs.existsSync(destDir)) {
168
+ const ts = new Date().toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
169
+ const backup = path.join(targetBase, `LivePilot.backup-${ts}`);
170
+ try {
171
+ fs.renameSync(destDir, backup);
172
+ console.log("Existing install backed up to: %s", backup);
173
+ } catch (e) {
174
+ throw new InstallerAbort(
175
+ `Could not back up previous LivePilot install at ${destDir}: ${e.message}`,
176
+ { recoverable: false }
177
+ );
178
+ }
179
+ }
180
+
76
181
  console.log("Installing LivePilot Remote Script...");
77
182
  console.log(" Source: %s", SOURCE_DIR);
78
183
  console.log(" Target: %s", destDir);
@@ -80,6 +185,7 @@ function install() {
80
185
  console.log("");
81
186
 
82
187
  copyDirSync(SOURCE_DIR, destDir);
188
+ _pruneBackups(targetBase);
83
189
 
84
190
  console.log("Done! Next steps:");
85
191
  console.log(" 1. Restart Ableton Live (or press Cmd+, to open Preferences)");
@@ -111,4 +217,4 @@ function uninstall() {
111
217
  }
112
218
  }
113
219
 
114
- module.exports = { install, uninstall };
220
+ module.exports = { install, uninstall, InstallerAbort };
Binary file
@@ -95,7 +95,7 @@ function anything() {
95
95
  function dispatch(cmd, args) {
96
96
  switch(cmd) {
97
97
  case "ping":
98
- send_response({"ok": true, "version": "1.10.6"});
98
+ send_response({"ok": true, "version": "1.10.8"});
99
99
  break;
100
100
  case "get_params":
101
101
  cmd_get_params(args);
@@ -183,6 +183,13 @@ function dispatch(cmd, args) {
183
183
  case "get_plugin_presets":
184
184
  cmd_get_plugin_presets(args);
185
185
  break;
186
+ // ── BUG-A2 / A3: deep-LOM properties not on the automatable surface ──
187
+ case "simpler_set_warp":
188
+ cmd_simpler_set_warp(args);
189
+ break;
190
+ case "compressor_set_sidechain":
191
+ cmd_compressor_set_sidechain(args);
192
+ break;
186
193
  default:
187
194
  send_response({"error": "Unknown command: " + cmd});
188
195
  }
@@ -641,8 +648,13 @@ function detect_key() {
641
648
  detected_key = note_names[best_key];
642
649
  detected_scale = best_scale;
643
650
 
644
- // Send to UI
645
- outlet(1, "key", detected_key + " " + detected_scale);
651
+ // Send to UI — use abbreviated scale ("min"/"maj") so text fits in the
652
+ // 72-pixel presentation widget, and pass a SINGLE symbol so Max's
653
+ // [route] + [prepend set] chain doesn't split atoms on the internal
654
+ // space. Max's [comment] displays whatever the `set` message carries.
655
+ var scale_abbr = (detected_scale === "minor") ? "min" : "maj";
656
+ var display = detected_key + " " + scale_abbr; // e.g., "D min"
657
+ outlet(1, "key", display);
646
658
  }
647
659
 
648
660
  function correlate(a, b) {
@@ -991,6 +1003,206 @@ function cmd_simpler_warp(args) {
991
1003
  }
992
1004
  }
993
1005
 
1006
+ // ── BUG-A2: Simpler warping property + warp_mode ─────────────────────
1007
+ //
1008
+ // Python's Remote Script ControlSurface API only exposes automatable
1009
+ // parameters. Simpler's `warping` and `warp_mode` live on the sample
1010
+ // child object (SimplerDevice.sample.*) — unreachable from the Python
1011
+ // side. Max JS LiveAPI can step INTO the sample child, so we do the
1012
+ // property write here and surface the result to the MCP server.
1013
+ //
1014
+ // args: [track_index, device_index, warp_on (0/1), warp_mode (-1 = leave alone, 0..6)]
1015
+ // warp_mode: 0=Beats, 1=Tones, 2=Texture, 3=Re-Pitch, 4=Complex, 6=Complex Pro
1016
+ //
1017
+ // Returns: {ok, warping, warp_mode} on success, {error} otherwise.
1018
+ function cmd_simpler_set_warp(args) {
1019
+ var track_idx = parseInt(args[0]);
1020
+ var device_idx = parseInt(args[1]);
1021
+ var warp_on = parseInt(args[2]);
1022
+ var warp_mode = args.length > 3 ? parseInt(args[3]) : -1;
1023
+
1024
+ var device_path = build_device_path(track_idx, device_idx);
1025
+ cursor_a.goto(device_path);
1026
+ if (cursor_a.id === 0) {
1027
+ send_response({"error": "Device not found at track " + track_idx + ", device " + device_idx});
1028
+ return;
1029
+ }
1030
+ if (cursor_a.get("class_name").toString() !== "OriginalSimpler") {
1031
+ send_response({"error": "Not a Simpler device (class is " + cursor_a.get("class_name") + ")"});
1032
+ return;
1033
+ }
1034
+
1035
+ // Step into the sample child — warping + warp_mode live there, not on
1036
+ // the device itself.
1037
+ try {
1038
+ cursor_a.goto(device_path + " sample");
1039
+ if (cursor_a.id === 0) {
1040
+ send_response({"error": "Simpler has no sample loaded (warping not applicable)"});
1041
+ return;
1042
+ }
1043
+ cursor_a.set("warping", warp_on ? 1 : 0);
1044
+ if (warp_on && warp_mode >= 0) {
1045
+ cursor_a.set("warp_mode", warp_mode);
1046
+ }
1047
+ // Read back so the caller can confirm the write landed
1048
+ var read_warping = parseInt(cursor_a.get("warping"));
1049
+ var read_warp_mode = parseInt(cursor_a.get("warp_mode"));
1050
+ send_response({
1051
+ "ok": true,
1052
+ "track_index": track_idx,
1053
+ "device_index": device_idx,
1054
+ "warping": read_warping,
1055
+ "warp_mode": read_warp_mode,
1056
+ });
1057
+ } catch(e) {
1058
+ send_response({"error": "simpler_set_warp failed: " + e.message});
1059
+ }
1060
+ }
1061
+
1062
+ // ── BUG-A3: Compressor sidechain input routing ───────────────────────
1063
+ //
1064
+ // Sidechain INPUT ROUTING is exposed as LiveAPI properties on the
1065
+ // Compressor device in Live 11+: sidechain_input_routing_type and
1066
+ // sidechain_input_routing_channel. They don't appear in the automatable
1067
+ // parameter list so the Python Remote Script can't reach them; Max JS
1068
+ // LiveAPI can.
1069
+ //
1070
+ // args: [track_index, device_index, routing_type, routing_channel]
1071
+ // routing_type: string — e.g. "1-Audio From" / track name / "Ext. In"
1072
+ // routing_channel: string — "Post FX" / "Pre FX" / "Post Mixer" / ...
1073
+ //
1074
+ // Returns: {ok, sidechain: {type, channel}} on success.
1075
+ // Older Live versions without these properties return a clean error.
1076
+ function cmd_compressor_set_sidechain(args) {
1077
+ var track_idx = parseInt(args[0]);
1078
+ var device_idx = parseInt(args[1]);
1079
+ var routing_type = String(args[2] || "");
1080
+ var routing_channel = String(args[3] || "");
1081
+
1082
+ var path = build_device_path(track_idx, device_idx);
1083
+ cursor_a.goto(path);
1084
+ if (cursor_a.id === 0) {
1085
+ send_response({"error": "Device not found at track " + track_idx + ", device " + device_idx});
1086
+ return;
1087
+ }
1088
+ var class_name = String(cursor_a.get("class_name"));
1089
+ if (class_name !== "Compressor2" && class_name !== "Compressor") {
1090
+ send_response({"error": "Not a Compressor device (class is " + class_name + ")"});
1091
+ return;
1092
+ }
1093
+
1094
+ // Helper: read a LiveAPI property that returns a JSON-serialized dict
1095
+ // or list. Max's `get()` wraps results in a single-element array,
1096
+ // and complex properties come back as JSON strings.
1097
+ function read_json_prop(name) {
1098
+ try {
1099
+ var raw = cursor_a.get(name);
1100
+ if (raw === null || raw === undefined) return null;
1101
+ if (Object.prototype.toString.call(raw) === "[object Array]" && raw.length === 1) {
1102
+ raw = raw[0];
1103
+ }
1104
+ if (typeof raw === "string") {
1105
+ try { return JSON.parse(raw); } catch(e) { return raw; }
1106
+ }
1107
+ return raw;
1108
+ } catch(e) {
1109
+ return null;
1110
+ }
1111
+ }
1112
+
1113
+ function find_by_name(list, name) {
1114
+ if (!list || !list.length || !name) return null;
1115
+ for (var i = 0; i < list.length; i++) {
1116
+ var entry = list[i];
1117
+ if (!entry) continue;
1118
+ var n = entry.display_name || entry.name;
1119
+ if (n === name) return entry;
1120
+ }
1121
+ return null;
1122
+ }
1123
+
1124
+ try {
1125
+ // Enable sidechain first — Live rejects routing writes on a
1126
+ // compressor with the sidechain disabled. Property is available
1127
+ // on Live 10+. Try/catch for legacy builds.
1128
+ try { cursor_a.set("sidechain_enabled", 1); } catch(e) {}
1129
+
1130
+ var debug = {};
1131
+
1132
+ // --- Routing TYPE (the source: "1-DRUMS", "Ext. In", "No Input", …)
1133
+ if (routing_type) {
1134
+ var types = read_json_prop("available_sidechain_input_routing_types");
1135
+ debug.requested_type = routing_type;
1136
+ debug.type_count = types && types.length ? types.length : 0;
1137
+ var t_match = find_by_name(types, routing_type);
1138
+ if (t_match && t_match.identifier !== undefined) {
1139
+ // LOM expects a RoutingType object; Max JS accepts a
1140
+ // JSON-encoded {identifier: N} for the `set`.
1141
+ cursor_a.set(
1142
+ "sidechain_input_routing_type",
1143
+ JSON.stringify({identifier: t_match.identifier})
1144
+ );
1145
+ debug.set_type = "ok (identifier=" + t_match.identifier + ")";
1146
+ } else {
1147
+ debug.set_type = "FAIL: no matching type";
1148
+ if (types) {
1149
+ debug.available_types = [];
1150
+ for (var i = 0; i < types.length; i++) {
1151
+ debug.available_types.push(types[i].display_name || types[i].name || "?");
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ // --- Routing CHANNEL (the tap point: "Post FX", "Pre FX", …)
1158
+ if (routing_channel) {
1159
+ var channels = read_json_prop("available_sidechain_input_routing_channels");
1160
+ debug.requested_channel = routing_channel;
1161
+ debug.channel_count = channels && channels.length ? channels.length : 0;
1162
+ var c_match = find_by_name(channels, routing_channel);
1163
+ if (c_match && c_match.identifier !== undefined) {
1164
+ cursor_a.set(
1165
+ "sidechain_input_routing_channel",
1166
+ JSON.stringify({identifier: c_match.identifier})
1167
+ );
1168
+ debug.set_channel = "ok (identifier=" + c_match.identifier + ")";
1169
+ } else {
1170
+ debug.set_channel = "FAIL: no matching channel";
1171
+ if (channels) {
1172
+ debug.available_channels = [];
1173
+ for (var j = 0; j < channels.length; j++) {
1174
+ debug.available_channels.push(channels[j].display_name || channels[j].name || "?");
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+
1180
+ // Read back canonical display_name for confirmation
1181
+ var cur_type = read_json_prop("sidechain_input_routing_type");
1182
+ var cur_channel = read_json_prop("sidechain_input_routing_channel");
1183
+ var read_type_name = (cur_type && cur_type.display_name) || "";
1184
+ var read_channel_name = (cur_channel && cur_channel.display_name) || "";
1185
+
1186
+ send_response({
1187
+ "ok": true,
1188
+ "track_index": track_idx,
1189
+ "device_index": device_idx,
1190
+ "sidechain": {
1191
+ "type": read_type_name,
1192
+ "channel": read_channel_name,
1193
+ "enabled": 1
1194
+ },
1195
+ "debug": debug
1196
+ });
1197
+ } catch(e) {
1198
+ send_response({
1199
+ "error": "compressor_set_sidechain failed: " + e.message
1200
+ + " (this Live build may not expose sidechain_input_routing_* —"
1201
+ + " user must set routing manually)"
1202
+ });
1203
+ }
1204
+ }
1205
+
994
1206
  // ── Phase 2: Warp Markers ─────────────────────────────────────────────
995
1207
 
996
1208
  function cmd_get_warp_markers(args) {
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.10.6"
2
+ __version__ = "1.10.8"