livepilot 1.9.11 → 1.9.13

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 (37) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +57 -0
  4. package/README.md +11 -10
  5. package/bin/livepilot.js +27 -9
  6. package/livepilot/.Codex-plugin/plugin.json +1 -1
  7. package/livepilot/.claude-plugin/plugin.json +1 -1
  8. package/livepilot/skills/livepilot-core/SKILL.md +6 -6
  9. package/livepilot/skills/livepilot-core/references/overview.md +1 -1
  10. package/livepilot/skills/livepilot-release/SKILL.md +4 -4
  11. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  12. package/m4l_device/livepilot_bridge.js +77 -3
  13. package/mcp_server/__init__.py +1 -1
  14. package/mcp_server/connection.py +5 -1
  15. package/mcp_server/curves.py +5 -1
  16. package/mcp_server/m4l_bridge.py +37 -20
  17. package/mcp_server/memory/technique_store.py +30 -2
  18. package/mcp_server/server.py +10 -2
  19. package/mcp_server/tools/analyzer.py +12 -1
  20. package/mcp_server/tools/arrangement.py +20 -5
  21. package/mcp_server/tools/automation.py +52 -0
  22. package/mcp_server/tools/devices.py +2 -3
  23. package/mcp_server/tools/generative.py +4 -0
  24. package/mcp_server/tools/midi_io.py +22 -4
  25. package/mcp_server/tools/theory.py +8 -1
  26. package/mcp_server/tools/transport.py +16 -6
  27. package/package.json +1 -1
  28. package/remote_script/LivePilot/__init__.py +3 -3
  29. package/remote_script/LivePilot/arrangement.py +12 -18
  30. package/remote_script/LivePilot/browser.py +19 -8
  31. package/remote_script/LivePilot/clip_automation.py +5 -4
  32. package/remote_script/LivePilot/clips.py +14 -6
  33. package/remote_script/LivePilot/devices.py +6 -3
  34. package/remote_script/LivePilot/server.py +20 -7
  35. package/remote_script/LivePilot/tracks.py +7 -2
  36. package/remote_script/LivePilot/transport.py +3 -3
  37. package/requirements.txt +6 -1
@@ -3,6 +3,7 @@ LivePilot - Device domain handlers (11 commands).
3
3
  """
4
4
 
5
5
  import Live
6
+ from collections import deque
6
7
 
7
8
  from .router import register
8
9
  from .utils import get_track, get_device
@@ -178,6 +179,8 @@ def toggle_device(song, params):
178
179
  break
179
180
  if on_param is None:
180
181
  # Fallback to parameter 0 for devices that don't use "Device On"
182
+ if not list(device.parameters):
183
+ raise ValueError("Device '%s' has no parameters to toggle" % device.name)
181
184
  on_param = device.parameters[0]
182
185
 
183
186
  on_param.value = 1.0 if active else 0.0
@@ -399,10 +402,10 @@ def find_and_load_device(song, params):
399
402
  This ensures raw 'Operator' is found before 'Hello Operator.adg' buried
400
403
  in a user_library subfolder."""
401
404
  nonlocal iterations
402
- # Queue of (item, depth) tuples
403
- queue = [(category, 0)]
405
+ # Queue of (item, depth) tuples — deque for O(1) popleft
406
+ queue = deque([(category, 0)])
404
407
  while queue:
405
- item, depth = queue.pop(0)
408
+ item, depth = queue.popleft()
406
409
  if depth > 8:
407
410
  continue
408
411
  try:
@@ -95,7 +95,8 @@ class LivePilotServer(object):
95
95
  self._thread = threading.Thread(target=self._server_loop)
96
96
  self._thread.daemon = True
97
97
  self._thread.start()
98
- self._log("Server started on %s:%d" % (self._host, self._port))
98
+ # Note: "Listening on ..." is logged from _server_loop after bind
99
+ # succeeds. Don't log "Server started" here — bind may still fail.
99
100
 
100
101
  def stop(self):
101
102
  """Shutdown the server gracefully."""
@@ -187,12 +188,12 @@ class LivePilotServer(object):
187
188
  except OSError as exc:
188
189
  self._log("Client error: %s" % exc)
189
190
  finally:
190
- with self._client_lock:
191
- self._client_connected = False
192
191
  try:
193
192
  client.close()
194
193
  except OSError:
195
194
  pass
195
+ with self._client_lock:
196
+ self._client_connected = False
196
197
  self._log("Client disconnected")
197
198
 
198
199
  def _handle_client(self, client):
@@ -205,6 +206,9 @@ class LivePilotServer(object):
205
206
  if not data:
206
207
  break
207
208
  buf += data.decode("utf-8", errors="replace")
209
+ if len(buf) > 4 * 1024 * 1024: # 4 MB
210
+ self._log("Client buffer overflow — disconnecting")
211
+ break
208
212
  while "\n" in buf:
209
213
  line, buf = buf.split("\n", 1)
210
214
  line = line.strip()
@@ -249,8 +253,14 @@ class LivePilotServer(object):
249
253
  try:
250
254
  self._cs.schedule_message(0, self._process_next_command)
251
255
  except AssertionError:
252
- # Already on main thread process directly
253
- self._process_next_command()
256
+ # ControlSurface is disconnectingreturn error instead of
257
+ # running LOM calls on the TCP thread (which would be unsafe)
258
+ response_queue.put({
259
+ "id": request_id,
260
+ "ok": False,
261
+ "error": {"code": "STATE_ERROR", "message": "Script is disconnecting"},
262
+ })
263
+ return
254
264
 
255
265
  # Wait for response from main thread
256
266
  try:
@@ -296,7 +306,8 @@ class LivePilotServer(object):
296
306
  try:
297
307
  self._cs.schedule_message(1, send_response) # ~100ms
298
308
  except AssertionError:
299
- send_response()
309
+ # ControlSurface disconnecting — send result immediately
310
+ response_queue.put(result)
300
311
  else:
301
312
  response_queue.put(result)
302
313
  # Drain any remaining queued commands
@@ -308,7 +319,9 @@ class LivePilotServer(object):
308
319
  try:
309
320
  self._cs.schedule_message(0, self._process_next_command)
310
321
  except AssertionError:
311
- self._process_next_command()
322
+ # ControlSurface disconnecting — drop remaining commands
323
+ # rather than running LOM calls on the wrong thread
324
+ pass
312
325
 
313
326
  # ── Socket I/O ───────────────────────────────────────────────────────
314
327
 
@@ -175,9 +175,14 @@ def duplicate_track(song, params):
175
175
  "Track index %d out of range (0..%d)"
176
176
  % (track_index, len(tracks) - 1)
177
177
  )
178
+ count_before = len(tracks)
178
179
  song.duplicate_track(track_index)
179
- new_index = track_index + 1
180
- return {"index": new_index, "name": list(song.tracks)[new_index].name}
180
+ all_tracks = list(song.tracks)
181
+ # For group tracks, Ableton duplicates the group + all children.
182
+ # The duplicate block starts right after the original group's last child.
183
+ added = len(all_tracks) - count_before
184
+ new_index = track_index + added
185
+ return {"index": new_index, "name": all_tracks[new_index].name}
181
186
 
182
187
 
183
188
  @register("set_track_name")
@@ -53,9 +53,9 @@ def get_session_info(song, params):
53
53
  "metronome": song.metronome,
54
54
  "record_mode": song.record_mode,
55
55
  "session_record": song.session_record,
56
- "track_count": len(list(song.tracks)),
57
- "return_track_count": len(list(song.return_tracks)),
58
- "scene_count": len(list(song.scenes)),
56
+ "track_count": len(tracks_info),
57
+ "return_track_count": len(return_tracks_info),
58
+ "scene_count": len(scenes_info),
59
59
  "tracks": tracks_info,
60
60
  "return_tracks": return_tracks_info,
61
61
  "scenes": scenes_info,
package/requirements.txt CHANGED
@@ -3,9 +3,14 @@ numpy>=1.24.0
3
3
  fastmcp>=3.0.0,<4.0.0
4
4
  midiutil>=1.2.1
5
5
  pretty_midi>=0.2.10
6
- opycleid>=0.5.1
7
6
  # v1.8 Perception Layer (offline analysis)
8
7
  pyloudnorm>=0.1.0
9
8
  soundfile>=0.12.0
10
9
  scipy>=1.11.0
11
10
  mutagen>=1.47.0
11
+
12
+ # Development / testing (not required for runtime)
13
+ # pip install pytest pytest-asyncio
14
+
15
+ # Optional: neo-Riemannian group theory (not required — harmony engine is pure Python)
16
+ # pip install opycleid>=0.5.1