livepilot 1.9.23 → 1.10.0

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 (191) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +119 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +144 -13
  6. package/bin/livepilot.js +87 -0
  7. package/installer/codex.js +147 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/skills/livepilot-core/SKILL.md +21 -4
  11. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
  12. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
  13. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
  14. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
  15. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
  16. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
  17. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
  18. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
  19. package/livepilot/skills/livepilot-core/references/overview.md +13 -9
  20. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
  21. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
  22. package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
  23. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  24. package/livepilot/skills/livepilot-release/SKILL.md +19 -5
  25. package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
  26. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
  27. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
  28. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
  29. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
  30. package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
  31. package/livepilot.mcpb +0 -0
  32. package/m4l_device/livepilot_bridge.js +1 -1
  33. package/manifest.json +2 -2
  34. package/mcp_server/__init__.py +1 -1
  35. package/mcp_server/atlas/__init__.py +357 -0
  36. package/mcp_server/atlas/device_atlas.json +44067 -0
  37. package/mcp_server/atlas/enrichments/__init__.py +111 -0
  38. package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
  39. package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
  40. package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
  41. package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
  42. package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
  43. package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
  44. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
  45. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
  46. package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
  47. package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
  48. package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
  49. package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
  50. package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
  51. package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
  52. package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
  53. package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
  54. package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
  55. package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
  56. package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
  57. package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
  58. package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
  59. package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
  60. package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
  61. package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
  62. package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
  63. package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
  64. package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
  65. package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
  66. package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
  67. package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
  68. package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
  69. package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
  70. package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
  71. package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
  72. package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
  73. package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
  74. package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
  75. package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
  76. package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
  77. package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
  78. package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
  79. package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
  80. package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
  81. package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
  82. package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
  83. package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
  84. package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
  85. package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
  86. package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
  87. package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
  88. package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
  89. package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
  90. package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
  91. package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
  92. package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
  93. package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
  94. package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
  95. package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
  96. package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
  97. package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
  98. package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
  99. package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
  100. package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
  101. package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
  102. package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
  103. package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
  104. package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
  105. package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
  106. package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
  107. package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
  108. package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
  109. package/mcp_server/atlas/scanner.py +236 -0
  110. package/mcp_server/atlas/tools.py +224 -0
  111. package/mcp_server/composer/__init__.py +1 -0
  112. package/mcp_server/composer/engine.py +452 -0
  113. package/mcp_server/composer/layer_planner.py +427 -0
  114. package/mcp_server/composer/prompt_parser.py +329 -0
  115. package/mcp_server/composer/tools.py +201 -0
  116. package/mcp_server/connection.py +53 -8
  117. package/mcp_server/corpus/__init__.py +377 -0
  118. package/mcp_server/device_forge/__init__.py +1 -0
  119. package/mcp_server/device_forge/builder.py +377 -0
  120. package/mcp_server/device_forge/models.py +142 -0
  121. package/mcp_server/device_forge/templates.py +483 -0
  122. package/mcp_server/device_forge/tools.py +162 -0
  123. package/mcp_server/hook_hunter/analyzer.py +23 -0
  124. package/mcp_server/hook_hunter/models.py +1 -0
  125. package/mcp_server/hook_hunter/tools.py +4 -2
  126. package/mcp_server/m4l_bridge.py +1 -0
  127. package/mcp_server/memory/taste_graph.py +68 -1
  128. package/mcp_server/memory/tools.py +15 -4
  129. package/mcp_server/musical_intelligence/detectors.py +14 -1
  130. package/mcp_server/musical_intelligence/tools.py +11 -8
  131. package/mcp_server/persistence/__init__.py +1 -0
  132. package/mcp_server/persistence/base_store.py +82 -0
  133. package/mcp_server/persistence/project_store.py +106 -0
  134. package/mcp_server/persistence/taste_store.py +122 -0
  135. package/mcp_server/preview_studio/models.py +1 -0
  136. package/mcp_server/preview_studio/tools.py +56 -13
  137. package/mcp_server/runtime/capability.py +66 -0
  138. package/mcp_server/runtime/capability_probe.py +137 -0
  139. package/mcp_server/runtime/execution_router.py +143 -0
  140. package/mcp_server/runtime/live_version.py +102 -0
  141. package/mcp_server/runtime/remote_commands.py +87 -0
  142. package/mcp_server/runtime/tools.py +18 -4
  143. package/mcp_server/sample_engine/__init__.py +1 -0
  144. package/mcp_server/sample_engine/analyzer.py +216 -0
  145. package/mcp_server/sample_engine/critics.py +390 -0
  146. package/mcp_server/sample_engine/models.py +193 -0
  147. package/mcp_server/sample_engine/moves.py +127 -0
  148. package/mcp_server/sample_engine/planner.py +186 -0
  149. package/mcp_server/sample_engine/sources.py +540 -0
  150. package/mcp_server/sample_engine/techniques.py +908 -0
  151. package/mcp_server/sample_engine/tools.py +442 -0
  152. package/mcp_server/semantic_moves/__init__.py +3 -0
  153. package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
  154. package/mcp_server/semantic_moves/mix_moves.py +41 -41
  155. package/mcp_server/semantic_moves/performance_moves.py +13 -13
  156. package/mcp_server/semantic_moves/sample_compilers.py +372 -0
  157. package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
  158. package/mcp_server/semantic_moves/tools.py +18 -17
  159. package/mcp_server/semantic_moves/transition_moves.py +16 -16
  160. package/mcp_server/server.py +51 -0
  161. package/mcp_server/services/__init__.py +1 -0
  162. package/mcp_server/services/motif_service.py +67 -0
  163. package/mcp_server/session_continuity/tracker.py +29 -1
  164. package/mcp_server/song_brain/builder.py +28 -1
  165. package/mcp_server/song_brain/models.py +4 -0
  166. package/mcp_server/song_brain/tools.py +20 -2
  167. package/mcp_server/sound_design/critics.py +89 -1
  168. package/mcp_server/splice_client/__init__.py +1 -0
  169. package/mcp_server/splice_client/client.py +347 -0
  170. package/mcp_server/splice_client/models.py +96 -0
  171. package/mcp_server/splice_client/protos/__init__.py +1 -0
  172. package/mcp_server/splice_client/protos/app_pb2.py +319 -0
  173. package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
  174. package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
  175. package/mcp_server/tools/arrangement.py +69 -0
  176. package/mcp_server/tools/automation.py +15 -2
  177. package/mcp_server/tools/devices.py +117 -6
  178. package/mcp_server/tools/notes.py +37 -4
  179. package/mcp_server/wonder_mode/diagnosis.py +5 -0
  180. package/mcp_server/wonder_mode/engine.py +85 -1
  181. package/mcp_server/wonder_mode/tools.py +6 -1
  182. package/package.json +12 -2
  183. package/remote_script/LivePilot/__init__.py +8 -1
  184. package/remote_script/LivePilot/arrangement.py +114 -0
  185. package/remote_script/LivePilot/browser.py +56 -1
  186. package/remote_script/LivePilot/devices.py +236 -6
  187. package/remote_script/LivePilot/mixing.py +8 -3
  188. package/remote_script/LivePilot/server.py +5 -1
  189. package/remote_script/LivePilot/transport.py +3 -0
  190. package/remote_script/LivePilot/version_detect.py +78 -0
  191. package/scripts/sync_metadata.py +132 -0
@@ -1,5 +1,5 @@
1
1
  """
2
- LivePilot - Browser domain handlers (5 commands).
2
+ LivePilot - Browser domain handlers (6 commands).
3
3
  """
4
4
 
5
5
  import Live
@@ -386,6 +386,61 @@ def load_browser_item(song, params):
386
386
  )
387
387
 
388
388
 
389
+ _SCAN_MAX_ITERATIONS = 100000
390
+
391
+
392
+ def _scan_recursive(item, results, depth, max_depth, max_per_category,
393
+ _counter=None):
394
+ """Recursively collect loadable browser items with iteration cap."""
395
+ if _counter is None:
396
+ _counter = [0]
397
+ if depth > max_depth or len(results) >= max_per_category:
398
+ return
399
+ for child in item.children:
400
+ _counter[0] += 1
401
+ if _counter[0] > _SCAN_MAX_ITERATIONS or len(results) >= max_per_category:
402
+ return
403
+ if child.is_loadable:
404
+ entry = {"name": child.name, "is_loadable": True}
405
+ try:
406
+ entry["uri"] = child.uri
407
+ except AttributeError:
408
+ entry["uri"] = None
409
+ results.append(entry)
410
+ if child.is_folder:
411
+ _scan_recursive(
412
+ child, results, depth + 1, max_depth, max_per_category,
413
+ _counter
414
+ )
415
+ if len(results) >= max_per_category:
416
+ return
417
+
418
+
419
+ @register("scan_browser_deep")
420
+ def scan_browser_deep(song, params):
421
+ """Walk the entire browser tree and return all loadable items by category.
422
+
423
+ Parameters
424
+ ----------
425
+ max_per_category : int, optional
426
+ Maximum items to collect per top-level category (default 1000).
427
+ max_depth : int, optional
428
+ Maximum recursion depth into the browser tree (default 4).
429
+ """
430
+ max_per_category = int(params.get("max_per_category", 1000))
431
+ max_depth = int(params.get("max_depth", 4))
432
+ browser = _get_browser()
433
+ categories = _get_categories(browser)
434
+
435
+ result = {}
436
+ for cat_name, cat_item in categories.items():
437
+ items = []
438
+ _scan_recursive(cat_item, items, 0, max_depth, max_per_category)
439
+ result[cat_name] = items
440
+
441
+ return {"categories": result}
442
+
443
+
389
444
  @register("get_device_presets")
390
445
  def get_device_presets(song, params):
391
446
  """List available presets for a device type by searching the browser.
@@ -1,5 +1,5 @@
1
1
  """
2
- LivePilot - Device domain handlers (11 commands).
2
+ LivePilot - Device domain handlers (12 commands).
3
3
  """
4
4
 
5
5
  import Live
@@ -46,7 +46,7 @@ def get_device_parameters(song, params):
46
46
 
47
47
  parameters = []
48
48
  for i, param in enumerate(device.parameters):
49
- parameters.append({
49
+ info = {
50
50
  "index": i,
51
51
  "name": param.name,
52
52
  "value": param.value,
@@ -54,7 +54,13 @@ def get_device_parameters(song, params):
54
54
  "max": param.max,
55
55
  "is_quantized": param.is_quantized,
56
56
  "value_string": param.str_for_value(param.value),
57
- })
57
+ }
58
+ # 12.2+ feature: native display_value
59
+ try:
60
+ info["display_value"] = param.display_value
61
+ except AttributeError:
62
+ pass
63
+ parameters.append(info)
58
64
  return {"parameters": parameters}
59
65
 
60
66
 
@@ -104,13 +110,19 @@ def set_device_parameter(song, params):
104
110
  raise ValueError("Must provide parameter_name or parameter_index")
105
111
 
106
112
  param.value = value
107
- return {
113
+ result = {
108
114
  "name": param.name,
109
115
  "value": param.value,
110
116
  "value_string": param.str_for_value(param.value),
111
117
  "min": param.min,
112
118
  "max": param.max,
113
119
  }
120
+ # 12.2+: include display_value
121
+ try:
122
+ result["display_value"] = param.display_value
123
+ except AttributeError:
124
+ pass
125
+ return result
114
126
 
115
127
 
116
128
  @register("batch_set_parameters")
@@ -163,11 +175,17 @@ def batch_set_parameters(song, params):
163
175
  )
164
176
 
165
177
  param.value = value
166
- results.append({
178
+ result_entry = {
167
179
  "name": param.name,
168
180
  "value": param.value,
169
181
  "value_string": param.str_for_value(param.value),
170
- })
182
+ }
183
+ # 12.2+: include display_value
184
+ try:
185
+ result_entry["display_value"] = param.display_value
186
+ except AttributeError:
187
+ pass
188
+ results.append(result_entry)
171
189
 
172
190
  return {"parameters": results}
173
191
 
@@ -408,6 +426,198 @@ def load_device_by_uri(song, params):
408
426
  )
409
427
 
410
428
 
429
+ # ── Device name registry for insert_device (12.3+) ──────────────────────
430
+
431
+ NATIVE_DEVICE_NAMES = frozenset({
432
+ # Instruments
433
+ "Analog", "Collision", "Drift", "Electric", "Drum Rack",
434
+ "Instrument Rack", "Meld", "Operator", "Sampler", "Simpler",
435
+ "Tension", "Wavetable",
436
+ # Audio Effects
437
+ "Align Delay", "Amp", "Audio Effect Rack", "Auto Filter",
438
+ "Auto Pan-Tremolo", "Auto Shift", "Beat Repeat", "Cabinet",
439
+ "Channel EQ", "Chorus-Ensemble", "Color Limiter", "Compressor",
440
+ "Convolution Reverb", "Corpus", "Delay", "Drum Buss",
441
+ "Dynamic Tube", "Echo", "EQ Eight", "EQ Three", "Erosion",
442
+ "External Audio Effect", "Flanger", "Frequency Shifter", "Gate",
443
+ "Glue Compressor", "Grain Delay", "Hybrid Reverb", "Limiter",
444
+ "Looper", "Multiband Dynamics", "Overdrive", "Pedal",
445
+ "Phaser-Flanger", "Pitch Hack", "Redux", "Re-Enveloper",
446
+ "Resonators", "Reverb", "Roar", "Saturator", "Shifter",
447
+ "Spectral Blur", "Spectral Resonator", "Spectral Time", "Tuner",
448
+ "Utility", "Vinyl Distortion", "Vocoder",
449
+ # MIDI Effects
450
+ "Arpeggiator", "Chord", "Expression Control", "MIDI Effect Rack",
451
+ "Note Echo", "Note Length", "Pitch", "Random", "Scale", "Strum",
452
+ "Velocity",
453
+ })
454
+
455
+ # Case-insensitive lookup for user convenience
456
+ _DEVICE_NAME_LOOKUP = {name.lower(): name for name in NATIVE_DEVICE_NAMES}
457
+
458
+
459
+ @register("insert_device")
460
+ def insert_device(song, params):
461
+ """Insert a native Live device by name (12.3+ API).
462
+
463
+ Much faster than browser search — a single call with no state dependency.
464
+ Only works for native devices (not plugins or M4L).
465
+
466
+ Required: track_index, device_name
467
+ Optional: position (-1 = end of chain, default), chain_index + device_index (for rack chains)
468
+ """
469
+ from .version_detect import has_feature
470
+
471
+ if not has_feature("insert_device"):
472
+ raise RuntimeError(
473
+ "insert_device requires Live 12.3+. "
474
+ "Use find_and_load_device (browser search) instead."
475
+ )
476
+
477
+ track_index = int(params["track_index"])
478
+ device_name = str(params["device_name"])
479
+ position = int(params.get("position", -1))
480
+ chain_index = params.get("chain_index")
481
+
482
+ # Resolve canonical name (case-insensitive)
483
+ canonical = _DEVICE_NAME_LOOKUP.get(device_name.lower())
484
+ if canonical is None:
485
+ raise ValueError(
486
+ "Device '%s' is not a native Live device. "
487
+ "insert_device only supports native devices (not plugins or M4L). "
488
+ "Use find_and_load_device for plugins."
489
+ % device_name
490
+ )
491
+
492
+ track = get_track(song, track_index)
493
+
494
+ song.begin_undo_step()
495
+ try:
496
+ if chain_index is not None:
497
+ # 12.3+ Chain.insert_device — insert into a rack chain
498
+ chain_index = int(chain_index)
499
+ device_on_track = get_device(track, int(params.get("device_index", 0)))
500
+ chains = list(device_on_track.chains)
501
+ if chain_index < 0 or chain_index >= len(chains):
502
+ raise IndexError(
503
+ "Chain index %d out of range (0..%d)"
504
+ % (chain_index, len(chains) - 1)
505
+ )
506
+ chain = chains[chain_index]
507
+ if position >= 0:
508
+ device = chain.insert_device(canonical, position)
509
+ else:
510
+ device = chain.insert_device(canonical)
511
+ else:
512
+ # Track-level insertion
513
+ if position >= 0:
514
+ device = track.insert_device(canonical, position)
515
+ else:
516
+ device = track.insert_device(canonical)
517
+ finally:
518
+ song.end_undo_step()
519
+
520
+ # Read back the device info — use "loaded" key to match
521
+ # the convention expected by _postflight_loaded_device on MCP side
522
+ result = {
523
+ "loaded": device.name,
524
+ "class_name": device.class_name,
525
+ "track_index": track_index,
526
+ "parameter_count": len(list(device.parameters)),
527
+ }
528
+ if position >= 0:
529
+ result["position"] = position
530
+ return result
531
+
532
+
533
+ @register("insert_rack_chain")
534
+ def insert_rack_chain(song, params):
535
+ """Insert a new chain into an Instrument Rack, Audio Effect Rack, or Drum Rack (12.3+).
536
+
537
+ Required: track_index, device_index
538
+ Optional: position (-1 = end)
539
+ """
540
+ from .version_detect import has_feature
541
+
542
+ if not has_feature("insert_chain"):
543
+ raise RuntimeError(
544
+ "insert_rack_chain requires Live 12.3+."
545
+ )
546
+
547
+ track_index = int(params["track_index"])
548
+ device_index = int(params["device_index"])
549
+ position = int(params.get("position", -1))
550
+
551
+ track = get_track(song, track_index)
552
+ device = get_device(track, device_index)
553
+
554
+ if not device.can_have_chains:
555
+ raise ValueError(
556
+ "Device '%s' is not a rack — cannot insert chains"
557
+ % device.name
558
+ )
559
+
560
+ song.begin_undo_step()
561
+ try:
562
+ if position >= 0:
563
+ device.insert_chain(position)
564
+ else:
565
+ device.insert_chain()
566
+ finally:
567
+ song.end_undo_step()
568
+
569
+ chain_count = len(list(device.chains))
570
+ return {
571
+ "inserted": True,
572
+ "track_index": track_index,
573
+ "device_index": device_index,
574
+ "chain_count": chain_count,
575
+ }
576
+
577
+
578
+ @register("set_drum_chain_note")
579
+ def set_drum_chain_note(song, params):
580
+ """Set which MIDI note triggers a drum chain (12.3+).
581
+
582
+ Required: track_index, device_index, chain_index, note
583
+ note: MIDI note number (0-127), or -1 for 'All Notes'
584
+ """
585
+ from .version_detect import has_feature
586
+
587
+ if not has_feature("drum_chain_in_note"):
588
+ raise RuntimeError(
589
+ "set_drum_chain_note requires Live 12.3+."
590
+ )
591
+
592
+ track_index = int(params["track_index"])
593
+ device_index = int(params["device_index"])
594
+ chain_index = int(params["chain_index"])
595
+ note = int(params["note"])
596
+
597
+ if note < -1 or note > 127:
598
+ raise ValueError("note must be -1 (All Notes) or 0-127")
599
+
600
+ track = get_track(song, track_index)
601
+ device = get_device(track, device_index)
602
+
603
+ chains = list(device.chains)
604
+ if chain_index < 0 or chain_index >= len(chains):
605
+ raise IndexError(
606
+ "Chain index %d out of range (0..%d)"
607
+ % (chain_index, len(chains) - 1)
608
+ )
609
+
610
+ chain = chains[chain_index]
611
+ chain.in_note = note
612
+
613
+ return {
614
+ "track_index": track_index,
615
+ "device_index": device_index,
616
+ "chain_index": chain_index,
617
+ "in_note": note,
618
+ }
619
+
620
+
411
621
  @register("find_and_load_device")
412
622
  def find_and_load_device(song, params):
413
623
  """Find a device by name in the browser and load it onto a track.
@@ -420,6 +630,26 @@ def find_and_load_device(song, params):
420
630
  track = get_track(song, track_index)
421
631
  browser = _get_browser()
422
632
 
633
+ # 12.3+ fast path: try insert_device for native devices
634
+ from .version_detect import has_feature
635
+ if has_feature("insert_device"):
636
+ canonical = _DEVICE_NAME_LOOKUP.get(device_name)
637
+ if canonical is not None:
638
+ try:
639
+ song.begin_undo_step()
640
+ try:
641
+ device = track.insert_device(canonical)
642
+ finally:
643
+ song.end_undo_step()
644
+ return {
645
+ "loaded": device.name,
646
+ "class_name": device.class_name,
647
+ "track_index": track_index,
648
+ "parameter_count": len(list(device.parameters)),
649
+ }
650
+ except Exception:
651
+ pass # Fall through to browser search
652
+
423
653
  MAX_ITERATIONS = 50000
424
654
  iterations = 0
425
655
 
@@ -133,14 +133,15 @@ def get_track_meters(song, params):
133
133
  "index": idx,
134
134
  "name": track.name,
135
135
  }
136
- if track.has_audio_output:
136
+ muted = bool(getattr(track, "mute", False))
137
+ if track.has_audio_output and not muted:
137
138
  entry["level"] = track.output_meter_level
138
139
  if include_stereo:
139
140
  entry["left"] = track.output_meter_left
140
141
  entry["right"] = track.output_meter_right
141
142
  else:
142
143
  entry["level"] = 0.0
143
- entry["has_audio_output"] = False
144
+ entry["has_audio_output"] = bool(getattr(track, "has_audio_output", False))
144
145
  if include_stereo:
145
146
  entry["left"] = 0.0
146
147
  entry["right"] = 0.0
@@ -177,7 +178,11 @@ def get_mix_snapshot(song, params):
177
178
  tracks.append({
178
179
  "index": i,
179
180
  "name": track.name,
180
- "meter_level": track.output_meter_level if track.has_audio_output else 0.0,
181
+ "meter_level": (
182
+ track.output_meter_level
183
+ if track.has_audio_output and not bool(getattr(track, "mute", False))
184
+ else 0.0
185
+ ),
181
186
  "volume": track.mixer_device.volume.value,
182
187
  "pan": track.mixer_device.panning.value,
183
188
  "mute": track.mute,
@@ -255,7 +255,11 @@ class LivePilotServer(object):
255
255
  except AssertionError:
256
256
  # ControlSurface is disconnecting — return error instead of
257
257
  # running LOM calls on the TCP thread (which would be unsafe)
258
- response_queue.put({
258
+ try:
259
+ self._command_queue.get_nowait()
260
+ except queue.Empty:
261
+ pass
262
+ self._send(client, {
259
263
  "id": request_id,
260
264
  "ok": False,
261
265
  "error": {"code": "STATE_ERROR", "message": "Script is disconnecting"},
@@ -3,6 +3,7 @@ LivePilot - Transport domain handlers (10 commands).
3
3
  """
4
4
 
5
5
  from .router import register
6
+ from .version_detect import version_string, get_api_features
6
7
 
7
8
 
8
9
  @register("get_session_info")
@@ -59,6 +60,8 @@ def get_session_info(song, params):
59
60
  "tracks": tracks_info,
60
61
  "return_tracks": return_tracks_info,
61
62
  "scenes": scenes_info,
63
+ "live_version": version_string(),
64
+ "api_features": get_api_features(),
62
65
  }
63
66
 
64
67
 
@@ -0,0 +1,78 @@
1
+ """
2
+ LivePilot - Ableton Live version detection and feature flags.
3
+
4
+ Detects the running Live version and provides feature availability checks.
5
+ Used by handlers to conditionally use new APIs (12.1.10+, 12.2+, 12.3+).
6
+ """
7
+
8
+ import Live
9
+
10
+ # ── Feature version requirements ────────────────────────────────────────
11
+
12
+ FEATURES = {
13
+ "create_midi_clip_arrangement": (12, 1, 10),
14
+ "looper_export": (12, 1, 0),
15
+ "tuning_system": (12, 1, 0),
16
+ "display_value": (12, 2, 0),
17
+ "clip_start_time_observable": (12, 2, 0),
18
+ "take_lanes_api": (12, 2, 0),
19
+ "insert_device": (12, 3, 0),
20
+ "insert_chain": (12, 3, 0),
21
+ "drum_chain_in_note": (12, 3, 0),
22
+ "stem_separation": (12, 3, 0),
23
+ "replace_sample_native": (12, 4, 0),
24
+ }
25
+
26
+
27
+ # ── Cached version ──────────────────────────────────────────────────────
28
+
29
+ _cached_version = None
30
+
31
+
32
+ def get_live_version():
33
+ """Return (major, minor, patch) of the running Live instance.
34
+
35
+ Uses Live.Application.get_application() to read version info.
36
+ Falls back to (12, 0, 0) if detection fails.
37
+ """
38
+ global _cached_version
39
+ if _cached_version is not None:
40
+ return _cached_version
41
+
42
+ try:
43
+ app = Live.Application.get_application()
44
+ major = app.get_major_version()
45
+ minor = app.get_minor_version()
46
+ # get_bugfix_version() was added later; fall back to 0
47
+ try:
48
+ patch = app.get_bugfix_version()
49
+ except AttributeError:
50
+ patch = 0
51
+ _cached_version = (int(major), int(minor), int(patch))
52
+ except Exception:
53
+ _cached_version = (12, 0, 0)
54
+
55
+ return _cached_version
56
+
57
+
58
+ def has_feature(feature_name):
59
+ """Check if a feature is available in the running Live version.
60
+
61
+ Returns True if the detected version >= the feature's required version.
62
+ Returns False for unknown feature names (safe default).
63
+ """
64
+ required = FEATURES.get(feature_name)
65
+ if required is None:
66
+ return False
67
+ return get_live_version() >= required
68
+
69
+
70
+ def get_api_features():
71
+ """Return a dict of all feature flags for the current version."""
72
+ return {name: has_feature(name) for name in FEATURES}
73
+
74
+
75
+ def version_string():
76
+ """Return version as a dot-separated string, e.g. '12.3.6'."""
77
+ v = get_live_version()
78
+ return "%d.%d.%d" % v
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env python3
2
+ """Metadata sync — single source of truth for version and tool count.
3
+
4
+ Reads version from package.json, tool count from test_tools_contract.py,
5
+ and verifies all known locations are in sync.
6
+
7
+ Usage:
8
+ python scripts/sync_metadata.py --check # verify, exit 1 if stale
9
+ python scripts/sync_metadata.py --fix # auto-fix stale references
10
+ """
11
+
12
+ import json
13
+ import re
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+
19
+
20
+ def get_version() -> str:
21
+ """Read version from package.json (source of truth)."""
22
+ pkg = json.loads((ROOT / "package.json").read_text())
23
+ return pkg["version"]
24
+
25
+
26
+ def get_tool_count() -> int:
27
+ """Read tool count from test_tools_contract.py assertion."""
28
+ src = (ROOT / "tests" / "test_tools_contract.py").read_text()
29
+ match = re.search(r"assert len\(tools\) == (\d+)", src)
30
+ if match:
31
+ return int(match.group(1))
32
+ raise ValueError("Could not find tool count assertion in test_tools_contract.py")
33
+
34
+
35
+ # Files that must contain the version string
36
+ VERSION_FILES = [
37
+ "package.json",
38
+ "server.json",
39
+ "manifest.json",
40
+ "livepilot/.claude-plugin/plugin.json",
41
+ "livepilot/.Codex-plugin/plugin.json",
42
+ ".claude-plugin/marketplace.json",
43
+ "mcp_server/__init__.py",
44
+ "remote_script/LivePilot/__init__.py",
45
+ "CLAUDE.md",
46
+ "AGENTS.md",
47
+ "livepilot/skills/livepilot-core/references/overview.md",
48
+ "docs/M4L_BRIDGE.md",
49
+ ]
50
+
51
+ # Files that must contain the tool count
52
+ TOOL_COUNT_FILES = [
53
+ "README.md",
54
+ "package.json",
55
+ "server.json",
56
+ "CLAUDE.md",
57
+ "AGENTS.md",
58
+ "CONTRIBUTING.md",
59
+ "livepilot/.claude-plugin/plugin.json",
60
+ "livepilot/.Codex-plugin/plugin.json",
61
+ "livepilot/skills/livepilot-core/SKILL.md",
62
+ "livepilot/skills/livepilot-core/references/overview.md",
63
+ "docs/manual/index.md",
64
+ "docs/manual/tool-reference.md",
65
+ "docs/manual/tool-catalog.md",
66
+ ]
67
+
68
+
69
+ def check_version(version: str) -> list[str]:
70
+ """Check all version files for staleness."""
71
+ issues = []
72
+ for rel_path in VERSION_FILES:
73
+ path = ROOT / rel_path
74
+ if not path.exists():
75
+ continue
76
+ content = path.read_text()
77
+ if version not in content:
78
+ # Find what version IS there
79
+ old = re.search(r"1\.\d+\.\d+", content)
80
+ old_ver = old.group(0) if old else "???"
81
+ if old_ver != version:
82
+ issues.append(f" {rel_path}: has {old_ver}, expected {version}")
83
+ return issues
84
+
85
+
86
+ def check_tool_count(count: int) -> list[str]:
87
+ """Check all tool count files for staleness."""
88
+ issues = []
89
+ count_str = str(count)
90
+ for rel_path in TOOL_COUNT_FILES:
91
+ path = ROOT / rel_path
92
+ if not path.exists():
93
+ continue
94
+ content = path.read_text()
95
+ # Look for "N tools" pattern
96
+ matches = re.findall(r"(\d+)\s+tools", content)
97
+ for m in matches:
98
+ if m != count_str and int(m) > 250: # ignore subset counts like "210 tools"
99
+ issues.append(f" {rel_path}: has '{m} tools', expected '{count_str} tools'")
100
+ break
101
+ return issues
102
+
103
+
104
+ def main():
105
+ mode = sys.argv[1] if len(sys.argv) > 1 else "--check"
106
+
107
+ version = get_version()
108
+ tool_count = get_tool_count()
109
+
110
+ print(f"Source of truth: version={version}, tools={tool_count}")
111
+
112
+ version_issues = check_version(version)
113
+ count_issues = check_tool_count(tool_count)
114
+
115
+ all_issues = version_issues + count_issues
116
+
117
+ if all_issues:
118
+ print(f"\nFound {len(all_issues)} stale reference(s):")
119
+ for issue in all_issues:
120
+ print(issue)
121
+ if mode == "--check":
122
+ sys.exit(1)
123
+ elif mode == "--fix":
124
+ print("\n--fix mode not yet implemented. Fix manually.")
125
+ sys.exit(1)
126
+ else:
127
+ print("All metadata in sync.")
128
+ sys.exit(0)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()