livepilot 1.23.2 → 1.23.4

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 (46) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +108 -10
  3. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  4. package/m4l_device/livepilot_bridge.js +39 -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/runtime/remote_commands.py +10 -0
  18. package/mcp_server/server.py +45 -24
  19. package/mcp_server/tools/agent_os.py +33 -9
  20. package/mcp_server/tools/analyzer.py +84 -23
  21. package/mcp_server/tools/browser.py +20 -1
  22. package/mcp_server/tools/devices.py +78 -11
  23. package/mcp_server/tools/perception.py +5 -1
  24. package/mcp_server/tools/tracks.py +39 -2
  25. package/mcp_server/user_corpus/__init__.py +48 -0
  26. package/mcp_server/user_corpus/manifest.py +142 -0
  27. package/mcp_server/user_corpus/plugin_engine/__init__.py +39 -0
  28. package/mcp_server/user_corpus/plugin_engine/detector.py +579 -0
  29. package/mcp_server/user_corpus/plugin_engine/manual.py +347 -0
  30. package/mcp_server/user_corpus/plugin_engine/research.py +247 -0
  31. package/mcp_server/user_corpus/runner.py +261 -0
  32. package/mcp_server/user_corpus/scanner.py +115 -0
  33. package/mcp_server/user_corpus/scanners/__init__.py +18 -0
  34. package/mcp_server/user_corpus/scanners/adg.py +79 -0
  35. package/mcp_server/user_corpus/scanners/als.py +144 -0
  36. package/mcp_server/user_corpus/scanners/amxd.py +374 -0
  37. package/mcp_server/user_corpus/scanners/plugin_preset.py +202 -0
  38. package/mcp_server/user_corpus/tools.py +904 -0
  39. package/mcp_server/user_corpus/wizard.py +224 -0
  40. package/package.json +2 -2
  41. package/remote_script/LivePilot/__init__.py +1 -1
  42. package/remote_script/LivePilot/browser.py +7 -2
  43. package/remote_script/LivePilot/devices.py +9 -0
  44. package/remote_script/LivePilot/simpler_sample.py +98 -0
  45. package/requirements.txt +3 -3
  46. package/server.json +2 -2
@@ -93,7 +93,11 @@ def analyze_loudness(
93
93
  file_path: str,
94
94
  detail: str = "summary",
95
95
  ) -> dict[str, Any]:
96
- """Analyze the integrated loudness of an audio file (offlineno Ableton needed).
96
+ """Analyze the integrated loudness of an audio file (OFFLINEneeds a rendered file).
97
+
98
+ ⚠ This tool reads a file on disk. It does NOT connect to Ableton.
99
+ For live session monitoring while a track is playing, use
100
+ analyze_loudness_live() instead — no file needed.
97
101
 
98
102
  Computes integrated LUFS (EBU R128), true peak, RMS, crest factor,
99
103
  loudness range (LRA), and streaming platform compliance.
@@ -163,21 +163,52 @@ def _find_name_collisions(ctx: Context, name: str) -> list[int]:
163
163
  return matches
164
164
 
165
165
 
166
+ def _resolve_color_alias(
167
+ color: Optional[int],
168
+ color_index: Optional[int],
169
+ ) -> Optional[int]:
170
+ """BUG-2026-04-26#3: accept both `color` and `color_index` keywords.
171
+
172
+ The track-creation tools used `color` while `set_track_color` used
173
+ `color_index`. Callers writing parallel tool batches (create + paint
174
+ in one shot) consistently picked the wrong name and lost a whole
175
+ parallel batch to the validation error. This helper accepts either,
176
+ rejects the conflict case, and returns the resolved value.
177
+ """
178
+ if color is not None and color_index is not None:
179
+ if color != color_index:
180
+ raise ValueError(
181
+ "Pass either 'color' or 'color_index', not both with "
182
+ f"different values (got color={color}, color_index={color_index})"
183
+ )
184
+ return color
185
+ if color is not None:
186
+ return color
187
+ return color_index
188
+
189
+
166
190
  @mcp.tool()
167
191
  def create_midi_track(
168
192
  ctx: Context,
169
193
  index: int = -1,
170
194
  name: Optional[str] = None,
171
195
  color: Optional[int] = None,
196
+ color_index: Optional[int] = None,
172
197
  ) -> dict:
173
198
  """Create a new MIDI track. index=-1 appends at end.
174
199
 
200
+ `color` and `color_index` are accepted interchangeably (BUG-2026-04-26#3).
201
+ Both reference Ableton's 0-69 color palette. Pass either; passing
202
+ both with different values is rejected.
203
+
175
204
  Response (v1.20.2+): when `name` is provided, the response carries
176
205
  a ``name_collision`` bool and ``existing_tracks_with_same_name``
177
206
  list[int]. Downstream role-based resolvers (find_tracks_by_role)
178
207
  match duplicate names and apply mix changes twice — check the
179
208
  warning before proceeding with mix moves on the new track's role.
180
209
  """
210
+ color = _resolve_color_alias(color, color_index)
211
+
181
212
  collisions: list[int] = []
182
213
  if name is not None and name.strip():
183
214
  collisions = _find_name_collisions(ctx, name)
@@ -205,12 +236,18 @@ def create_audio_track(
205
236
  index: int = -1,
206
237
  name: Optional[str] = None,
207
238
  color: Optional[int] = None,
239
+ color_index: Optional[int] = None,
208
240
  ) -> dict:
209
241
  """Create a new audio track. index=-1 appends at end.
210
242
 
243
+ `color` and `color_index` are accepted interchangeably (BUG-2026-04-26#3).
244
+ See create_midi_track for full semantics.
245
+
211
246
  Response (v1.20.2+): ``name_collision`` + ``existing_tracks_with_same_name``
212
247
  same as create_midi_track — see BUG #5 rationale there.
213
248
  """
249
+ color = _resolve_color_alias(color, color_index)
250
+
214
251
  collisions: list[int] = []
215
252
  if name is not None and name.strip():
216
253
  collisions = _find_name_collisions(ctx, name)
@@ -309,12 +346,12 @@ def set_track_solo(ctx: Context, track_index: int, solo: bool) -> dict:
309
346
 
310
347
 
311
348
  @mcp.tool()
312
- def set_track_arm(ctx: Context, track_index: int, armed: bool) -> dict:
349
+ def set_track_arm(ctx: Context, track_index: int, arm: bool) -> dict:
313
350
  """Arm or disarm a track for recording."""
314
351
  _validate_track_index(track_index)
315
352
  return _get_ableton(ctx).send_command("set_track_arm", {
316
353
  "track_index": track_index,
317
- "arm": armed,
354
+ "arm": arm,
318
355
  })
319
356
 
320
357
 
@@ -0,0 +1,48 @@
1
+ """User-corpus builder — scan your own .als / .adg / .amxd / plugin presets / samples
2
+ into queryable atlas overlays.
3
+
4
+ See docs/USER_CORPUS_GUIDE.md for the full architecture + user guide.
5
+
6
+ Public API:
7
+ from mcp_server.user_corpus import (
8
+ Scanner, register_scanner, get_scanner, list_scanners,
9
+ Manifest, Source, load_manifest, save_manifest,
10
+ run_scan, ScanResult,
11
+ )
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from .scanner import Scanner, register_scanner, get_scanner, list_scanners
16
+ from .manifest import (
17
+ Manifest,
18
+ Source,
19
+ load_manifest,
20
+ save_manifest,
21
+ init_default_manifest,
22
+ DEFAULT_MANIFEST_PATH,
23
+ DEFAULT_OUTPUT_ROOT,
24
+ )
25
+ from .runner import run_scan, ScanResult, SourceScanResult
26
+
27
+ # Eager-import the built-in scanners so their @register_scanner decorators fire.
28
+ from .scanners import als as _als # noqa: F401
29
+ from .scanners import adg as _adg # noqa: F401
30
+ from .scanners import amxd as _amxd # noqa: F401
31
+ from .scanners import plugin_preset as _pp # noqa: F401
32
+
33
+ __all__ = [
34
+ "Scanner",
35
+ "register_scanner",
36
+ "get_scanner",
37
+ "list_scanners",
38
+ "Manifest",
39
+ "Source",
40
+ "load_manifest",
41
+ "save_manifest",
42
+ "init_default_manifest",
43
+ "DEFAULT_MANIFEST_PATH",
44
+ "DEFAULT_OUTPUT_ROOT",
45
+ "run_scan",
46
+ "ScanResult",
47
+ "SourceScanResult",
48
+ ]
@@ -0,0 +1,142 @@
1
+ """Manifest — declarative source registry for user corpus scans.
2
+
3
+ Schema lives in the YAML; this module is just (de)serialization + dataclasses.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from dataclasses import dataclass, field, asdict
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import yaml
15
+
16
+
17
+ # ─── Default paths ───────────────────────────────────────────────────────────
18
+
19
+ DEFAULT_OUTPUT_ROOT = Path.home() / ".livepilot" / "atlas-overlays" / "user"
20
+ DEFAULT_MANIFEST_PATH = DEFAULT_OUTPUT_ROOT / "manifest.yaml"
21
+
22
+ CURRENT_SCHEMA_VERSION = 1
23
+
24
+
25
+ # ─── Data classes ────────────────────────────────────────────────────────────
26
+
27
+
28
+ @dataclass
29
+ class Source:
30
+ """One declared scan source."""
31
+ id: str
32
+ type: str # scanner type_id
33
+ path: str # filesystem path (may contain ~)
34
+ recursive: bool = True
35
+ exclude_globs: list[str] = field(default_factory=list)
36
+ last_scanned: str | None = None # ISO 8601 UTC, set by the runner
37
+ file_count: int | None = None # set by the runner after scan
38
+ options: dict[str, Any] = field(default_factory=dict)
39
+
40
+ @property
41
+ def resolved_path(self) -> Path:
42
+ return Path(os.path.expanduser(self.path)).resolve()
43
+
44
+ def mark_scanned(self, file_count: int) -> None:
45
+ self.last_scanned = datetime.now(timezone.utc).isoformat(timespec="seconds")
46
+ self.file_count = file_count
47
+
48
+
49
+ @dataclass
50
+ class Manifest:
51
+ """Top-level manifest — all scan sources + global options."""
52
+ schema_version: int = CURRENT_SCHEMA_VERSION
53
+ sources: list[Source] = field(default_factory=list)
54
+ output: dict[str, Any] = field(default_factory=lambda: {
55
+ "root": str(DEFAULT_OUTPUT_ROOT),
56
+ "schema_version": CURRENT_SCHEMA_VERSION,
57
+ })
58
+ options: dict[str, Any] = field(default_factory=lambda: {
59
+ "parallel_workers": 4,
60
+ "skip_unchanged": True,
61
+ "log_level": "info",
62
+ "on_error": "continue",
63
+ })
64
+ ai_annotation: dict[str, Any] = field(default_factory=lambda: {
65
+ "enabled": False,
66
+ "model": "sonnet",
67
+ "fields": [
68
+ "sonic_fingerprint",
69
+ "tags",
70
+ "reach_for",
71
+ "avoid",
72
+ "cross_references",
73
+ ],
74
+ })
75
+
76
+ @property
77
+ def output_root(self) -> Path:
78
+ return Path(os.path.expanduser(self.output.get("root", str(DEFAULT_OUTPUT_ROOT))))
79
+
80
+ def find_source(self, source_id: str) -> Source | None:
81
+ for s in self.sources:
82
+ if s.id == source_id:
83
+ return s
84
+ return None
85
+
86
+ def add_source(self, source: Source) -> None:
87
+ if self.find_source(source.id):
88
+ raise ValueError(f"Source id '{source.id}' already exists; remove or rename first")
89
+ self.sources.append(source)
90
+
91
+ def remove_source(self, source_id: str) -> Source | None:
92
+ match = self.find_source(source_id)
93
+ if match:
94
+ self.sources.remove(match)
95
+ return match
96
+
97
+
98
+ # ─── (De)serialization ───────────────────────────────────────────────────────
99
+
100
+
101
+ def load_manifest(path: Path = DEFAULT_MANIFEST_PATH) -> Manifest:
102
+ """Load a manifest YAML. Returns a default Manifest if the file is missing."""
103
+ if not path.exists():
104
+ return Manifest()
105
+ raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
106
+ sources = [Source(**s) for s in (raw.get("sources") or [])]
107
+ return Manifest(
108
+ schema_version=raw.get("schema_version", CURRENT_SCHEMA_VERSION),
109
+ sources=sources,
110
+ output=raw.get("output") or Manifest().output,
111
+ options=raw.get("options") or Manifest().options,
112
+ ai_annotation=raw.get("ai_annotation") or Manifest().ai_annotation,
113
+ )
114
+
115
+
116
+ def save_manifest(manifest: Manifest, path: Path = DEFAULT_MANIFEST_PATH) -> None:
117
+ """Persist a manifest as YAML. Creates parent dirs if needed."""
118
+ path.parent.mkdir(parents=True, exist_ok=True)
119
+ data = {
120
+ "schema_version": manifest.schema_version,
121
+ "sources": [
122
+ {k: v for k, v in asdict(s).items() if v is not None and v != [] and v != {}}
123
+ for s in manifest.sources
124
+ ],
125
+ "output": manifest.output,
126
+ "options": manifest.options,
127
+ "ai_annotation": manifest.ai_annotation,
128
+ }
129
+ path.write_text(
130
+ yaml.dump(data, sort_keys=False, default_flow_style=False, width=200, allow_unicode=True),
131
+ encoding="utf-8",
132
+ )
133
+
134
+
135
+ def init_default_manifest(path: Path = DEFAULT_MANIFEST_PATH) -> Manifest:
136
+ """Create a fresh manifest at the default path, ensuring directory exists."""
137
+ path.parent.mkdir(parents=True, exist_ok=True)
138
+ if path.exists():
139
+ return load_manifest(path)
140
+ m = Manifest()
141
+ save_manifest(m, path)
142
+ return m
@@ -0,0 +1,39 @@
1
+ """Plugin Knowledge Engine — detects installed plugins, extracts identity from
2
+ their bundles, finds + extracts manuals, and emits research / synthesis briefs
3
+ for the agent to fulfill via WebSearch + sonnet subagents.
4
+
5
+ See docs/PLUGIN_KNOWLEDGE_ENGINE.md for the full architecture.
6
+
7
+ Public API:
8
+ from mcp_server.user_corpus.plugin_engine import (
9
+ detect_installed_plugins, discover_manuals_for_plugin,
10
+ extract_manual_text, build_research_targets,
11
+ build_synthesis_brief, default_plugin_dir,
12
+ )
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from .detector import (
17
+ detect_installed_plugins,
18
+ default_plugin_dir,
19
+ DetectedPlugin,
20
+ )
21
+ from .manual import (
22
+ discover_manuals_for_plugin,
23
+ extract_manual_text,
24
+ ManualCandidate,
25
+ ManualExtraction,
26
+ )
27
+ from .research import build_research_targets, build_synthesis_brief
28
+
29
+ __all__ = [
30
+ "detect_installed_plugins",
31
+ "default_plugin_dir",
32
+ "DetectedPlugin",
33
+ "discover_manuals_for_plugin",
34
+ "extract_manual_text",
35
+ "ManualCandidate",
36
+ "ManualExtraction",
37
+ "build_research_targets",
38
+ "build_synthesis_brief",
39
+ ]