livepilot 1.22.0 → 1.23.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.
- package/CHANGELOG.md +87 -0
- package/README.md +12 -7
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +6 -0
- package/mcp_server/atlas/overlays.py +261 -0
- package/mcp_server/atlas/tools.py +98 -0
- package/mcp_server/server.py +29 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.23.0 — 2026-04-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **User-local atlas overlay mechanism** for extending the atlas with namespaced YAML content from `~/.livepilot/atlas-overlays/<namespace>/` (custom hardware libraries, signature chains, technique recipes). Survives npm updates. Generalizes the v1.22.0 user-scan pattern from "atlas data" to "any user-local namespace."
|
|
7
|
+
- 3 new MCP tools (430 → 433):
|
|
8
|
+
- `extension_atlas_search(query, namespace?, entity_type?, limit?)` — weighted substring search across overlay entries
|
|
9
|
+
- `extension_atlas_get(namespace, entity_id)` — fetch a single overlay entry with full body (including `requires_firmware` if present)
|
|
10
|
+
- `extension_atlas_list(namespace?)` — enumerate namespaces + entity_type counts
|
|
11
|
+
- New file: `mcp_server/atlas/overlays.py` — `OverlayEntry` dataclass, `OverlayIndex` class, `load_overlays()`, lazy path resolver, module-level singleton.
|
|
12
|
+
- New doc: `docs/EXTENSION_API.md` — public API contract for extension authors.
|
|
13
|
+
|
|
14
|
+
### Behavior
|
|
15
|
+
- Atlas overlays load at server boot from `~/.livepilot/atlas-overlays/`. Non-fatal — server continues if missing or malformed.
|
|
16
|
+
- Loader uses `yaml.safe_load` only (rejects Python tags). Per-file parse failures log a WARN and skip the file; per-entry validation failures log a WARN and skip the entry; duplicate `(namespace, entity_type, entity_id)` last-loaded wins with a WARN.
|
|
17
|
+
- For `entity_type: signature_chain`, `tags` and `artists` are required (search ranker depends on them). Other entity types treat these as optional.
|
|
18
|
+
- `entity_id` and `entity_type` are str-coerced to defend against YAML scalar values like `entity_id: 42`.
|
|
19
|
+
|
|
20
|
+
### Notes for extension authors
|
|
21
|
+
- The contract (`OverlayEntry` field names, `extension_atlas_*` tool API) is stable from v1.23.0 forward.
|
|
22
|
+
- Tool-name collisions: FastMCP enforces first-registered-wins. Bundled tools always beat extensions.
|
|
23
|
+
- Phase 2 (user-local Python extensions via `register(mcp)`) lands in a future minor version.
|
|
24
|
+
|
|
25
|
+
Spec: `docs/superpowers/specs/2026-04-25-user-local-extensions-design.md` (gitignored, design-time artifact)
|
|
26
|
+
Plan: `docs/superpowers/plans/2026-04-25-phase-1a-atlas-overlays-plan.md` (gitignored, implementation tracker)
|
|
27
|
+
|
|
28
|
+
## 1.22.1 — Bundled enrichment coverage gate (April 25 2026)
|
|
29
|
+
|
|
30
|
+
Closes the one item carried from v1.22.0's atlas-separation work: a
|
|
31
|
+
visibility + soft-gate for the drift between the atlas file's
|
|
32
|
+
self-reported enrichment count and the YAML files on disk.
|
|
33
|
+
|
|
34
|
+
### What changed
|
|
35
|
+
|
|
36
|
+
Two enrichment numbers now surface in `sync_metadata --check`:
|
|
37
|
+
|
|
38
|
+
- **`enriched=N`** (existing) — YAML profiles authored in `mcp_server/atlas/enrichments/`. Measures "what's available for merge."
|
|
39
|
+
- **`bundled_enriched=N`** (new) — `stats.enriched_devices` from the shipped `mcp_server/atlas/device_atlas.json`. Measures "what the last scan_full_library run actually applied at build time."
|
|
40
|
+
|
|
41
|
+
These measure different things. YAML count is authoring effort; bundled
|
|
42
|
+
count is runtime coverage as of the atlas's last regeneration. They
|
|
43
|
+
drift naturally (someone adds a YAML without re-scanning) — but until
|
|
44
|
+
v1.22.1 the drift was invisible to CI.
|
|
45
|
+
|
|
46
|
+
### Soft gate
|
|
47
|
+
|
|
48
|
+
Warns (doesn't fail) on two conditions:
|
|
49
|
+
|
|
50
|
+
1. **`bundled_enriched == 0`** with YAMLs on disk — scanner never ran
|
|
51
|
+
or failed completely. Most likely the repo's bundled atlas got
|
|
52
|
+
accidentally emptied or mis-committed.
|
|
53
|
+
2. **`bundled_enriched / yaml_count < 50%`** — scanner truncated or had
|
|
54
|
+
severe pack-coverage failures. Current shipped atlas is 87/120 = 72%
|
|
55
|
+
coverage (healthy — the 33 orphan gap is the miditool-domain YAMLs
|
|
56
|
+
that Live's browser scanner can't see).
|
|
57
|
+
|
|
58
|
+
Why soft: the relationship `yaml >= bundled` is only true in
|
|
59
|
+
single-pack-scan scenarios. Multi-category duplication (native +
|
|
60
|
+
max_for_live + user_library for the same device_id) can push
|
|
61
|
+
`bundled > yaml`. Strict equality would produce false alarms. The soft
|
|
62
|
+
gate catches the two real failure modes while staying silent on healthy
|
|
63
|
+
cases.
|
|
64
|
+
|
|
65
|
+
### Output format
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Source of truth: version=1.22.1, tools=430, domains=53, bridge_cmds=31,
|
|
69
|
+
enriched=120, bundled_enriched=87, genres=4, moves=44,
|
|
70
|
+
analyzer_tools=38, atlas_devices=5264
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Warnings (if any) print above the fail/pass line with a ⚠️ header,
|
|
74
|
+
separate from the issue list. The exit code is unchanged — warnings
|
|
75
|
+
don't fail CI.
|
|
76
|
+
|
|
77
|
+
### Tests
|
|
78
|
+
|
|
79
|
+
7 new TDD tests in `tests/test_claim_consistency.py`. Full suite: 3143
|
|
80
|
+
pass (3136 prior + 7 new), 1 skipped.
|
|
81
|
+
|
|
82
|
+
### Why this is a patch, not a feature
|
|
83
|
+
|
|
84
|
+
Pure CI-gate tightening. Zero user-visible runtime behavior change;
|
|
85
|
+
the only observable delta is one additional field in the banner plus
|
|
86
|
+
the possibility of a soft-warning line during `sync_metadata --check`.
|
|
87
|
+
No new tools, no atlas behavior change. The v1.22.0 user/bundled split
|
|
88
|
+
is the feature release; v1.22.1 is its mechanical follow-through.
|
|
89
|
+
|
|
3
90
|
## 1.22.0 — User atlas separation: ~/.livepilot/atlas/ (April 25 2026)
|
|
4
91
|
|
|
5
92
|
First v1.22 release. Splits the device atlas into two files that serve
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
An agentic production system for Ableton Live 12.<br>
|
|
20
|
-
|
|
20
|
+
433 tools. 53 domains. Device atlas. Plan-aware Splice integration. Auto-composition. Spectral perception. Technique memory. Drum-rack pad builder. Live dead-device detection.
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
<br>
|
|
@@ -82,7 +82,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
82
82
|
│ └─────────────────┼──────────────────┘ │
|
|
83
83
|
│ ▼ │
|
|
84
84
|
│ ┌─────────────────┐ │
|
|
85
|
-
│ │
|
|
85
|
+
│ │ 433 MCP Tools │ │
|
|
86
86
|
│ │ 53 domains │ │
|
|
87
87
|
│ └────────┬────────┘ │
|
|
88
88
|
│ │ │
|
|
@@ -123,7 +123,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
123
123
|
|
|
124
124
|
## The Intelligence Layer
|
|
125
125
|
|
|
126
|
-
12 engines sit on top of the
|
|
126
|
+
12 engines sit on top of the 433 tools. They give the AI musical judgment, not just musical execution.
|
|
127
127
|
|
|
128
128
|
### SongBrain — What the Song Is
|
|
129
129
|
|
|
@@ -175,7 +175,7 @@ Every engine follows: **measure before → act → measure after → compare**.
|
|
|
175
175
|
|
|
176
176
|
## Tools
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
433 tools across 53 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
|
|
179
179
|
|
|
180
180
|
<br>
|
|
181
181
|
|
|
@@ -230,7 +230,7 @@ WARP ─────────── get / add / move / remove markers
|
|
|
230
230
|
|
|
231
231
|
<br>
|
|
232
232
|
|
|
233
|
-
### Device Atlas —
|
|
233
|
+
### Device Atlas — 13 tools
|
|
234
234
|
|
|
235
235
|
The atlas is an in-memory indexed database of Ableton's entire device library.
|
|
236
236
|
|
|
@@ -254,8 +254,13 @@ atlas_techniques_for_device Reverse-lookup: what techniques reference this de
|
|
|
254
254
|
atlas_pack_info Inspect a single Ableton pack — devices + enrichment coverage
|
|
255
255
|
scan_full_library Scan what's actually installed on this machine
|
|
256
256
|
reload_atlas Hot-reload the atlas after adding enrichments
|
|
257
|
+
extension_atlas_search [v1.23.0+] Search user-local atlas overlays
|
|
258
|
+
extension_atlas_get [v1.23.0+] Fetch a single overlay entry by namespace
|
|
259
|
+
extension_atlas_list [v1.23.0+] Enumerate overlay namespaces + entity_type counts
|
|
257
260
|
```
|
|
258
261
|
|
|
262
|
+
**v1.23.0 — User-local extensions:** Drop YAML files at `~/.livepilot/atlas-overlays/<namespace>/` to extend the atlas with custom hardware libraries, signature chains, or technique recipes — survives npm updates. See [`docs/EXTENSION_API.md`](docs/EXTENSION_API.md).
|
|
263
|
+
|
|
259
264
|
<br>
|
|
260
265
|
|
|
261
266
|
### Sample Engine — 23 tools
|
|
@@ -391,7 +396,7 @@ The V2 intelligence layer. These tools analyze, diagnose, plan, evaluate, and le
|
|
|
391
396
|
| Creative Constraints | 5 | constraint activation, reference-inspired variants |
|
|
392
397
|
| Preview Studio | 5 | variant creation, preview rendering, comparison, commit |
|
|
393
398
|
|
|
394
|
-
> **[View all
|
|
399
|
+
> **[View all 433 tools →](docs/manual/tool-catalog.md)**
|
|
395
400
|
|
|
396
401
|
<br>
|
|
397
402
|
|
|
@@ -618,7 +623,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, code guidelines
|
|
|
618
623
|
|
|
619
624
|
| Document | What's inside |
|
|
620
625
|
|----------|---------------|
|
|
621
|
-
| [Manual](docs/manual/index.md) | Complete reference: architecture, all
|
|
626
|
+
| [Manual](docs/manual/index.md) | Complete reference: architecture, all 433 tools, workflows |
|
|
622
627
|
| [Intelligence Layer](docs/manual/intelligence.md) | How the 12 engines connect — conductor, moves, preview, evaluation |
|
|
623
628
|
| [Device Atlas](docs/manual/device-atlas.md) | 5264 devices indexed — search, suggest, chain building |
|
|
624
629
|
| [Samples & Slicing](docs/manual/samples.md) | 3-source search, fitness critics, slice workflows |
|
|
Binary file
|
|
@@ -34,7 +34,7 @@ outlets = 2; // 0: to udpsend (responses), 1: to buffer~/status
|
|
|
34
34
|
// Single source of truth for the bridge version — bumped alongside the
|
|
35
35
|
// rest of the release manifest. Surfaced in the UI via messnamed("livepilot_version", ...)
|
|
36
36
|
// so the frozen .amxd visibly reports which build it was last exported from.
|
|
37
|
-
var VERSION = "1.
|
|
37
|
+
var VERSION = "1.23.0";
|
|
38
38
|
|
|
39
39
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
40
40
|
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.23.0"
|
|
@@ -596,3 +596,9 @@ def invalidate_atlas() -> None:
|
|
|
596
596
|
def _load_atlas() -> AtlasManager:
|
|
597
597
|
"""Legacy shim — kept so atlas/tools.py still works. Prefer get_atlas()."""
|
|
598
598
|
return get_atlas()
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# v1.23.0: re-export overlay accessor so callers can do
|
|
602
|
+
# `from mcp_server.atlas import get_overlay_index` mirroring the existing
|
|
603
|
+
# `from mcp_server.atlas import get_atlas` ergonomic.
|
|
604
|
+
from .overlays import get_overlay_index, load_overlays # noqa: E402, F401
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# mcp_server/atlas/overlays.py
|
|
2
|
+
"""User-local atlas overlay loader (v1.23.0).
|
|
3
|
+
|
|
4
|
+
Generalizes the v1.22.0 BUNDLED_ATLAS_PATH / USER_ATLAS_PATH pattern to
|
|
5
|
+
support arbitrary user-local namespaces of YAML overlay entries
|
|
6
|
+
(machines, signature chains, aesthetic lineages, techniques) under
|
|
7
|
+
~/.livepilot/atlas-overlays/<namespace>/.
|
|
8
|
+
|
|
9
|
+
Per spec: docs/superpowers/specs/2026-04-25-user-local-extensions-design.md
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class OverlayEntry:
|
|
25
|
+
"""A single overlay entity loaded from a YAML file under a namespace.
|
|
26
|
+
|
|
27
|
+
Field names mirror the spec §5.1. `entity_id` (not `id`) avoids
|
|
28
|
+
shadowing the Python `id()` builtin and matches the
|
|
29
|
+
`OverlayIndex.get(namespace, entity_id)` accessor signature.
|
|
30
|
+
|
|
31
|
+
For entity_type='signature_chain', `tags` and `artists` are required
|
|
32
|
+
(the search ranker hits them). The loader enforces this — see
|
|
33
|
+
`_validate_entry` (added in a later task).
|
|
34
|
+
"""
|
|
35
|
+
namespace: str
|
|
36
|
+
entity_type: str
|
|
37
|
+
entity_id: str
|
|
38
|
+
name: str
|
|
39
|
+
description: str
|
|
40
|
+
tags: list[str] = field(default_factory=list)
|
|
41
|
+
artists: list[str] = field(default_factory=list)
|
|
42
|
+
requires_box: Optional[str] = None
|
|
43
|
+
body: dict = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OverlayIndex:
|
|
47
|
+
"""In-memory index of overlay entries, partitioned by (namespace, entity_type, entity_id).
|
|
48
|
+
|
|
49
|
+
Mutated in place by load_overlays() (added in a later task). Tools call
|
|
50
|
+
get_overlay_index() at request time to read the current state — never
|
|
51
|
+
capture a reference at import time.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
self._entries: dict[tuple[str, str, str], OverlayEntry] = {}
|
|
56
|
+
|
|
57
|
+
def add(self, entry: OverlayEntry) -> Optional[OverlayEntry]:
|
|
58
|
+
"""Insert or replace. Returns the previous entry on collision (or None
|
|
59
|
+
on a fresh insert) so callers can log a duplicate-id warning per spec §7."""
|
|
60
|
+
key = (entry.namespace, entry.entity_type, entry.entity_id)
|
|
61
|
+
previous = self._entries.get(key)
|
|
62
|
+
self._entries[key] = entry
|
|
63
|
+
return previous
|
|
64
|
+
|
|
65
|
+
def get(self, namespace: str, entity_id: str) -> Optional[OverlayEntry]:
|
|
66
|
+
"""Lookup by (namespace, entity_id), ignoring entity_type.
|
|
67
|
+
|
|
68
|
+
If two entries share the same (namespace, entity_id) across different
|
|
69
|
+
entity_types, returns whichever the dict iterator yields first
|
|
70
|
+
(insertion order in CPython 3.7+). The loader (Tasks 7+8) is responsible
|
|
71
|
+
for preventing such collisions via dup-id warnings.
|
|
72
|
+
"""
|
|
73
|
+
for (ns, _et, eid), entry in self._entries.items():
|
|
74
|
+
if ns == namespace and eid == entity_id:
|
|
75
|
+
return entry
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def list_namespaces(self) -> list[str]:
|
|
79
|
+
return sorted({ns for (ns, _, _) in self._entries.keys()})
|
|
80
|
+
|
|
81
|
+
def list_entity_types(self, namespace: str) -> list[str]:
|
|
82
|
+
return sorted({et for (ns, et, _) in self._entries.keys() if ns == namespace})
|
|
83
|
+
|
|
84
|
+
def clear(self) -> None:
|
|
85
|
+
"""Reset for idempotency (used by load_overlays in a later task)."""
|
|
86
|
+
self._entries.clear()
|
|
87
|
+
|
|
88
|
+
def all_entries(self) -> list[OverlayEntry]:
|
|
89
|
+
return list(self._entries.values())
|
|
90
|
+
|
|
91
|
+
def search(self, query: str, namespace: Optional[str] = None,
|
|
92
|
+
entity_type: Optional[str] = None,
|
|
93
|
+
limit: int = 10) -> list[OverlayEntry]:
|
|
94
|
+
"""Weighted substring search.
|
|
95
|
+
|
|
96
|
+
Scores per entry:
|
|
97
|
+
+1000 if query == entity_id (case-insensitive exact)
|
|
98
|
+
+100 per substring hit in name
|
|
99
|
+
+50 per substring hit in tag or artist
|
|
100
|
+
+10 per substring hit in description
|
|
101
|
+
|
|
102
|
+
Sorts by descending score, then by entity_id for stable ties.
|
|
103
|
+
Filters by namespace and/or entity_type if provided.
|
|
104
|
+
Empty query returns empty list.
|
|
105
|
+
"""
|
|
106
|
+
q = (query or "").strip().lower()
|
|
107
|
+
if not q:
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
scored: list[tuple[int, str, OverlayEntry]] = []
|
|
111
|
+
for entry in self._entries.values():
|
|
112
|
+
if namespace is not None and entry.namespace != namespace:
|
|
113
|
+
continue
|
|
114
|
+
if entity_type is not None and entry.entity_type != entity_type:
|
|
115
|
+
continue
|
|
116
|
+
score = 0
|
|
117
|
+
if entry.entity_id.lower() == q:
|
|
118
|
+
score += 1000
|
|
119
|
+
if q in entry.name.lower():
|
|
120
|
+
score += 100
|
|
121
|
+
for tag in entry.tags:
|
|
122
|
+
if q in str(tag).lower():
|
|
123
|
+
score += 50
|
|
124
|
+
for artist in entry.artists:
|
|
125
|
+
if q in str(artist).lower():
|
|
126
|
+
score += 50
|
|
127
|
+
if q in entry.description.lower():
|
|
128
|
+
score += 10
|
|
129
|
+
if score > 0:
|
|
130
|
+
scored.append((score, entry.entity_id, entry))
|
|
131
|
+
|
|
132
|
+
scored.sort(key=lambda triple: (-triple[0], triple[1]))
|
|
133
|
+
return [entry for (_, _, entry) in scored[:max(0, limit)]]
|
|
134
|
+
|
|
135
|
+
def stats(self) -> dict:
|
|
136
|
+
"""Counts per namespace per entity_type (used by extension_atlas_list in Task 12)."""
|
|
137
|
+
counts: dict[str, dict[str, int]] = {}
|
|
138
|
+
for (ns, et, _eid) in self._entries.keys():
|
|
139
|
+
counts.setdefault(ns, {}).setdefault(et, 0)
|
|
140
|
+
counts[ns][et] += 1
|
|
141
|
+
return counts
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _resolve_overlay_root() -> Path:
|
|
145
|
+
"""Lazy resolver mirroring v1.22.0 _resolve_atlas_path() pattern.
|
|
146
|
+
Tests monkeypatch Path.home() and expect this to re-evaluate."""
|
|
147
|
+
return Path.home() / ".livepilot" / "atlas-overlays"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _validate_entry(entry: dict, source_path: Path,
|
|
151
|
+
log: logging.Logger) -> bool:
|
|
152
|
+
"""True if entry has required fields. Log + return False otherwise.
|
|
153
|
+
|
|
154
|
+
Per spec §5.1:
|
|
155
|
+
- All entries: entity_id + entity_type required
|
|
156
|
+
- entity_type=signature_chain: also tags + artists required
|
|
157
|
+
"""
|
|
158
|
+
eid = entry.get("entity_id")
|
|
159
|
+
etype = entry.get("entity_type")
|
|
160
|
+
if not eid:
|
|
161
|
+
log.warning(f"overlays: skipped entry in {source_path}: missing 'entity_id'")
|
|
162
|
+
return False
|
|
163
|
+
if not etype:
|
|
164
|
+
log.warning(f"overlays: skipped {eid} in {source_path}: missing 'entity_type'")
|
|
165
|
+
return False
|
|
166
|
+
if etype == "signature_chain":
|
|
167
|
+
if not entry.get("tags"):
|
|
168
|
+
log.warning(f"overlays: skipped {eid} in {source_path}: signature_chain requires 'tags'")
|
|
169
|
+
return False
|
|
170
|
+
if not entry.get("artists"):
|
|
171
|
+
log.warning(f"overlays: skipped {eid} in {source_path}: signature_chain requires 'artists'")
|
|
172
|
+
return False
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _entry_from_dict(d: dict, namespace: str) -> OverlayEntry:
|
|
177
|
+
"""Build an OverlayEntry from a validated YAML dict.
|
|
178
|
+
The full original dict is preserved as `body` so callers can read
|
|
179
|
+
arbitrary extra fields (architecture, requires_machines, sources, etc.).
|
|
180
|
+
|
|
181
|
+
Coerces entity_id and entity_type to str() defensively — guards against
|
|
182
|
+
YAMLs that use non-string scalar values (e.g., `entity_id: 42`) which
|
|
183
|
+
would otherwise break downstream search() (.lower() on int).
|
|
184
|
+
"""
|
|
185
|
+
return OverlayEntry(
|
|
186
|
+
namespace=namespace,
|
|
187
|
+
entity_type=str(d["entity_type"]),
|
|
188
|
+
entity_id=str(d["entity_id"]),
|
|
189
|
+
name=d.get("name", ""),
|
|
190
|
+
description=d.get("description", ""),
|
|
191
|
+
tags=list(d.get("tags") or []),
|
|
192
|
+
artists=list(d.get("artists") or []),
|
|
193
|
+
requires_box=d.get("requires_box"),
|
|
194
|
+
body=d,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def load_overlays(root: Optional[Path] = None,
|
|
199
|
+
log: Optional[logging.Logger] = None) -> "OverlayIndex":
|
|
200
|
+
"""Scan root for namespace subdirs; load YAMLs; mutate the singleton; return it.
|
|
201
|
+
|
|
202
|
+
Per spec §5.1:
|
|
203
|
+
- Each immediate subdirectory of root is a namespace.
|
|
204
|
+
- Within each namespace, *.yaml/*.yml files are loaded recursively.
|
|
205
|
+
- File may contain a single dict OR a list of dicts.
|
|
206
|
+
- yaml.safe_load ONLY (rejects Python tags).
|
|
207
|
+
- Idempotent: clears the singleton first.
|
|
208
|
+
"""
|
|
209
|
+
log = log or logger
|
|
210
|
+
if root is None:
|
|
211
|
+
root = _resolve_overlay_root()
|
|
212
|
+
|
|
213
|
+
idx = get_overlay_index()
|
|
214
|
+
idx.clear()
|
|
215
|
+
|
|
216
|
+
if not root.exists():
|
|
217
|
+
return idx
|
|
218
|
+
|
|
219
|
+
for ns_dir in sorted(root.iterdir()):
|
|
220
|
+
if not ns_dir.is_dir():
|
|
221
|
+
continue
|
|
222
|
+
namespace = ns_dir.name
|
|
223
|
+
for yaml_path in sorted(list(ns_dir.rglob("*.yaml")) +
|
|
224
|
+
list(ns_dir.rglob("*.yml"))):
|
|
225
|
+
try:
|
|
226
|
+
with yaml_path.open("r") as f:
|
|
227
|
+
parsed = yaml.safe_load(f)
|
|
228
|
+
except yaml.YAMLError as e:
|
|
229
|
+
log.warning(f"overlays: skipped {yaml_path}: {e}")
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if parsed is None:
|
|
233
|
+
continue
|
|
234
|
+
entries = parsed if isinstance(parsed, list) else [parsed]
|
|
235
|
+
for entry_dict in entries:
|
|
236
|
+
if not isinstance(entry_dict, dict):
|
|
237
|
+
log.warning(f"overlays: skipped non-dict entry in {yaml_path}")
|
|
238
|
+
continue
|
|
239
|
+
if not _validate_entry(entry_dict, yaml_path, log):
|
|
240
|
+
continue
|
|
241
|
+
new_entry = _entry_from_dict(entry_dict, namespace)
|
|
242
|
+
previous = idx.add(new_entry)
|
|
243
|
+
if previous is not None:
|
|
244
|
+
log.warning(
|
|
245
|
+
f"overlays: duplicate ({new_entry.namespace}, "
|
|
246
|
+
f"{new_entry.entity_type}, {new_entry.entity_id}) "
|
|
247
|
+
f"in {yaml_path} — last-loaded wins"
|
|
248
|
+
)
|
|
249
|
+
return idx
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# Module-level singleton — initialized empty at import. Per spec §5.1, §6.1.
|
|
253
|
+
# load_overlays() mutates this in place. Tools call get_overlay_index() at
|
|
254
|
+
# request time so they always see current state (never capture a reference).
|
|
255
|
+
_overlay_index: "OverlayIndex" = OverlayIndex()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_overlay_index() -> "OverlayIndex":
|
|
259
|
+
"""Accessor for the live overlay singleton. Always returns the same
|
|
260
|
+
instance — load_overlays() mutates it in place rather than replacing."""
|
|
261
|
+
return _overlay_index
|
|
@@ -588,3 +588,101 @@ def reload_atlas(ctx: Context) -> dict:
|
|
|
588
588
|
"reloaded": True,
|
|
589
589
|
"device_count": atlas.device_count if atlas else 0,
|
|
590
590
|
}
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
594
|
+
# v1.23.0: User-local atlas overlays (extension_atlas_*)
|
|
595
|
+
#
|
|
596
|
+
# These tools surface the OverlayIndex populated by load_overlays() at
|
|
597
|
+
# server boot from ~/.livepilot/atlas-overlays/<namespace>/. Independent
|
|
598
|
+
# of the existing atlas_* tools, which are tightly coupled to the device
|
|
599
|
+
# schema (URIs, packs, categories). Per spec §5.3.
|
|
600
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _serialize_overlay_entry(entry) -> dict:
|
|
604
|
+
"""Serialize an OverlayEntry to a JSON-safe dict for MCP tool returns."""
|
|
605
|
+
return {
|
|
606
|
+
"namespace": entry.namespace,
|
|
607
|
+
"entity_type": entry.entity_type,
|
|
608
|
+
"entity_id": entry.entity_id,
|
|
609
|
+
"name": entry.name,
|
|
610
|
+
"description": entry.description,
|
|
611
|
+
"tags": entry.tags,
|
|
612
|
+
"artists": entry.artists,
|
|
613
|
+
"requires_box": entry.requires_box,
|
|
614
|
+
"body": entry.body,
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@mcp.tool()
|
|
619
|
+
def extension_atlas_search(ctx: Context, query: str,
|
|
620
|
+
namespace: str = "",
|
|
621
|
+
entity_type: str = "",
|
|
622
|
+
limit: int = 10) -> dict:
|
|
623
|
+
"""Search user-local atlas overlays under ~/.livepilot/atlas-overlays/.
|
|
624
|
+
|
|
625
|
+
Use this for content from extension namespaces (e.g., 'elektron', 'prophet') —
|
|
626
|
+
NOT for the main Ableton device atlas (use atlas_search for that).
|
|
627
|
+
|
|
628
|
+
query: case-insensitive substring; matches against entity_id (highest weight),
|
|
629
|
+
name, tags/artists, description (lowest weight).
|
|
630
|
+
namespace: restrict to one namespace (e.g., 'elektron'); empty = search all.
|
|
631
|
+
entity_type: restrict to one entity_type (e.g., 'signature_chain'); empty = all.
|
|
632
|
+
limit: maximum results to return.
|
|
633
|
+
"""
|
|
634
|
+
from .overlays import get_overlay_index
|
|
635
|
+
idx = get_overlay_index()
|
|
636
|
+
ns = namespace or None
|
|
637
|
+
et = entity_type or None
|
|
638
|
+
matches = idx.search(query, namespace=ns, entity_type=et, limit=limit)
|
|
639
|
+
return {
|
|
640
|
+
"query": query,
|
|
641
|
+
"namespace": namespace or None,
|
|
642
|
+
"entity_type": entity_type or None,
|
|
643
|
+
"count": len(matches),
|
|
644
|
+
"results": [_serialize_overlay_entry(e) for e in matches],
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@mcp.tool()
|
|
649
|
+
def extension_atlas_get(ctx: Context, namespace: str, entity_id: str) -> dict:
|
|
650
|
+
"""Fetch a single overlay entry by namespace + entity_id.
|
|
651
|
+
|
|
652
|
+
Returns the full entry including the original YAML body so callers can read
|
|
653
|
+
arbitrary extension-specific fields (architecture, requires_machines,
|
|
654
|
+
requires_firmware, sources, etc.).
|
|
655
|
+
|
|
656
|
+
If the entry has a `requires_firmware` field, surface it to the user before
|
|
657
|
+
recommending the chain (per spec §7) — e.g., "this needs Monomachine OS 1.32+".
|
|
658
|
+
"""
|
|
659
|
+
from .overlays import get_overlay_index
|
|
660
|
+
idx = get_overlay_index()
|
|
661
|
+
entry = idx.get(namespace, entity_id)
|
|
662
|
+
if entry is None:
|
|
663
|
+
return {
|
|
664
|
+
"error": f"entity '{entity_id}' not found in namespace '{namespace}'",
|
|
665
|
+
"suggestion": "Use extension_atlas_search to find available entries, "
|
|
666
|
+
"or extension_atlas_list to see installed namespaces."
|
|
667
|
+
}
|
|
668
|
+
return _serialize_overlay_entry(entry)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
@mcp.tool()
|
|
672
|
+
def extension_atlas_list(ctx: Context, namespace: str = "") -> dict:
|
|
673
|
+
"""Enumerate user-local overlay namespaces and their entity_type counts.
|
|
674
|
+
|
|
675
|
+
With no namespace: returns full list of namespaces and per-type counts.
|
|
676
|
+
With a namespace: returns just the entity_types present in that namespace.
|
|
677
|
+
"""
|
|
678
|
+
from .overlays import get_overlay_index
|
|
679
|
+
idx = get_overlay_index()
|
|
680
|
+
if namespace:
|
|
681
|
+
return {
|
|
682
|
+
"namespace": namespace,
|
|
683
|
+
"entity_types": idx.list_entity_types(namespace),
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
"namespaces": idx.list_namespaces(),
|
|
687
|
+
"counts": idx.stats(),
|
|
688
|
+
}
|
package/mcp_server/server.py
CHANGED
|
@@ -497,6 +497,35 @@ _assert_tool_registry_accessible()
|
|
|
497
497
|
_patch_tool_schemas()
|
|
498
498
|
|
|
499
499
|
|
|
500
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
501
|
+
# v1.23.0: User-local atlas overlay boot hook.
|
|
502
|
+
#
|
|
503
|
+
# Loads YAMLs from ~/.livepilot/atlas-overlays/<namespace>/ into the
|
|
504
|
+
# module-level OverlayIndex singleton. The 3 extension_atlas_* tools
|
|
505
|
+
# registered above resolve the singleton at REQUEST time (via the
|
|
506
|
+
# get_overlay_index() accessor), so this load can happen after their
|
|
507
|
+
# registration without ordering issues.
|
|
508
|
+
#
|
|
509
|
+
# Failures are logged but never abort boot — server starts even if the
|
|
510
|
+
# user has no overlays installed or has malformed YAMLs.
|
|
511
|
+
# Spec: docs/superpowers/specs/2026-04-25-user-local-extensions-design.md §6.1
|
|
512
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
513
|
+
try:
|
|
514
|
+
from .atlas.overlays import load_overlays
|
|
515
|
+
_overlay_idx_at_boot = load_overlays()
|
|
516
|
+
_overlay_count = len(_overlay_idx_at_boot.all_entries())
|
|
517
|
+
if _overlay_count:
|
|
518
|
+
logger.info(
|
|
519
|
+
f"User-local overlays loaded: {_overlay_count} entries across "
|
|
520
|
+
f"namespaces {_overlay_idx_at_boot.list_namespaces()}"
|
|
521
|
+
)
|
|
522
|
+
else:
|
|
523
|
+
logger.debug("User-local overlays: none installed at "
|
|
524
|
+
"~/.livepilot/atlas-overlays/")
|
|
525
|
+
except Exception as e:
|
|
526
|
+
logger.warning(f"User-local overlay load failed (non-fatal, server continues): {e}")
|
|
527
|
+
|
|
528
|
+
|
|
500
529
|
def main():
|
|
501
530
|
"""Run the MCP server over stdio."""
|
|
502
531
|
mcp.run(transport="stdio")
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 — 433 tools, 53 domains, 44 semantic moves. Device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, 12 creative intelligence engines",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -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.
|
|
8
|
+
__version__ = "1.23.0"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "433-tool agentic MCP production system for Ableton Live 12 — 53 domains, 44 semantic moves, device atlas (5264 devices), Splice intelligence (gRPC + GraphQL), 9-band spectral perception auto-loaded, Creative Director skill, technique memory, 12 creative engines",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.23.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.23.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|