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.
- package/CHANGELOG.md +124 -0
- package/README.md +108 -10
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +39 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/cross_pack_chain.py +658 -0
- package/mcp_server/atlas/demo_story.py +700 -0
- package/mcp_server/atlas/extract_chain.py +786 -0
- package/mcp_server/atlas/macro_fingerprint.py +554 -0
- package/mcp_server/atlas/overlays.py +95 -3
- package/mcp_server/atlas/pack_aware_compose.py +1255 -0
- package/mcp_server/atlas/preset_resolver.py +238 -0
- package/mcp_server/atlas/tools.py +1001 -31
- package/mcp_server/atlas/transplant.py +1177 -0
- package/mcp_server/mix_engine/state_builder.py +44 -1
- package/mcp_server/runtime/capability_state.py +34 -3
- package/mcp_server/runtime/remote_commands.py +10 -0
- package/mcp_server/server.py +45 -24
- package/mcp_server/tools/agent_os.py +33 -9
- package/mcp_server/tools/analyzer.py +84 -23
- package/mcp_server/tools/browser.py +20 -1
- package/mcp_server/tools/devices.py +78 -11
- package/mcp_server/tools/perception.py +5 -1
- package/mcp_server/tools/tracks.py +39 -2
- package/mcp_server/user_corpus/__init__.py +48 -0
- package/mcp_server/user_corpus/manifest.py +142 -0
- package/mcp_server/user_corpus/plugin_engine/__init__.py +39 -0
- package/mcp_server/user_corpus/plugin_engine/detector.py +579 -0
- package/mcp_server/user_corpus/plugin_engine/manual.py +347 -0
- package/mcp_server/user_corpus/plugin_engine/research.py +247 -0
- package/mcp_server/user_corpus/runner.py +261 -0
- package/mcp_server/user_corpus/scanner.py +115 -0
- package/mcp_server/user_corpus/scanners/__init__.py +18 -0
- package/mcp_server/user_corpus/scanners/adg.py +79 -0
- package/mcp_server/user_corpus/scanners/als.py +144 -0
- package/mcp_server/user_corpus/scanners/amxd.py +374 -0
- package/mcp_server/user_corpus/scanners/plugin_preset.py +202 -0
- package/mcp_server/user_corpus/tools.py +904 -0
- package/mcp_server/user_corpus/wizard.py +224 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/browser.py +7 -2
- package/remote_script/LivePilot/devices.py +9 -0
- package/remote_script/LivePilot/simpler_sample.py +98 -0
- package/requirements.txt +3 -3
- 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 (
|
|
96
|
+
"""Analyze the integrated loudness of an audio file (OFFLINE — needs 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,
|
|
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":
|
|
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
|
+
]
|