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
@@ -143,6 +143,48 @@ def create_arrangement_clip(
143
143
  return _get_ableton(ctx).send_command("create_arrangement_clip", params)
144
144
 
145
145
 
146
+ @mcp.tool()
147
+ def create_native_arrangement_clip(
148
+ ctx: Context,
149
+ track_index: int,
150
+ start_time: float,
151
+ length: float,
152
+ name: str = "",
153
+ color_index: Optional[int] = None,
154
+ ) -> dict:
155
+ """Create an empty MIDI clip directly in Arrangement View (Live 12.1.10+).
156
+
157
+ Unlike create_arrangement_clip (which duplicates a session clip), this creates
158
+ a native arrangement clip with full automation envelope support — volume rides,
159
+ filter sweeps, send automation all work natively.
160
+
161
+ Requires Live 12.1.10+. Falls back with a clear error on older versions.
162
+
163
+ track_index: 0+ for regular MIDI tracks
164
+ start_time: beat position (0.0 = song start, 4.0 = bar 2 in 4/4)
165
+ length: clip length in beats
166
+ name: optional clip display name
167
+ color_index: optional 0-69 Ableton color
168
+ """
169
+ _validate_track_index(track_index)
170
+ if start_time < 0:
171
+ raise ValueError("start_time must be >= 0")
172
+ if length <= 0:
173
+ raise ValueError("length must be > 0")
174
+
175
+ params = {
176
+ "track_index": track_index,
177
+ "start_time": start_time,
178
+ "length": length,
179
+ }
180
+ if name:
181
+ params["name"] = name
182
+ if color_index is not None:
183
+ params["color_index"] = color_index
184
+
185
+ return _get_ableton(ctx).send_command("create_native_arrangement_clip", params)
186
+
187
+
146
188
  @mcp.tool()
147
189
  def add_arrangement_notes(
148
190
  ctx: Context,
@@ -288,6 +330,33 @@ def back_to_arranger(ctx: Context) -> dict:
288
330
  return _get_ableton(ctx).send_command("back_to_arranger")
289
331
 
290
332
 
333
+ @mcp.tool()
334
+ def force_arrangement(
335
+ ctx: Context,
336
+ beat_time: float = 0,
337
+ loop_start: float = 0,
338
+ loop_length: float = 0,
339
+ play: bool = True,
340
+ ) -> dict:
341
+ """Force ALL tracks to follow the arrangement and start playback.
342
+
343
+ Atomically: stops all session clips, releases every track from
344
+ session override, sets back-to-arranger, jumps to position, and
345
+ starts playing. This is the "play my arrangement from the top"
346
+ command.
347
+
348
+ beat_time: position to start from (default 0 = beginning)
349
+ loop_start: loop region start in beats (default 0)
350
+ loop_length: loop region length in beats (0 = no loop change)
351
+ play: whether to start playback (default True)
352
+ """
353
+ params: dict = {"beat_time": beat_time, "play": play}
354
+ if loop_length > 0:
355
+ params["loop_start"] = loop_start
356
+ params["loop_length"] = loop_length
357
+ return _get_ableton(ctx).send_command("force_arrangement", params)
358
+
359
+
291
360
  @mcp.tool()
292
361
  def get_arrangement_notes(
293
362
  ctx: Context,
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  from typing import Any, Optional
11
11
 
12
12
  from fastmcp import Context
13
+ from pydantic import BaseModel, Field
13
14
 
14
15
  from ..curves import generate_curve, generate_from_recipe, list_recipes
15
16
  from ..server import mcp
@@ -27,10 +28,22 @@ def _ensure_list(v: Any) -> list:
27
28
  except json.JSONDecodeError as exc:
28
29
  raise ValueError(f"Invalid JSON in parameter: {exc}") from exc
29
30
  if isinstance(v, list):
30
- return v
31
+ normalized = []
32
+ for item in v:
33
+ if isinstance(item, BaseModel):
34
+ normalized.append(item.model_dump(exclude_none=True))
35
+ else:
36
+ normalized.append(item)
37
+ return normalized
31
38
  return [v]
32
39
 
33
40
 
41
+ class AutomationPoint(BaseModel):
42
+ time: float
43
+ value: float
44
+ duration: Optional[float] = Field(default=None, ge=0.0)
45
+
46
+
34
47
  @mcp.tool()
35
48
  def get_clip_automation(
36
49
  ctx: Context,
@@ -62,7 +75,7 @@ def set_clip_automation(
62
75
  track_index: int,
63
76
  clip_index: int,
64
77
  parameter_type: str,
65
- points: Any,
78
+ points: list[AutomationPoint] | str,
66
79
  device_index: Optional[int] = None,
67
80
  parameter_index: Optional[int] = None,
68
81
  send_index: Optional[int] = None,
@@ -1,6 +1,6 @@
1
1
  """Device MCP tools — parameters, racks, browser loading, plugin deep control.
2
2
 
3
- 15 tools matching the Remote Script devices domain + M4L bridge.
3
+ 16 tools matching the Remote Script devices domain + M4L bridge.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -359,14 +359,15 @@ def find_and_load_device(ctx: Context, track_index: int, device_name: str) -> di
359
359
  if not device_name.strip():
360
360
  raise ValueError("device_name cannot be empty")
361
361
 
362
- # Guardrail: bare Drum Rack produces silence (no samples loaded)
362
+ # Guardrail: bare Drum Rack produces silence unless building programmatically (12.3+)
363
363
  if device_name.strip().lower() == "drum rack":
364
364
  raise ValueError(
365
365
  "Loading a bare 'Drum Rack' creates an empty rack that produces silence. "
366
- "Instead, use search_browser(path='drums') to find a kit preset "
367
- "(e.g., '808 Core Kit'), then load it with load_browser_item(). "
368
- "Or use DS drum synths (DS Kick, DS Snare, DS HH, DS Tom, DS Clap, "
369
- "DS Cymbal) which are self-contained."
366
+ "Options: (1) search_browser(path='drums') to find a kit preset "
367
+ "(e.g., '808 Core Kit'), then load with load_browser_item(). "
368
+ "(2) On Live 12.3+: use insert_device('Drum Rack') + insert_rack_chain "
369
+ "+ set_drum_chain_note to build a kit programmatically. "
370
+ "(3) DS drum synths (DS Kick, DS Snare, DS HH) are self-contained."
370
371
  )
371
372
 
372
373
  result = _get_ableton(ctx).send_command("find_and_load_device", {
@@ -376,6 +377,116 @@ def find_and_load_device(ctx: Context, track_index: int, device_name: str) -> di
376
377
  return _postflight_loaded_device(ctx, result)
377
378
 
378
379
 
380
+ @mcp.tool()
381
+ def insert_device(
382
+ ctx: Context,
383
+ track_index: int,
384
+ device_name: str,
385
+ position: int = -1,
386
+ device_index: Optional[int] = None,
387
+ chain_index: Optional[int] = None,
388
+ ) -> dict:
389
+ """Insert a native Live device by name — 10x faster than browser search (Live 12.3+).
390
+
391
+ Only works for native devices (Reverb, Compressor, EQ Eight, Drift, etc.).
392
+ For plugins, M4L devices, or presets, use find_and_load_device or load_browser_item.
393
+
394
+ track_index: 0+ for regular tracks, -1/-2 for returns, -1000 for master
395
+ device_name: exact device name (e.g. 'Reverb', 'Auto Filter', 'Wavetable')
396
+ position: device chain position (0 = first, -1 = end of chain)
397
+ device_index: required when inserting into a rack chain (identifies the rack)
398
+ chain_index: insert into this chain of a rack device (for building drum kits)
399
+
400
+ Drum Rack construction workflow (12.3+):
401
+ 1. insert_device(track_index, 'Drum Rack') — create empty rack
402
+ 2. insert_rack_chain(track_index, device_index) — add chains
403
+ 3. set_drum_chain_note(chain_index, note=36) — assign C1 (kick)
404
+ 4. insert_device(track_index, 'Simpler', — add instrument
405
+ device_index=rack_idx, chain_index=0) into chain
406
+
407
+ On Live < 12.3: returns an error suggesting find_and_load_device instead.
408
+ """
409
+ _validate_track_index(track_index)
410
+ if not device_name.strip():
411
+ raise ValueError("device_name cannot be empty")
412
+
413
+ params = {
414
+ "track_index": track_index,
415
+ "device_name": device_name,
416
+ "position": position,
417
+ }
418
+ if device_index is not None:
419
+ params["device_index"] = device_index
420
+ if chain_index is not None:
421
+ if device_index is None:
422
+ raise ValueError("device_index is required when chain_index is provided")
423
+ _validate_device_index(device_index)
424
+ _validate_chain_index(chain_index)
425
+ params["chain_index"] = chain_index
426
+
427
+ result = _get_ableton(ctx).send_command("insert_device", params)
428
+ return _postflight_loaded_device(ctx, result)
429
+
430
+
431
+ @mcp.tool()
432
+ def insert_rack_chain(
433
+ ctx: Context,
434
+ track_index: int,
435
+ device_index: int,
436
+ position: int = -1,
437
+ ) -> dict:
438
+ """Insert a new chain into a Rack device — Instrument Rack, Audio Effect Rack, or Drum Rack (Live 12.3+).
439
+
440
+ Use with insert_device + set_drum_chain_note to build Drum Racks from scratch:
441
+ 1. insert_device(track, 'Drum Rack') to create the rack
442
+ 2. insert_rack_chain(track, rack_device_index) to add chains
443
+ 3. set_drum_chain_note(chain_index=0, note=36) to assign C1 (kick)
444
+ 4. insert_device(track, 'Simpler', device_index=rack, chain_index=0) into chain
445
+
446
+ track_index: track containing the rack
447
+ device_index: rack device index on the track
448
+ position: chain position (-1 = append to end)
449
+ """
450
+ _validate_track_index(track_index)
451
+ _validate_device_index(device_index)
452
+
453
+ return _get_ableton(ctx).send_command("insert_rack_chain", {
454
+ "track_index": track_index,
455
+ "device_index": device_index,
456
+ "position": position,
457
+ })
458
+
459
+
460
+ @mcp.tool()
461
+ def set_drum_chain_note(
462
+ ctx: Context,
463
+ track_index: int,
464
+ device_index: int,
465
+ chain_index: int,
466
+ note: int,
467
+ ) -> dict:
468
+ """Set which MIDI note triggers a Drum Rack chain (Live 12.3+).
469
+
470
+ Standard drum mapping:
471
+ C1 (36) = Kick, D1 (38) = Snare, F#1 (42) = Closed HH,
472
+ A#1 (46) = Open HH, C#2 (49) = Crash, D#2 (51) = Ride
473
+
474
+ note: MIDI note 0-127, or -1 for 'All Notes'
475
+ """
476
+ _validate_track_index(track_index)
477
+ _validate_device_index(device_index)
478
+ _validate_chain_index(chain_index)
479
+ if note < -1 or note > 127:
480
+ raise ValueError("note must be -1 (All Notes) or 0-127")
481
+
482
+ return _get_ableton(ctx).send_command("set_drum_chain_note", {
483
+ "track_index": track_index,
484
+ "device_index": device_index,
485
+ "chain_index": chain_index,
486
+ "note": note,
487
+ })
488
+
489
+
379
490
  @mcp.tool()
380
491
  def set_simpler_playback_mode(
381
492
  ctx: Context,
@@ -9,6 +9,7 @@ import json
9
9
  from typing import Any, Optional
10
10
 
11
11
  from fastmcp import Context
12
+ from pydantic import BaseModel, Field
12
13
 
13
14
  from ..server import mcp
14
15
 
@@ -25,9 +26,36 @@ def _ensure_list(value: Any) -> list:
25
26
  return json.loads(value)
26
27
  except json.JSONDecodeError as exc:
27
28
  raise ValueError(f"Invalid JSON in parameter: {exc}") from exc
29
+ if isinstance(value, list):
30
+ normalized = []
31
+ for item in value:
32
+ if isinstance(item, BaseModel):
33
+ normalized.append(item.model_dump(exclude_none=True))
34
+ else:
35
+ normalized.append(item)
36
+ return normalized
28
37
  return value
29
38
 
30
39
 
40
+ class NoteSpec(BaseModel):
41
+ pitch: int = Field(ge=0, le=127)
42
+ start_time: float
43
+ duration: float = Field(gt=0)
44
+ velocity: Optional[float] = Field(default=None, ge=0.0, le=127.0)
45
+ probability: Optional[float] = Field(default=None, ge=0.0, le=1.0)
46
+ velocity_deviation: Optional[float] = Field(default=None, ge=-127.0, le=127.0)
47
+ release_velocity: Optional[float] = Field(default=None, ge=0.0, le=127.0)
48
+
49
+
50
+ class NoteModification(BaseModel):
51
+ note_id: int
52
+ pitch: Optional[int] = Field(default=None, ge=0, le=127)
53
+ start_time: Optional[float] = None
54
+ duration: Optional[float] = Field(default=None, gt=0)
55
+ velocity: Optional[float] = Field(default=None, ge=0.0, le=127.0)
56
+ probability: Optional[float] = Field(default=None, ge=0.0, le=1.0)
57
+
58
+
31
59
  def _validate_track_index(track_index: int):
32
60
  """Validate track index. Must be >= 0 for regular tracks."""
33
61
  if track_index < 0:
@@ -78,7 +106,7 @@ def _validate_note(note: dict):
78
106
 
79
107
 
80
108
  @mcp.tool()
81
- def add_notes(ctx: Context, track_index: int, clip_index: int, notes: Any) -> dict:
109
+ def add_notes(ctx: Context, track_index: int, clip_index: int, notes: list[NoteSpec] | str) -> dict:
82
110
  """Add MIDI notes to a clip. notes is a JSON array: [{pitch, start_time, duration, velocity?, probability?, velocity_deviation?, release_velocity?}]."""
83
111
  _validate_track_index(track_index)
84
112
  _validate_clip_index(clip_index)
@@ -157,7 +185,7 @@ def remove_notes(
157
185
 
158
186
 
159
187
  @mcp.tool()
160
- def remove_notes_by_id(ctx: Context, track_index: int, clip_index: int, note_ids: Any) -> dict:
188
+ def remove_notes_by_id(ctx: Context, track_index: int, clip_index: int, note_ids: list[int] | str) -> dict:
161
189
  """Remove specific MIDI notes by their IDs (JSON array of ints). Use undo to revert."""
162
190
  _validate_track_index(track_index)
163
191
  _validate_clip_index(clip_index)
@@ -172,7 +200,12 @@ def remove_notes_by_id(ctx: Context, track_index: int, clip_index: int, note_ids
172
200
 
173
201
 
174
202
  @mcp.tool()
175
- def modify_notes(ctx: Context, track_index: int, clip_index: int, modifications: Any) -> dict:
203
+ def modify_notes(
204
+ ctx: Context,
205
+ track_index: int,
206
+ clip_index: int,
207
+ modifications: list[NoteModification] | str,
208
+ ) -> dict:
176
209
  """Modify existing MIDI notes by ID. modifications is a JSON array: [{note_id, pitch?, start_time?, duration?, velocity?, probability?}]."""
177
210
  _validate_track_index(track_index)
178
211
  _validate_clip_index(clip_index)
@@ -202,7 +235,7 @@ def duplicate_notes(
202
235
  ctx: Context,
203
236
  track_index: int,
204
237
  clip_index: int,
205
- note_ids: Any,
238
+ note_ids: list[int] | str,
206
239
  time_offset: float = 0.0,
207
240
  ) -> dict:
208
241
  """Duplicate specific notes by ID (JSON array of ints), with optional time offset (in beats)."""
@@ -23,6 +23,11 @@ _DOMAIN_MAP: dict[str, list[str]] = {
23
23
  "too_safe_to_progress": ["sound_design", "transition"],
24
24
  "section_missing": ["arrangement", "transition"],
25
25
  "transition_not_earned": ["transition", "arrangement"],
26
+ # Sample-domain patterns (session-state analysis, not action-history)
27
+ "no_organic_texture": ["sample", "sound_design"],
28
+ "stale_drums": ["sample", "arrangement"],
29
+ "vocal_processing_monotony": ["sample", "sound_design"],
30
+ "dense_but_static": ["sample", "mix"],
26
31
  }
27
32
 
28
33
  _STUCKNESS_THRESHOLD = 0.2 # Below this, treat as user_request
@@ -390,8 +390,86 @@ def _all_same_family(variants: list[dict]) -> bool:
390
390
  return len(families) <= 1 and len(variants) > 1
391
391
 
392
392
 
393
- # ── Pipeline orchestrator ────────────────────────────────────────
393
+ # ── Corpus intelligence enrichment ──────────────────────────────
394
+
395
+
396
+ def _get_corpus_hints(request_text: str, diagnosis: dict | None) -> dict | None:
397
+ """Query the corpus for creative hints relevant to the request.
398
+
399
+ Returns a dict with emotional_recipe, genre_chain, automation_density,
400
+ and technique_suggestions — or None if corpus is unavailable.
401
+ """
402
+ try:
403
+ from ..corpus import get_corpus
404
+ except ImportError:
405
+ return None
406
+
407
+ corpus = get_corpus()
408
+ if not corpus.emotional_recipes and not corpus.genre_chains:
409
+ return None
410
+
411
+ hints: dict = {}
412
+ request_lower = request_text.lower()
413
+
414
+ # Check for emotional keywords
415
+ _EMOTION_KEYWORDS = {
416
+ "warm": "warmth & comfort", "cold": "tension & anxiety",
417
+ "dark": "melancholy", "bright": "euphoria",
418
+ "aggressive": "danger", "soft": "warmth & comfort",
419
+ "anxious": "tension & anxiety", "nostalgic": "nostalgia",
420
+ "vast": "vastness", "ethereal": "vastness",
421
+ "sad": "melancholy", "happy": "euphoria",
422
+ "tension": "tension & anxiety", "release": "euphoria",
423
+ }
424
+ for keyword, emotion_key in _EMOTION_KEYWORDS.items():
425
+ if keyword in request_lower:
426
+ recipe = corpus.suggest_for_emotion(emotion_key)
427
+ if recipe:
428
+ hints["emotional_recipe"] = {
429
+ "emotion": recipe.emotion,
430
+ "technique_count": len(recipe.techniques),
431
+ "first_techniques": [t[:100] for t in recipe.techniques[:3]],
432
+ }
433
+ break
434
+
435
+ # Check for genre keywords
436
+ _GENRE_KEYWORDS = ["dub", "techno", "minimal", "ambient", "idm", "trap",
437
+ "sophie", "arca", "house", "trance", "drum and bass"]
438
+ for genre in _GENRE_KEYWORDS:
439
+ if genre in request_lower:
440
+ chain = corpus.get_genre_chain(genre)
441
+ if chain:
442
+ hints["genre_chain"] = {
443
+ "genre": chain.genre,
444
+ "devices": chain.devices[:5],
445
+ "description": chain.description[:120],
446
+ }
447
+ break
448
+
449
+ # Check for physical model keywords
450
+ _MATERIAL_KEYWORDS = ["water", "metal", "glass", "breath", "fire", "electric"]
451
+ for material in _MATERIAL_KEYWORDS:
452
+ if material in request_lower:
453
+ model = corpus.suggest_for_material(material)
454
+ if model:
455
+ hints["physical_model"] = {
456
+ "material": model.material,
457
+ "devices": model.devices[:4],
458
+ }
459
+ break
394
460
 
461
+ # Automation density from diagnosis section type
462
+ if diagnosis:
463
+ problem_class = diagnosis.get("problem_class", "")
464
+ if "static" in problem_class or "flat" in problem_class:
465
+ hints["automation_density"] = corpus.get_automation_density_for_section("peak")
466
+ elif "breakdown" in problem_class:
467
+ hints["automation_density"] = corpus.get_automation_density_for_section("breakdown")
468
+
469
+ return hints if hints else None
470
+
471
+
472
+ # ── Pipeline orchestrator ────────────────────────────────────────
395
473
 
396
474
 
397
475
  def generate_wonder_variants(
@@ -414,6 +492,9 @@ def generate_wonder_variants(
414
492
  labels = ["safe", "strong", "unexpected"]
415
493
  variants = []
416
494
 
495
+ # Load corpus intelligence for variant enrichment
496
+ corpus_hints = _get_corpus_hints(request_text, diagnosis)
497
+
417
498
  # Build executable variants from distinct moves
418
499
  for i, move in enumerate(distinct):
419
500
  label = labels[i]
@@ -429,6 +510,9 @@ def generate_wonder_variants(
429
510
  # Score taste on envelope-adjusted move for consistency with targets_snapshot
430
511
  v["taste_fit"] = compute_taste_fit(move_with_envelope, taste_graph)
431
512
  v["distinctness_reason"] = _explain_distinctness(move, distinct, i)
513
+ # Enrich with corpus knowledge
514
+ if corpus_hints:
515
+ v["corpus_hints"] = corpus_hints
432
516
  variants.append(v)
433
517
 
434
518
  executable_count = len(variants)
@@ -29,9 +29,14 @@ def _get_taste_graph(ctx: Context):
29
29
  from ..memory.taste_graph import build_taste_graph
30
30
  from ..memory.taste_memory import TasteMemoryStore
31
31
  from ..memory.anti_memory import AntiMemoryStore
32
+ from ..persistence.taste_store import PersistentTasteStore
32
33
  taste_store = ctx.lifespan_context.setdefault("taste_memory", TasteMemoryStore())
33
34
  anti_store = ctx.lifespan_context.setdefault("anti_memory", AntiMemoryStore())
34
- return build_taste_graph(taste_store=taste_store, anti_store=anti_store)
35
+ persistent = ctx.lifespan_context.setdefault("persistent_taste", PersistentTasteStore())
36
+ return build_taste_graph(
37
+ taste_store=taste_store, anti_store=anti_store,
38
+ persistent_store=persistent,
39
+ )
35
40
  except Exception:
36
41
  pass
37
42
  return None
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.23",
3
+ "version": "1.10.0",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 293 tools, 39 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 — 316 tools, 43 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",
@@ -17,6 +17,16 @@
17
17
  "bugs": {
18
18
  "url": "https://github.com/dreamrec/LivePilot/issues"
19
19
  },
20
+ "funding": [
21
+ {
22
+ "type": "patreon",
23
+ "url": "https://www.patreon.com/c/dreamrec"
24
+ },
25
+ {
26
+ "type": "github",
27
+ "url": "https://github.com/sponsors/dreamrec"
28
+ }
29
+ ],
20
30
  "keywords": [
21
31
  "mcp",
22
32
  "mcp-server",
@@ -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.9.23"
8
+ __version__ = "1.10.0"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from .server import LivePilotServer
@@ -20,6 +20,7 @@ from . import browser # noqa: F401 — registers browser handlers
20
20
  from . import arrangement # noqa: F401 — registers arrangement handlers
21
21
  from . import diagnostics # noqa: F401 — registers diagnostics handler
22
22
  from . import clip_automation # noqa: F401 — registers clip automation handlers
23
+ from . import version_detect # noqa: F401 — version detection
23
24
 
24
25
 
25
26
  def create_instance(c_instance):
@@ -36,6 +37,12 @@ class LivePilot(ControlSurface):
36
37
  self._server.start()
37
38
  self.log_message("LivePilot v%s starting..." % __version__)
38
39
  self.show_message("LivePilot v%s starting..." % __version__)
40
+ v = version_detect.version_string()
41
+ self.log_message("LivePilot detected Ableton Live %s" % v)
42
+ features = version_detect.get_api_features()
43
+ enabled = [k for k, flag in features.items() if flag]
44
+ if enabled:
45
+ self.log_message(" Enabled features: %s" % ", ".join(enabled))
39
46
 
40
47
  def disconnect(self):
41
48
  """Called by Ableton when the script is unloaded."""
@@ -169,6 +169,70 @@ def create_arrangement_clip(song, params):
169
169
  }
170
170
 
171
171
 
172
+ @register("create_native_arrangement_clip")
173
+ def create_native_arrangement_clip(song, params):
174
+ """Create an empty MIDI clip in arrangement using the native 12.1.10+ API.
175
+
176
+ Unlike create_arrangement_clip (which duplicates a session clip),
177
+ this creates a true native clip with full automation envelope support.
178
+
179
+ Required: track_index, start_time, length
180
+ Optional: name, color_index
181
+ """
182
+ from .version_detect import has_feature
183
+
184
+ if not has_feature("create_midi_clip_arrangement"):
185
+ raise RuntimeError(
186
+ "create_native_arrangement_clip requires Live 12.1.10+. "
187
+ "Use create_arrangement_clip (session clip duplication) instead."
188
+ )
189
+
190
+ track_index = int(params["track_index"])
191
+ start_time = float(params["start_time"])
192
+ length = float(params["length"])
193
+ if length <= 0:
194
+ raise ValueError("length must be > 0")
195
+ if start_time < 0:
196
+ raise ValueError("start_time must be >= 0")
197
+
198
+ track = get_track(song, track_index)
199
+ if not track.has_midi_input:
200
+ raise ValueError(
201
+ "Track %d is not a MIDI track — create_native_arrangement_clip "
202
+ "only works on MIDI tracks" % track_index
203
+ )
204
+
205
+ song.begin_undo_step()
206
+ try:
207
+ clip = track.create_midi_clip(start_time, length)
208
+
209
+ name = params.get("name")
210
+ if name:
211
+ clip.name = str(name)
212
+ color_index = params.get("color_index")
213
+ if color_index is not None:
214
+ clip.color_index = int(color_index)
215
+ finally:
216
+ song.end_undo_step()
217
+
218
+ # Find the clip index in arrangement_clips
219
+ clip_index = None
220
+ for i, c in enumerate(track.arrangement_clips):
221
+ if abs(c.start_time - start_time) < 0.01:
222
+ clip_index = i
223
+ break
224
+
225
+ return {
226
+ "track_index": track_index,
227
+ "clip_index": clip_index,
228
+ "start_time": start_time,
229
+ "length": length,
230
+ "name": clip.name,
231
+ "has_envelope_support": True,
232
+ "native": True,
233
+ }
234
+
235
+
172
236
  @register("add_arrangement_notes")
173
237
  def add_arrangement_notes(song, params):
174
238
  """Add MIDI notes to an arrangement clip (by index in arrangement_clips)."""
@@ -713,3 +777,53 @@ def back_to_arranger(song, params):
713
777
  """Switch playback from session clips back to the arrangement timeline."""
714
778
  song.back_to_arranger = True
715
779
  return {"back_to_arranger": True}
780
+
781
+
782
+ @register("force_arrangement")
783
+ def force_arrangement(song, params):
784
+ """Force ALL tracks to follow the arrangement timeline.
785
+
786
+ Stops all session clips, releases every track from session override,
787
+ sets back_to_arranger, and optionally jumps to a start position.
788
+
789
+ This is the atomic "play the arrangement from the top" command.
790
+ """
791
+ # 1. Stop playback
792
+ was_playing = song.is_playing
793
+ if was_playing:
794
+ song.stop_playing()
795
+
796
+ # 2. Stop playing clip slots individually to release session overrides
797
+ # (track.stop_all_clips() throws STATE_ERROR when tracks have no clips)
798
+ for track in list(song.tracks) + list(song.return_tracks):
799
+ try:
800
+ for slot in track.clip_slots:
801
+ if slot.has_clip and slot.is_playing:
802
+ slot.clip.stop()
803
+ except Exception:
804
+ pass
805
+
806
+ # 3. Global back-to-arranger
807
+ song.back_to_arranger = True
808
+
809
+ # 4. Jump to position (default: start)
810
+ beat_time = float(params.get("beat_time", 0))
811
+ song.current_song_time = max(0, beat_time)
812
+
813
+ # 5. Set loop if requested
814
+ if "loop_length" in params:
815
+ song.loop_start = float(params.get("loop_start", 0))
816
+ song.loop_length = float(params["loop_length"])
817
+ song.loop = True
818
+
819
+ # 6. Start playback if requested (default: yes)
820
+ play = params.get("play", True)
821
+ if play:
822
+ song.start_playing()
823
+
824
+ return {
825
+ "arrangement_active": True,
826
+ "position": song.current_song_time,
827
+ "is_playing": song.is_playing,
828
+ "loop": song.loop,
829
+ }