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
@@ -0,0 +1,66 @@
1
+ """Capability and degradation reporting for advanced tools.
2
+
3
+ Every advanced tool reports its operational state so callers know
4
+ what data was available, what was missing, and how much to trust
5
+ the result.
6
+
7
+ Levels:
8
+ full — all required data sources available
9
+ fallback — some data missing, result is degraded but useful
10
+ analytical_only — no live data, pure heuristic
11
+ unavailable — cannot operate at all
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+
18
+
19
+ @dataclass
20
+ class CapabilityReport:
21
+ """Operational state of an advanced tool invocation."""
22
+
23
+ level: str = "full" # full, fallback, analytical_only, unavailable
24
+ confidence: float = 1.0
25
+ available_sources: list[str] = field(default_factory=list)
26
+ missing_sources: list[str] = field(default_factory=list)
27
+ fallback_used: str = ""
28
+ reason: str = ""
29
+
30
+ def to_dict(self) -> dict:
31
+ d = {"capability": self.level, "confidence": round(self.confidence, 2)}
32
+ if self.missing_sources:
33
+ d["missing"] = self.missing_sources
34
+ if self.fallback_used:
35
+ d["fallback"] = self.fallback_used
36
+ if self.reason:
37
+ d["reason"] = self.reason
38
+ return d
39
+
40
+
41
+ def build_capability(
42
+ required: list[str],
43
+ available: dict[str, bool],
44
+ ) -> CapabilityReport:
45
+ """Build a capability report from required vs available data sources."""
46
+ missing = [r for r in required if not available.get(r, False)]
47
+ present = [r for r in required if available.get(r, False)]
48
+
49
+ if not missing:
50
+ return CapabilityReport(
51
+ level="full", confidence=1.0, available_sources=present,
52
+ )
53
+
54
+ if len(missing) == len(required):
55
+ return CapabilityReport(
56
+ level="analytical_only", confidence=0.2,
57
+ available_sources=[], missing_sources=missing,
58
+ reason="No required data sources available",
59
+ )
60
+
61
+ ratio = len(present) / len(required)
62
+ return CapabilityReport(
63
+ level="fallback", confidence=round(ratio * 0.8, 2),
64
+ available_sources=present, missing_sources=missing,
65
+ fallback_used="degraded inference from partial data",
66
+ )
@@ -0,0 +1,137 @@
1
+ """Runtime capability probe — detects what's available at startup.
2
+
3
+ Reports capability tiers: Core Control, Analyzer-Enhanced,
4
+ Offline Analysis, Creative Intelligence, Persistent Memory.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ def probe_capabilities(
15
+ ableton: Any = None,
16
+ ctx: Any = None,
17
+ ) -> dict:
18
+ """Probe runtime capabilities and return a structured report.
19
+
20
+ Can be called at startup or on demand via --doctor.
21
+ """
22
+ report: dict[str, dict] = {}
23
+
24
+ # 1. Ableton reachability
25
+ ableton_ok = False
26
+ if ableton is not None:
27
+ try:
28
+ info = ableton.send_command("ping")
29
+ ableton_ok = info is not None
30
+ except Exception:
31
+ pass
32
+ report["ableton"] = {
33
+ "status": "ok" if ableton_ok else "unavailable",
34
+ "detail": "TCP 9878 connection active" if ableton_ok else "Not connected",
35
+ }
36
+
37
+ # 1b. Live version capabilities
38
+ live_version_str = "12.0.0"
39
+ if ableton_ok:
40
+ try:
41
+ info = ableton.send_command("get_session_info")
42
+ live_version_str = info.get("live_version", "12.0.0")
43
+ except Exception:
44
+ pass
45
+ from .live_version import LiveVersionCapabilities
46
+ version_caps = LiveVersionCapabilities.from_version_string(live_version_str)
47
+ report["live_version"] = {
48
+ "status": "ok",
49
+ "version": live_version_str,
50
+ "capability_tier": version_caps.capability_tier,
51
+ "features": version_caps.to_dict(),
52
+ }
53
+
54
+ # 2. Remote Script parity
55
+ from .remote_commands import REMOTE_COMMANDS
56
+ report["remote_script"] = {
57
+ "status": "ok",
58
+ "command_count": len(REMOTE_COMMANDS),
59
+ "detail": f"{len(REMOTE_COMMANDS)} registered commands",
60
+ }
61
+
62
+ # 3. M4L bridge
63
+ bridge_ok = False
64
+ if ctx is not None:
65
+ lifespan_context = getattr(ctx, "lifespan_context", {}) if hasattr(ctx, "lifespan_context") else {}
66
+ bridge = lifespan_context.get("m4l")
67
+ spectral = lifespan_context.get("spectral")
68
+ bridge_ok = bridge is not None and spectral is not None and getattr(spectral, "is_connected", False)
69
+ report["m4l_bridge"] = {
70
+ "status": "ok" if bridge_ok else "unavailable",
71
+ "detail": "UDP 9880 / OSC 9881 active" if bridge_ok else "Not connected — 30 analyzer tools unavailable",
72
+ }
73
+
74
+ # 4. Offline perception
75
+ numpy_ok = False
76
+ try:
77
+ import numpy # noqa: F401
78
+ numpy_ok = True
79
+ except ImportError:
80
+ pass
81
+ report["offline_perception"] = {
82
+ "status": "ok" if numpy_ok else "degraded",
83
+ "detail": "numpy available" if numpy_ok else "numpy not installed — offline analysis unavailable",
84
+ }
85
+
86
+ # 5. Persistence
87
+ livepilot_dir = Path.home() / ".livepilot"
88
+ persistence_ok = livepilot_dir.exists() and os.access(livepilot_dir, os.W_OK)
89
+ taste_exists = (livepilot_dir / "taste.json").exists()
90
+ techniques_exists = (livepilot_dir / "memory" / "techniques.json").exists()
91
+ report["persistence"] = {
92
+ "status": "ok" if persistence_ok else "unavailable",
93
+ "detail": f"~/.livepilot/ {'writable' if persistence_ok else 'not found'}",
94
+ "taste_store": taste_exists,
95
+ "technique_store": techniques_exists,
96
+ }
97
+
98
+ # 6. Capability tier — highest active tier
99
+ if ableton_ok and bridge_ok:
100
+ tier = "analyzer_enhanced"
101
+ elif ableton_ok:
102
+ tier = "core_control"
103
+ else:
104
+ tier = "creative_intelligence" # heuristic-only, no Ableton connection
105
+
106
+ report["tier"] = {
107
+ "active": tier,
108
+ "levels": {
109
+ "core_control": ableton_ok,
110
+ "analyzer_enhanced": ableton_ok and bridge_ok,
111
+ "offline_analysis": numpy_ok,
112
+ "creative_intelligence": True, # always available
113
+ "persistent_memory": persistence_ok,
114
+ },
115
+ }
116
+
117
+ return report
118
+
119
+
120
+ def format_doctor_report(report: dict) -> str:
121
+ """Format capability report for --doctor output."""
122
+ lines = ["LivePilot Capability Report", "=" * 40]
123
+
124
+ icons = {"ok": " PASS", "unavailable": " FAIL", "degraded": " WARN"}
125
+
126
+ for area in ["ableton", "remote_script", "m4l_bridge", "offline_perception", "persistence"]:
127
+ info = report.get(area, {})
128
+ status = info.get("status", "unknown")
129
+ icon = icons.get(status, " ????")
130
+ detail = info.get("detail", "")
131
+ lines.append(f"{icon} {area}: {detail}")
132
+
133
+ tier = report.get("tier", {}).get("active", "unknown")
134
+ lines.append("")
135
+ lines.append(f"Active tier: {tier}")
136
+
137
+ return "\n".join(lines)
@@ -0,0 +1,143 @@
1
+ """Unified execution router for compiled plan steps.
2
+
3
+ Classifies each step by backend (remote_command, mcp_tool, bridge_command)
4
+ and dispatches to the correct execution path. Replaces the pattern of
5
+ sending everything through ableton.send_command() blindly.
6
+
7
+ Step backends:
8
+ remote_command — valid Remote Script handler, goes through TCP
9
+ bridge_command — M4L bridge handler, goes through TCP (requires bridge)
10
+ mcp_tool — MCP-layer Python function, called directly
11
+ unknown — not a valid target anywhere
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from typing import Any, Optional
18
+
19
+ from .remote_commands import BRIDGE_COMMANDS, REMOTE_COMMANDS
20
+
21
+
22
+ # MCP-only tools that exist as Python functions but NOT as TCP handlers.
23
+ # These must be called through direct import, not ableton.send_command().
24
+ MCP_TOOLS: frozenset[str] = frozenset({
25
+ "apply_automation_shape",
26
+ "apply_gesture_template",
27
+ "analyze_mix",
28
+ "get_master_spectrum",
29
+ "get_emotional_arc",
30
+ "capture_audio",
31
+ "get_motif_graph",
32
+ # Device Forge tools (MCP-only, no TCP handler)
33
+ "generate_m4l_effect",
34
+ "install_m4l_device",
35
+ "list_genexpr_templates",
36
+ })
37
+
38
+
39
+ @dataclass
40
+ class ExecutionResult:
41
+ """Result of executing a single plan step."""
42
+
43
+ ok: bool = False
44
+ backend: str = ""
45
+ tool: str = ""
46
+ result: Any = None
47
+ error: str = ""
48
+
49
+ def to_dict(self) -> dict:
50
+ d = {"ok": self.ok, "backend": self.backend, "tool": self.tool}
51
+ if self.ok:
52
+ d["result"] = self.result
53
+ else:
54
+ d["error"] = self.error
55
+ return d
56
+
57
+
58
+ def classify_step(tool: str) -> str:
59
+ """Classify a step's execution backend."""
60
+ if tool in REMOTE_COMMANDS:
61
+ return "remote_command"
62
+ if tool in BRIDGE_COMMANDS:
63
+ return "bridge_command"
64
+ if tool in MCP_TOOLS:
65
+ return "mcp_tool"
66
+ return "unknown"
67
+
68
+
69
+ def execute_step(
70
+ tool: str,
71
+ params: dict,
72
+ ableton: Any = None,
73
+ ctx: Any = None,
74
+ declared_backend: str | None = None,
75
+ ) -> ExecutionResult:
76
+ """Execute a single plan step through the correct backend."""
77
+ backend = declared_backend if declared_backend in ("remote_command", "bridge_command", "mcp_tool") else classify_step(tool)
78
+
79
+ if backend in ("remote_command", "bridge_command"):
80
+ if ableton is None:
81
+ return ExecutionResult(
82
+ ok=False, backend=backend, tool=tool,
83
+ error="Ableton connection unavailable",
84
+ )
85
+ try:
86
+ result = ableton.send_command(tool, params)
87
+ return ExecutionResult(ok=True, backend=backend, tool=tool, result=result)
88
+ except Exception as e:
89
+ return ExecutionResult(ok=False, backend=backend, tool=tool, error=str(e))
90
+
91
+ elif backend == "mcp_tool":
92
+ # MCP tools require direct Python dispatch.
93
+ # For now, return a clear error — full MCP dispatch is wired per-tool
94
+ # in the callers (apply_semantic_move, render_preview_variant).
95
+ return ExecutionResult(
96
+ ok=False, backend=backend, tool=tool,
97
+ error=f"MCP tool '{tool}' requires direct Python dispatch — "
98
+ f"not executable through TCP. Use the MCP layer directly.",
99
+ )
100
+
101
+ else:
102
+ return ExecutionResult(
103
+ ok=False, backend="unknown", tool=tool,
104
+ error=f"Unknown tool '{tool}' — not a Remote Script command, "
105
+ f"bridge command, or registered MCP tool",
106
+ )
107
+
108
+
109
+ def execute_plan_steps(
110
+ steps: list[dict],
111
+ ableton: Any = None,
112
+ ctx: Any = None,
113
+ stop_on_failure: bool = True,
114
+ ) -> list[ExecutionResult]:
115
+ """Execute a list of plan steps, returning results for each.
116
+
117
+ Stops on first failure by default. Set stop_on_failure=False
118
+ to continue past errors (useful for best-effort execution).
119
+ """
120
+ results: list[ExecutionResult] = []
121
+
122
+ for step in steps:
123
+ tool = step.get("tool") or step.get("command", "")
124
+ params = step.get("params") or step.get("args", {})
125
+ # Honor declared backend from step annotations (PR5) if present
126
+ declared_backend = step.get("backend")
127
+
128
+ if not tool:
129
+ results.append(ExecutionResult(
130
+ ok=False, backend="unknown", tool="",
131
+ error="Step has no tool/command field",
132
+ ))
133
+ if stop_on_failure:
134
+ break
135
+ continue
136
+
137
+ result = execute_step(tool, params, ableton=ableton, ctx=ctx, declared_backend=declared_backend)
138
+ results.append(result)
139
+
140
+ if not result.ok and stop_on_failure:
141
+ break
142
+
143
+ return results
@@ -0,0 +1,102 @@
1
+ """MCP-side Live version capabilities model.
2
+
3
+ Pure data model — no I/O. Parses version info from get_session_info
4
+ responses and exposes feature flags for tool routing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class LiveVersionCapabilities:
14
+ """Feature availability based on detected Live version."""
15
+
16
+ major: int = 12
17
+ minor: int = 0
18
+ patch: int = 0
19
+
20
+ @classmethod
21
+ def from_version_string(cls, version_str: str) -> LiveVersionCapabilities:
22
+ """Parse '12.3.6' into a capabilities instance."""
23
+ parts = version_str.split(".")
24
+ major = int(parts[0]) if len(parts) > 0 else 12
25
+ minor = int(parts[1]) if len(parts) > 1 else 0
26
+ patch = int(parts[2]) if len(parts) > 2 else 0
27
+ return cls(major=major, minor=minor, patch=patch)
28
+
29
+ @classmethod
30
+ def from_session_info(cls, session_info: dict) -> LiveVersionCapabilities:
31
+ """Extract version from get_session_info response.
32
+
33
+ Looks for 'live_version' field. Falls back to 12.0.0 if absent
34
+ (pre-upgrade Remote Script).
35
+ """
36
+ version_str = session_info.get("live_version", "12.0.0")
37
+ return cls.from_version_string(version_str)
38
+
39
+ @property
40
+ def _version_tuple(self) -> tuple[int, int, int]:
41
+ return (self.major, self.minor, self.patch)
42
+
43
+ # ── Feature flags ──────────────────────────────────────────────
44
+
45
+ @property
46
+ def has_native_arrangement_clips(self) -> bool:
47
+ """Track.create_midi_clip(start, length) — 12.1.10+"""
48
+ return self._version_tuple >= (12, 1, 10)
49
+
50
+ @property
51
+ def has_display_value(self) -> bool:
52
+ """DeviceParameter.display_value — 12.2+"""
53
+ return self._version_tuple >= (12, 2, 0)
54
+
55
+ @property
56
+ def has_insert_device(self) -> bool:
57
+ """Track.insert_device(name, index?) — 12.3+"""
58
+ return self._version_tuple >= (12, 3, 0)
59
+
60
+ @property
61
+ def has_drum_rack_construction(self) -> bool:
62
+ """insert_chain + DrumChain.in_note — 12.3+"""
63
+ return self._version_tuple >= (12, 3, 0)
64
+
65
+ @property
66
+ def has_take_lanes(self) -> bool:
67
+ """Take Lanes API — 12.2+"""
68
+ return self._version_tuple >= (12, 2, 0)
69
+
70
+ @property
71
+ def has_stem_separation(self) -> bool:
72
+ """Stem separation via MFL — 12.3+"""
73
+ return self._version_tuple >= (12, 3, 0)
74
+
75
+ @property
76
+ def has_replace_sample_native(self) -> bool:
77
+ """SimplerDevice.replace_sample(path) — 12.4+"""
78
+ return self._version_tuple >= (12, 4, 0)
79
+
80
+ @property
81
+ def capability_tier(self) -> str:
82
+ """Human-readable tier: core | enhanced_arrangement | full_intelligence."""
83
+ if self._version_tuple >= (12, 3, 0):
84
+ return "full_intelligence"
85
+ elif self._version_tuple >= (12, 1, 10):
86
+ return "enhanced_arrangement"
87
+ else:
88
+ return "core"
89
+
90
+ def to_dict(self) -> dict:
91
+ """Serialize for API responses and capability probes."""
92
+ return {
93
+ "version": f"{self.major}.{self.minor}.{self.patch}",
94
+ "capability_tier": self.capability_tier,
95
+ "native_arrangement_clips": self.has_native_arrangement_clips,
96
+ "display_value": self.has_display_value,
97
+ "insert_device": self.has_insert_device,
98
+ "drum_rack_construction": self.has_drum_rack_construction,
99
+ "take_lanes": self.has_take_lanes,
100
+ "stem_separation": self.has_stem_separation,
101
+ "replace_sample_native": self.has_replace_sample_native,
102
+ }
@@ -0,0 +1,87 @@
1
+ """Canonical set of all valid Remote Script commands.
2
+
3
+ Every command here has a @register handler in remote_script/LivePilot/.
4
+ This is the source of truth for what can be called via
5
+ ableton.send_command(). If a command is not in this set, sending it
6
+ through TCP will return NOT_FOUND from the Remote Script.
7
+
8
+ Maintained manually — the Remote Script uses Ableton's Python and
9
+ cannot be imported in CI. Update this when adding new handlers.
10
+ """
11
+
12
+ REMOTE_COMMANDS: frozenset[str] = frozenset({
13
+ # transport (10)
14
+ "get_session_info", "set_tempo", "set_time_signature",
15
+ "start_playback", "stop_playback", "continue_playback",
16
+ "toggle_metronome", "set_session_loop", "undo", "redo",
17
+ # tracks (17)
18
+ "get_track_info", "create_midi_track", "create_audio_track",
19
+ "create_return_track", "delete_track", "duplicate_track",
20
+ "set_track_name", "set_track_color", "set_track_mute",
21
+ "set_track_solo", "set_track_arm", "stop_track_clips",
22
+ "set_group_fold", "set_track_input_monitoring",
23
+ "get_freeze_status", "freeze_track", "flatten_track",
24
+ # clips (11)
25
+ "get_clip_info", "create_clip", "delete_clip", "duplicate_clip",
26
+ "fire_clip", "stop_clip", "set_clip_name", "set_clip_color",
27
+ "set_clip_loop", "set_clip_launch", "set_clip_warp_mode",
28
+ # notes (8)
29
+ "add_notes", "get_notes", "remove_notes", "remove_notes_by_id",
30
+ "modify_notes", "duplicate_notes", "transpose_notes", "quantize_clip",
31
+ # mixing (11)
32
+ "set_track_volume", "set_track_pan", "set_track_send",
33
+ "get_return_tracks", "get_master_track", "set_master_volume",
34
+ "get_track_routing", "get_track_meters", "get_master_meters",
35
+ "get_mix_snapshot", "set_track_routing",
36
+ # scenes (12)
37
+ "get_scenes_info", "create_scene", "delete_scene", "duplicate_scene",
38
+ "fire_scene", "set_scene_name", "set_scene_color", "set_scene_tempo",
39
+ "get_scene_matrix", "fire_scene_clips", "stop_all_clips",
40
+ "get_playing_clips",
41
+ # devices (15)
42
+ "get_device_info", "get_device_parameters", "set_device_parameter",
43
+ "batch_set_parameters", "toggle_device", "delete_device",
44
+ "move_device", "load_device_by_uri", "find_and_load_device",
45
+ "set_simpler_playback_mode", "get_rack_chains", "set_chain_volume",
46
+ "insert_device", # 12.3+ native device insertion
47
+ "insert_rack_chain", # 12.3+ rack chain insertion
48
+ "set_drum_chain_note", # 12.3+ drum chain note assignment
49
+ # clip_automation (3)
50
+ "get_clip_automation", "set_clip_automation", "clear_clip_automation",
51
+ # browser (6)
52
+ "get_browser_tree", "get_browser_items", "search_browser",
53
+ "load_browser_item", "get_device_presets",
54
+ "scan_browser_deep", # Atlas deep scan — returns full category tree
55
+ # arrangement (21)
56
+ "get_arrangement_clips", "create_arrangement_clip",
57
+ "create_native_arrangement_clip",
58
+ "add_arrangement_notes", "get_arrangement_notes",
59
+ "remove_arrangement_notes", "remove_arrangement_notes_by_id",
60
+ "modify_arrangement_notes", "duplicate_arrangement_notes",
61
+ "set_arrangement_automation", "transpose_arrangement_notes",
62
+ "set_arrangement_clip_name", "jump_to_time",
63
+ "capture_midi", "start_recording", "stop_recording",
64
+ "get_cue_points", "jump_to_cue", "toggle_cue_point",
65
+ "back_to_arranger", "force_arrangement",
66
+ # diagnostics (1)
67
+ "get_session_diagnostics",
68
+ # ping (built-in)
69
+ "ping",
70
+ })
71
+
72
+ # M4L bridge commands — routed through TCP but handled by livepilot_bridge.js
73
+ # These require the M4L Analyzer device on the master track.
74
+ BRIDGE_COMMANDS: frozenset[str] = frozenset({
75
+ "get_params", "get_hidden_params", "get_auto_state", "walk_rack",
76
+ "get_chains_deep", "get_track_cpu", "get_selected", "get_key",
77
+ "get_clip_file_path", "replace_simpler_sample", "get_simpler_slices",
78
+ "crop_simpler", "reverse_simpler", "warp_simpler",
79
+ "get_warp_markers", "add_warp_marker", "move_warp_marker",
80
+ "remove_warp_marker", "capture_audio", "capture_stop",
81
+ "check_flucoma", "scrub_clip", "stop_scrub", "get_display_values",
82
+ "get_plugin_params", "map_plugin_param", "get_plugin_presets",
83
+ "load_sample_to_simpler",
84
+ })
85
+
86
+ # Combined: all valid send_command targets
87
+ ALL_VALID_COMMANDS: frozenset[str] = REMOTE_COMMANDS | BRIDGE_COMMANDS
@@ -94,14 +94,19 @@ def get_session_kernel(
94
94
 
95
95
  # Core: session info + capability state
96
96
  session_info = ableton.send_command("get_session_info")
97
+ session_ok = isinstance(session_info, dict) and "error" not in session_info
97
98
 
98
99
  analyzer_ok = False
100
+ analyzer_fresh = False
99
101
  if spectral is not None:
100
102
  analyzer_ok = spectral.is_connected
103
+ if analyzer_ok:
104
+ analyzer_fresh = spectral.get("spectrum") is not None
101
105
 
102
106
  state = build_capability_state(
103
- session_ok=True,
107
+ session_ok=session_ok,
104
108
  analyzer_ok=analyzer_ok,
109
+ analyzer_fresh=analyzer_fresh,
105
110
  memory_ok=True,
106
111
  )
107
112
 
@@ -112,10 +117,19 @@ def get_session_kernel(
112
117
  session_mem = []
113
118
 
114
119
  try:
115
- from .action_ledger import ActionLedger
116
- ledger = ActionLedger.instance()
120
+ from .action_ledger import SessionLedger
121
+ ledger = ctx.lifespan_context.get("action_ledger")
122
+ if ledger is None:
123
+ ledger = SessionLedger()
124
+ ctx.lifespan_context["action_ledger"] = ledger
117
125
  if ledger:
118
- ledger_summary = ledger.summary()
126
+ recent = ledger.get_recent_moves(limit=10)
127
+ ledger_summary = {
128
+ "total_moves": len(ledger._entries),
129
+ "memory_candidate_count": len(ledger.get_memory_candidates()),
130
+ "last_move": ledger.get_last_move().to_dict() if ledger.get_last_move() else None,
131
+ "recent_moves": [entry.to_dict() for entry in recent],
132
+ }
119
133
  except Exception:
120
134
  pass
121
135
 
@@ -0,0 +1 @@
1
+ """Sample Engine — intelligence layer for sample discovery, analysis, and manipulation."""