livepilot 1.23.3 → 1.23.5

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 (45) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +106 -8
  3. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  4. package/m4l_device/livepilot_bridge.js +1 -1
  5. package/mcp_server/__init__.py +1 -1
  6. package/mcp_server/atlas/cross_pack_chain.py +658 -0
  7. package/mcp_server/atlas/demo_story.py +700 -0
  8. package/mcp_server/atlas/extract_chain.py +786 -0
  9. package/mcp_server/atlas/macro_fingerprint.py +554 -0
  10. package/mcp_server/atlas/overlays.py +95 -3
  11. package/mcp_server/atlas/pack_aware_compose.py +1255 -0
  12. package/mcp_server/atlas/preset_resolver.py +238 -0
  13. package/mcp_server/atlas/tools.py +1001 -31
  14. package/mcp_server/atlas/transplant.py +1177 -0
  15. package/mcp_server/mix_engine/state_builder.py +44 -1
  16. package/mcp_server/runtime/capability_state.py +34 -3
  17. package/mcp_server/server.py +45 -24
  18. package/mcp_server/tools/agent_os.py +33 -9
  19. package/mcp_server/tools/analyzer.py +38 -7
  20. package/mcp_server/tools/browser.py +20 -1
  21. package/mcp_server/tools/devices.py +78 -11
  22. package/mcp_server/tools/perception.py +5 -1
  23. package/mcp_server/tools/tracks.py +39 -2
  24. package/mcp_server/user_corpus/__init__.py +48 -0
  25. package/mcp_server/user_corpus/manifest.py +142 -0
  26. package/mcp_server/user_corpus/plugin_engine/__init__.py +39 -0
  27. package/mcp_server/user_corpus/plugin_engine/detector.py +579 -0
  28. package/mcp_server/user_corpus/plugin_engine/manual.py +347 -0
  29. package/mcp_server/user_corpus/plugin_engine/research.py +247 -0
  30. package/mcp_server/user_corpus/runner.py +261 -0
  31. package/mcp_server/user_corpus/scanner.py +115 -0
  32. package/mcp_server/user_corpus/scanners/__init__.py +18 -0
  33. package/mcp_server/user_corpus/scanners/adg.py +79 -0
  34. package/mcp_server/user_corpus/scanners/als.py +144 -0
  35. package/mcp_server/user_corpus/scanners/amxd.py +374 -0
  36. package/mcp_server/user_corpus/scanners/plugin_preset.py +202 -0
  37. package/mcp_server/user_corpus/tools.py +904 -0
  38. package/mcp_server/user_corpus/wizard.py +224 -0
  39. package/package.json +2 -2
  40. package/remote_script/LivePilot/__init__.py +1 -1
  41. package/remote_script/LivePilot/browser.py +7 -2
  42. package/remote_script/LivePilot/server.py +38 -22
  43. package/remote_script/LivePilot/transport.py +15 -5
  44. package/requirements.txt +3 -3
  45. package/server.json +2 -2
@@ -0,0 +1,238 @@
1
+ """Resolve a demo track's device → matching preset sidecar.
2
+
3
+ Demo sidecars record device class + user_name but NOT macro names. Preset
4
+ sidecars carry the producer-assigned macro names. This module bridges the
5
+ two so plan emitters can output executable set_device_parameter steps with
6
+ real names (e.g. "Rift Rate") instead of generic "Macro N" labels, AND can
7
+ suggest a search_browser query for load_browser_item URI resolution.
8
+
9
+ Demo macro shape (no name):
10
+ [{"index": 0, "value": "1"}, ...]
11
+ Preset macro shape (with producer-assigned name):
12
+ [{"index": 0, "value": "1", "name": "Rift Rate"}, ...]
13
+
14
+ Live's browser URIs are FileId-keyed and require a runtime browser query —
15
+ there's no static URI we can derive from the sidecar alone, so this module
16
+ returns a search_browser HINT that the agent uses to resolve to a concrete
17
+ URI before calling load_browser_item.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ from functools import lru_cache
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
+ PRESET_PARSES_ROOT = (
28
+ Path.home() / ".livepilot" / "atlas-overlays" / "packs" / "_preset_parses"
29
+ )
30
+
31
+
32
+ @lru_cache(maxsize=64)
33
+ def _load_pack_index(pack_slug: str) -> tuple[tuple[str, str, str], ...]:
34
+ """Return tuple of (preset_name, rack_class, sidecar_path) for the pack.
35
+
36
+ Cached. Returns empty tuple if pack dir doesn't exist.
37
+ """
38
+ pack_dir = PRESET_PARSES_ROOT / pack_slug
39
+ if not pack_dir.is_dir():
40
+ # Try hyphen/underscore swap as a safety net
41
+ alt = PRESET_PARSES_ROOT / pack_slug.replace("_", "-")
42
+ if alt.is_dir():
43
+ pack_dir = alt
44
+ else:
45
+ return ()
46
+ entries: list[tuple[str, str, str]] = []
47
+ for sidecar in pack_dir.glob("*.json"):
48
+ try:
49
+ data = json.loads(sidecar.read_text(encoding="utf-8"))
50
+ except (OSError, json.JSONDecodeError):
51
+ continue
52
+ name = (data.get("name") or "").strip()
53
+ rack_class = (data.get("rack_class") or "").strip()
54
+ if name:
55
+ entries.append((name, rack_class, str(sidecar)))
56
+ return tuple(entries)
57
+
58
+
59
+ def resolve_preset_for_device(
60
+ pack_slug: str,
61
+ device_class: str,
62
+ device_user_name: str,
63
+ ) -> dict:
64
+ """Find the matching preset sidecar for a demo track's device.
65
+
66
+ Match priority:
67
+ 1. Exact name match (case-insensitive) within the pack
68
+ 2. Exact name match with matching rack_class
69
+ 3. Substring match (preset_name in user_name OR vice versa)
70
+ 4. None found → empty result
71
+
72
+ Args:
73
+ pack_slug: pack identifier, e.g. "drone-lab"
74
+ device_class: Live device class, e.g. "InstrumentGroupDevice"
75
+ (used as a tiebreaker; pass empty string to skip)
76
+ device_user_name: rack's user-visible name, e.g. "Pioneer Drone"
77
+
78
+ Returns dict:
79
+ found: bool
80
+ match_type: "exact" | "exact_with_class" | "partial" | "none"
81
+ sidecar_path: str | None
82
+ preset_name: str | None
83
+ macro_names: dict[int, str] # {macro_index: producer-assigned name}
84
+ browser_search_hint: dict | None
85
+ {name_filter: str, suggested_path: str}
86
+ preset_file: str | None # original .adg path for logging
87
+ """
88
+ if not device_user_name or not pack_slug:
89
+ return _empty_result()
90
+
91
+ target = device_user_name.strip().lower()
92
+ pack_index = _load_pack_index(pack_slug)
93
+ if not pack_index:
94
+ return _empty_result()
95
+
96
+ # Pass 1: exact name + rack_class match (strongest)
97
+ if device_class:
98
+ for name, rack_class, sidecar_path in pack_index:
99
+ if name.lower() == target and rack_class == device_class:
100
+ return _build_result(sidecar_path, name, "exact_with_class")
101
+
102
+ # Pass 2: exact name match (any class)
103
+ for name, _rc, sidecar_path in pack_index:
104
+ if name.lower() == target:
105
+ return _build_result(sidecar_path, name, "exact")
106
+
107
+ # Pass 3: substring fallback
108
+ for name, _rc, sidecar_path in pack_index:
109
+ nlow = name.lower()
110
+ if (nlow and (nlow in target or target in nlow)
111
+ and abs(len(nlow) - len(target)) < max(len(nlow), len(target))):
112
+ return _build_result(sidecar_path, name, "partial")
113
+
114
+ return _empty_result()
115
+
116
+
117
+ def lookup_macro_name(
118
+ pack_slug: str, device_user_name: str, macro_index: int
119
+ ) -> Optional[str]:
120
+ """Convenience: get a single macro name without re-resolving every time."""
121
+ res = resolve_preset_for_device(pack_slug, "", device_user_name)
122
+ return res["macro_names"].get(macro_index)
123
+
124
+
125
+ # ─── internals ────────────────────────────────────────────────────────────────
126
+
127
+
128
+ def _build_result(sidecar_path: str, preset_name: str, match_type: str) -> dict:
129
+ try:
130
+ data = json.loads(Path(sidecar_path).read_text(encoding="utf-8"))
131
+ except (OSError, json.JSONDecodeError):
132
+ return _empty_result()
133
+
134
+ macros = data.get("macros") or []
135
+ macro_names: dict[int, str] = {}
136
+ for m in macros:
137
+ try:
138
+ idx = int(m.get("index", -1))
139
+ name = (m.get("name") or "").strip()
140
+ if idx >= 0 and name:
141
+ macro_names[idx] = name
142
+ except (ValueError, TypeError):
143
+ continue
144
+
145
+ preset_file = data.get("file") or ""
146
+ return {
147
+ "found": True,
148
+ "match_type": match_type,
149
+ "sidecar_path": sidecar_path,
150
+ "preset_name": preset_name,
151
+ "macro_names": macro_names,
152
+ "browser_search_hint": {
153
+ "name_filter": preset_name,
154
+ "suggested_path": _infer_browser_path(preset_file),
155
+ },
156
+ "preset_file": preset_file,
157
+ }
158
+
159
+
160
+ def _empty_result() -> dict:
161
+ return {
162
+ "found": False,
163
+ "match_type": "none",
164
+ "sidecar_path": None,
165
+ "preset_name": None,
166
+ "macro_names": {},
167
+ "browser_search_hint": None,
168
+ "preset_file": None,
169
+ }
170
+
171
+
172
+ def _infer_browser_path(file_path: str) -> str:
173
+ """Map a preset sidecar 'file' field to a search_browser path category.
174
+
175
+ Examples:
176
+ "Drone Lab/Sounds/Synth Pad/Pioneer Drone.adg" → "sounds"
177
+ "Beat Tools/Drums/Kicks/Foo.adg" → "drums"
178
+ "Inspired by Nature/Instruments/Tree Tone.adg" → "instruments"
179
+
180
+ Defaults to "sounds" for unknown layouts (the broadest factory-pack
181
+ category for instrument racks).
182
+ """
183
+ parts = [p.lower() for p in file_path.split("/") if p]
184
+ if len(parts) < 2:
185
+ return "sounds"
186
+ second = parts[1]
187
+ if "sound" in second or "synth" in second:
188
+ return "sounds"
189
+ if "drum" in second:
190
+ return "drums"
191
+ if "instrument" in second:
192
+ return "instruments"
193
+ if "audio effect" in second or "fx" in second or "effect" in second:
194
+ return "audio_effects"
195
+ if "midi" in second:
196
+ return "midi_effects"
197
+ return "sounds"
198
+
199
+
200
+ def emit_load_step(
201
+ pack_slug: str,
202
+ device_class: str,
203
+ device_user_name: str,
204
+ track_index: int,
205
+ ) -> dict:
206
+ """Build a load_browser_item plan step with embedded search-resolution hint.
207
+
208
+ Returns a dict matching the shape downstream callers (extract_chain,
209
+ pack_aware_compose) emit into their executable_steps lists. The agent
210
+ is expected to call search_browser(**browser_search_hint) first, then
211
+ load_browser_item(track_index=track_index, uri=<resolved_uri>).
212
+ """
213
+ res = resolve_preset_for_device(pack_slug, device_class, device_user_name)
214
+ step: dict = {
215
+ "action": "load_browser_item",
216
+ "track_index": track_index,
217
+ "name": device_user_name,
218
+ "device_class": device_class,
219
+ "comment": (
220
+ "Resolve URI via search_browser before calling load_browser_item. "
221
+ "The browser_search_hint provides the recommended path + name_filter."
222
+ ),
223
+ }
224
+ if res["found"]:
225
+ step["browser_search_hint"] = res["browser_search_hint"]
226
+ step["preset_name"] = res["preset_name"]
227
+ step["preset_file"] = res["preset_file"]
228
+ step["match_type"] = res["match_type"]
229
+ else:
230
+ step["browser_search_hint"] = {
231
+ "name_filter": device_user_name,
232
+ "suggested_path": "sounds",
233
+ }
234
+ step["match_type"] = "none"
235
+ step["comment"] += (
236
+ f" (no preset sidecar found in pack '{pack_slug}'; using user_name as fallback)"
237
+ )
238
+ return step