livepilot 1.22.1 → 1.23.1

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 CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.23.1 — 2026-04-25
4
+
5
+ ### Fixed
6
+ - `extension_atlas_search` multi-word queries returned 0 hits because the search did literal-substring matching. `"sophie ponyboy"` failed to match `"SOPHIE — Ponyboy kick"` because of the em-dash separator. Now: query is tokenized on whitespace, each token must match somewhere (AND semantics), per-token scores sum for ranking. Single-token queries collapse to the original behavior — fully backwards-compatible.
7
+
8
+ ### Tests
9
+ - 3 new tests covering multi-token AND, em-dash separator handling, per-token score aggregation.
10
+
11
+ ## v1.23.0 — 2026-04-25
12
+
13
+ ### Added
14
+ - **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."
15
+ - 3 new MCP tools (430 → 433):
16
+ - `extension_atlas_search(query, namespace?, entity_type?, limit?)` — weighted substring search across overlay entries
17
+ - `extension_atlas_get(namespace, entity_id)` — fetch a single overlay entry with full body (including `requires_firmware` if present)
18
+ - `extension_atlas_list(namespace?)` — enumerate namespaces + entity_type counts
19
+ - New file: `mcp_server/atlas/overlays.py` — `OverlayEntry` dataclass, `OverlayIndex` class, `load_overlays()`, lazy path resolver, module-level singleton.
20
+ - New doc: `docs/EXTENSION_API.md` — public API contract for extension authors.
21
+
22
+ ### Behavior
23
+ - Atlas overlays load at server boot from `~/.livepilot/atlas-overlays/`. Non-fatal — server continues if missing or malformed.
24
+ - 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.
25
+ - For `entity_type: signature_chain`, `tags` and `artists` are required (search ranker depends on them). Other entity types treat these as optional.
26
+ - `entity_id` and `entity_type` are str-coerced to defend against YAML scalar values like `entity_id: 42`.
27
+
28
+ ### Notes for extension authors
29
+ - The contract (`OverlayEntry` field names, `extension_atlas_*` tool API) is stable from v1.23.0 forward.
30
+ - Tool-name collisions: FastMCP enforces first-registered-wins. Bundled tools always beat extensions.
31
+ - Phase 2 (user-local Python extensions via `register(mcp)`) lands in a future minor version.
32
+
33
+ Spec: `docs/superpowers/specs/2026-04-25-user-local-extensions-design.md` (gitignored, design-time artifact)
34
+ Plan: `docs/superpowers/plans/2026-04-25-phase-1a-atlas-overlays-plan.md` (gitignored, implementation tracker)
35
+
3
36
  ## 1.22.1 — Bundled enrichment coverage gate (April 25 2026)
4
37
 
5
38
  Closes the one item carried from v1.22.0's atlas-separation work: a
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
- 430 tools. 53 domains. Device atlas. Plan-aware Splice integration. Auto-composition. Spectral perception. Technique memory. Drum-rack pad builder. Live dead-device detection.
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
- │ │ 430 MCP Tools │ │
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 430 tools. They give the AI musical judgment, not just musical execution.
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
- 430 tools across 53 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
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 — 10 tools
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 430 tools →](docs/manual/tool-catalog.md)**
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 430 tools, workflows |
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.21.3";
37
+ var VERSION = "1.23.0";
38
38
 
39
39
  // ── State ──────────────────────────────────────────────────────────────────
40
40
 
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.22.1"
2
+ __version__ = "1.23.1"
@@ -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,282 @@
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 with whitespace-tokenized AND semantics.
95
+
96
+ The query is split on whitespace into tokens. Each token is scored
97
+ against each entry independently:
98
+ +1000 if token == entity_id (case-insensitive exact match)
99
+ +100 per substring hit in name
100
+ +50 per substring hit in tag or artist
101
+ +10 per substring hit in description
102
+
103
+ An entry matches only if EVERY token scores > 0 somewhere (AND
104
+ semantics — prevents 'sophie ponyboy' from matching unrelated
105
+ entries that contain only one of the two words). The entry's
106
+ final score is the sum across all tokens.
107
+
108
+ Sorts by descending score, then by entity_id for stable ties.
109
+ Filters by namespace and/or entity_type if provided.
110
+ Empty query returns empty list.
111
+ """
112
+ q = (query or "").strip().lower()
113
+ if not q:
114
+ return []
115
+ tokens = q.split()
116
+ if not tokens:
117
+ return []
118
+
119
+ scored: list[tuple[int, str, OverlayEntry]] = []
120
+ for entry in self._entries.values():
121
+ if namespace is not None and entry.namespace != namespace:
122
+ continue
123
+ if entity_type is not None and entry.entity_type != entity_type:
124
+ continue
125
+
126
+ name_lower = entry.name.lower()
127
+ entity_id_lower = entry.entity_id.lower()
128
+ description_lower = entry.description.lower()
129
+ tags_lower = [str(t).lower() for t in entry.tags]
130
+ artists_lower = [str(a).lower() for a in entry.artists]
131
+
132
+ token_scores = []
133
+ for tok in tokens:
134
+ s = 0
135
+ if entity_id_lower == tok:
136
+ s += 1000
137
+ if tok in name_lower:
138
+ s += 100
139
+ for tag in tags_lower:
140
+ if tok in tag:
141
+ s += 50
142
+ for artist in artists_lower:
143
+ if tok in artist:
144
+ s += 50
145
+ if tok in description_lower:
146
+ s += 10
147
+ token_scores.append(s)
148
+
149
+ # AND semantics — every token must match somewhere
150
+ if all(s > 0 for s in token_scores):
151
+ scored.append((sum(token_scores), entry.entity_id, entry))
152
+
153
+ scored.sort(key=lambda triple: (-triple[0], triple[1]))
154
+ return [entry for (_, _, entry) in scored[:max(0, limit)]]
155
+
156
+ def stats(self) -> dict:
157
+ """Counts per namespace per entity_type (used by extension_atlas_list in Task 12)."""
158
+ counts: dict[str, dict[str, int]] = {}
159
+ for (ns, et, _eid) in self._entries.keys():
160
+ counts.setdefault(ns, {}).setdefault(et, 0)
161
+ counts[ns][et] += 1
162
+ return counts
163
+
164
+
165
+ def _resolve_overlay_root() -> Path:
166
+ """Lazy resolver mirroring v1.22.0 _resolve_atlas_path() pattern.
167
+ Tests monkeypatch Path.home() and expect this to re-evaluate."""
168
+ return Path.home() / ".livepilot" / "atlas-overlays"
169
+
170
+
171
+ def _validate_entry(entry: dict, source_path: Path,
172
+ log: logging.Logger) -> bool:
173
+ """True if entry has required fields. Log + return False otherwise.
174
+
175
+ Per spec §5.1:
176
+ - All entries: entity_id + entity_type required
177
+ - entity_type=signature_chain: also tags + artists required
178
+ """
179
+ eid = entry.get("entity_id")
180
+ etype = entry.get("entity_type")
181
+ if not eid:
182
+ log.warning(f"overlays: skipped entry in {source_path}: missing 'entity_id'")
183
+ return False
184
+ if not etype:
185
+ log.warning(f"overlays: skipped {eid} in {source_path}: missing 'entity_type'")
186
+ return False
187
+ if etype == "signature_chain":
188
+ if not entry.get("tags"):
189
+ log.warning(f"overlays: skipped {eid} in {source_path}: signature_chain requires 'tags'")
190
+ return False
191
+ if not entry.get("artists"):
192
+ log.warning(f"overlays: skipped {eid} in {source_path}: signature_chain requires 'artists'")
193
+ return False
194
+ return True
195
+
196
+
197
+ def _entry_from_dict(d: dict, namespace: str) -> OverlayEntry:
198
+ """Build an OverlayEntry from a validated YAML dict.
199
+ The full original dict is preserved as `body` so callers can read
200
+ arbitrary extra fields (architecture, requires_machines, sources, etc.).
201
+
202
+ Coerces entity_id and entity_type to str() defensively — guards against
203
+ YAMLs that use non-string scalar values (e.g., `entity_id: 42`) which
204
+ would otherwise break downstream search() (.lower() on int).
205
+ """
206
+ return OverlayEntry(
207
+ namespace=namespace,
208
+ entity_type=str(d["entity_type"]),
209
+ entity_id=str(d["entity_id"]),
210
+ name=d.get("name", ""),
211
+ description=d.get("description", ""),
212
+ tags=list(d.get("tags") or []),
213
+ artists=list(d.get("artists") or []),
214
+ requires_box=d.get("requires_box"),
215
+ body=d,
216
+ )
217
+
218
+
219
+ def load_overlays(root: Optional[Path] = None,
220
+ log: Optional[logging.Logger] = None) -> "OverlayIndex":
221
+ """Scan root for namespace subdirs; load YAMLs; mutate the singleton; return it.
222
+
223
+ Per spec §5.1:
224
+ - Each immediate subdirectory of root is a namespace.
225
+ - Within each namespace, *.yaml/*.yml files are loaded recursively.
226
+ - File may contain a single dict OR a list of dicts.
227
+ - yaml.safe_load ONLY (rejects Python tags).
228
+ - Idempotent: clears the singleton first.
229
+ """
230
+ log = log or logger
231
+ if root is None:
232
+ root = _resolve_overlay_root()
233
+
234
+ idx = get_overlay_index()
235
+ idx.clear()
236
+
237
+ if not root.exists():
238
+ return idx
239
+
240
+ for ns_dir in sorted(root.iterdir()):
241
+ if not ns_dir.is_dir():
242
+ continue
243
+ namespace = ns_dir.name
244
+ for yaml_path in sorted(list(ns_dir.rglob("*.yaml")) +
245
+ list(ns_dir.rglob("*.yml"))):
246
+ try:
247
+ with yaml_path.open("r") as f:
248
+ parsed = yaml.safe_load(f)
249
+ except yaml.YAMLError as e:
250
+ log.warning(f"overlays: skipped {yaml_path}: {e}")
251
+ continue
252
+
253
+ if parsed is None:
254
+ continue
255
+ entries = parsed if isinstance(parsed, list) else [parsed]
256
+ for entry_dict in entries:
257
+ if not isinstance(entry_dict, dict):
258
+ log.warning(f"overlays: skipped non-dict entry in {yaml_path}")
259
+ continue
260
+ if not _validate_entry(entry_dict, yaml_path, log):
261
+ continue
262
+ new_entry = _entry_from_dict(entry_dict, namespace)
263
+ previous = idx.add(new_entry)
264
+ if previous is not None:
265
+ log.warning(
266
+ f"overlays: duplicate ({new_entry.namespace}, "
267
+ f"{new_entry.entity_type}, {new_entry.entity_id}) "
268
+ f"in {yaml_path} — last-loaded wins"
269
+ )
270
+ return idx
271
+
272
+
273
+ # Module-level singleton — initialized empty at import. Per spec §5.1, §6.1.
274
+ # load_overlays() mutates this in place. Tools call get_overlay_index() at
275
+ # request time so they always see current state (never capture a reference).
276
+ _overlay_index: "OverlayIndex" = OverlayIndex()
277
+
278
+
279
+ def get_overlay_index() -> "OverlayIndex":
280
+ """Accessor for the live overlay singleton. Always returns the same
281
+ instance — load_overlays() mutates it in place rather than replacing."""
282
+ 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
+ }
@@ -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.22.1",
3
+ "version": "1.23.1",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 430 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",
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.22.1"
8
+ __version__ = "1.23.1"
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": "430-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",
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.22.1",
9
+ "version": "1.23.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "livepilot",
14
- "version": "1.22.1",
14
+ "version": "1.23.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }