livepilot 1.17.0 → 1.17.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 +42 -0
- package/installer/codex.js +14 -0
- 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/sample_engine/tools.py +66 -65
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.17.1 — Splice auto-reconnect + Codex installer fix (April 23 2026)
|
|
4
|
+
|
|
5
|
+
Two bug fixes discovered in a parallel worktree hours after v1.17.0
|
|
6
|
+
shipped. Non-breaking, test-locked, ships as a patch.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Splice client auto-reconnect** (`mcp_server/sample_engine/tools.py`):
|
|
11
|
+
Every Splice MCP tool now reconnects the shared gRPC client on demand
|
|
12
|
+
via a new `_ensure_splice_client_connected()` helper. Before this fix,
|
|
13
|
+
if the Splice desktop app launched AFTER the MCP server (common when
|
|
14
|
+
users start the MCP via Claude Code before booting Splice), every
|
|
15
|
+
Splice tool stayed stuck in a disconnected state until the WHOLE MCP
|
|
16
|
+
server was restarted. The fix re-checks on every tool invocation, so
|
|
17
|
+
the first successful Splice-desktop boot auto-recovers the client
|
|
18
|
+
transparently. Tools affected: `get_splice_credits`,
|
|
19
|
+
`splice_catalog_hunt`, `search_samples` (when routing through Splice),
|
|
20
|
+
plus every other Splice tool that reads from the shared context.
|
|
21
|
+
3 new regression tests in `tests/test_splice_reconnect_tools.py` lock
|
|
22
|
+
the reconnect behavior.
|
|
23
|
+
- **Codex plugin installer writes `.mcp.json`** (`installer/codex.js`):
|
|
24
|
+
The installer was copying plugin files into the Codex plugins
|
|
25
|
+
directory but omitting the `.mcp.json` config that tells Codex how
|
|
26
|
+
to launch the MCP server. Codex users had to manually create the
|
|
27
|
+
file or run the command with additional flags. Now
|
|
28
|
+
`writeLocalMcpConfig(destDir)` writes the correct
|
|
29
|
+
`{mcpServers: {livepilot: {command, args}}}` shape during install.
|
|
30
|
+
1 new regression test in `tests/test_codex_plugin_installer.py`
|
|
31
|
+
asserts the file content.
|
|
32
|
+
|
|
33
|
+
### Verified
|
|
34
|
+
|
|
35
|
+
155/155 tests green. `sync_metadata.py --check`: all metadata in sync
|
|
36
|
+
at version=1.17.1, tools=426, domains=52, bridge_cmds=30, enriched=120.
|
|
37
|
+
|
|
38
|
+
### Distribution
|
|
39
|
+
|
|
40
|
+
Same channels as v1.17.0 — GitHub release + npm + `.mcpb` + plugin
|
|
41
|
+
cache. Tool count and domain count unchanged; this is purely a
|
|
42
|
+
reliability patch.
|
|
43
|
+
|
|
44
|
+
|
|
3
45
|
## 1.17.0 — 2026-04-22 handoff close-out (April 22 2026, late)
|
|
4
46
|
|
|
5
47
|
Closes every item in the 2026-04-22 handoff document: Splice's
|
package/installer/codex.js
CHANGED
|
@@ -42,6 +42,19 @@ function loadManifest() {
|
|
|
42
42
|
return JSON.parse(fs.readFileSync(SOURCE_MANIFEST, "utf-8"));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function writeLocalMcpConfig(destDir) {
|
|
46
|
+
const file = path.join(destDir, ".mcp.json");
|
|
47
|
+
const config = {
|
|
48
|
+
mcpServers: {
|
|
49
|
+
livepilot: {
|
|
50
|
+
command: process.execPath,
|
|
51
|
+
args: [path.join(ROOT, "bin", "livepilot.js")],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
function ensureMarketplace(pluginName) {
|
|
46
59
|
const file = marketplacePath();
|
|
47
60
|
let marketplace = {
|
|
@@ -115,6 +128,7 @@ function installCodexPlugin() {
|
|
|
115
128
|
fs.mkdirSync(path.dirname(destDir), { recursive: true });
|
|
116
129
|
fs.rmSync(destDir, { recursive: true, force: true });
|
|
117
130
|
copyDirSync(SOURCE_DIR, destDir);
|
|
131
|
+
writeLocalMcpConfig(destDir);
|
|
118
132
|
|
|
119
133
|
console.log("Done! Next steps:");
|
|
120
134
|
console.log(" 1. Open or refresh Codex");
|
|
Binary file
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.17.
|
|
2
|
+
__version__ = "1.17.1"
|
|
@@ -259,14 +259,10 @@ async def search_samples(
|
|
|
259
259
|
# Splice search — prefer gRPC online catalog when available, fall back
|
|
260
260
|
# to local SQLite index. See docs/2026-04-14-bugs-discovered.md — P0-2.
|
|
261
261
|
if source in (None, "splice"):
|
|
262
|
-
grpc_client =
|
|
263
|
-
try:
|
|
264
|
-
grpc_client = ctx.lifespan_context.get("splice_client")
|
|
265
|
-
except AttributeError:
|
|
266
|
-
grpc_client = None
|
|
262
|
+
grpc_client = await _ensure_splice_client_connected(ctx)
|
|
267
263
|
|
|
268
264
|
used_grpc = False
|
|
269
|
-
if grpc_client is not None
|
|
265
|
+
if grpc_client is not None:
|
|
270
266
|
try:
|
|
271
267
|
grpc_result = await grpc_client.search_samples(
|
|
272
268
|
query=query,
|
|
@@ -712,6 +708,43 @@ _SPLICE_USER_LIB_DEST = "~/Music/Ableton/User Library/Samples/Splice"
|
|
|
712
708
|
_SPLICE_PREVIEW_CACHE = "~/Library/Caches/LivePilot/splice_previews"
|
|
713
709
|
|
|
714
710
|
|
|
711
|
+
def _get_splice_client_from_context(ctx: Context):
|
|
712
|
+
"""Return the shared Splice client from lifespan context when present."""
|
|
713
|
+
try:
|
|
714
|
+
return ctx.lifespan_context.get("splice_client")
|
|
715
|
+
except AttributeError:
|
|
716
|
+
return None
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
async def _ensure_splice_client_connected(ctx: Context):
|
|
720
|
+
"""Reconnect the shared Splice client on demand.
|
|
721
|
+
|
|
722
|
+
The MCP server creates one long-lived client during startup. If that
|
|
723
|
+
first handshake races or Splice launches later, the old behavior kept
|
|
724
|
+
every tool stuck in a disconnected state until the whole MCP server
|
|
725
|
+
restarted. Re-check here so tool results reflect current desktop state.
|
|
726
|
+
"""
|
|
727
|
+
client = _get_splice_client_from_context(ctx)
|
|
728
|
+
if client is None:
|
|
729
|
+
return None
|
|
730
|
+
if getattr(client, "connected", False):
|
|
731
|
+
return client
|
|
732
|
+
|
|
733
|
+
connect = getattr(client, "connect", None)
|
|
734
|
+
if connect is None:
|
|
735
|
+
return None
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
await connect()
|
|
739
|
+
except Exception as exc:
|
|
740
|
+
logger.debug("Splice reconnect failed: %s", exc)
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
if getattr(client, "connected", False):
|
|
744
|
+
return client
|
|
745
|
+
return None
|
|
746
|
+
|
|
747
|
+
|
|
715
748
|
@mcp.tool()
|
|
716
749
|
async def get_splice_credits(ctx: Context) -> dict:
|
|
717
750
|
"""Get the user's current Splice plan, credits, and daily sample quota.
|
|
@@ -745,15 +778,10 @@ async def get_splice_credits(ctx: Context) -> dict:
|
|
|
745
778
|
from ..splice_client.models import PlanKind
|
|
746
779
|
from ..splice_client.quota import get_tracker
|
|
747
780
|
|
|
748
|
-
client = None
|
|
749
|
-
try:
|
|
750
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
751
|
-
except AttributeError:
|
|
752
|
-
pass
|
|
753
|
-
|
|
754
781
|
quota_summary = get_tracker().summary()
|
|
782
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
755
783
|
|
|
756
|
-
if client is None
|
|
784
|
+
if client is None:
|
|
757
785
|
return {
|
|
758
786
|
"connected": False,
|
|
759
787
|
"username": "",
|
|
@@ -857,13 +885,8 @@ async def splice_catalog_hunt(
|
|
|
857
885
|
Each sample entry contains `file_hash` which you can pass to
|
|
858
886
|
`splice_download_sample` to trigger a download.
|
|
859
887
|
"""
|
|
860
|
-
client =
|
|
861
|
-
|
|
862
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
863
|
-
except AttributeError:
|
|
864
|
-
pass
|
|
865
|
-
|
|
866
|
-
if client is None or not getattr(client, "connected", False):
|
|
888
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
889
|
+
if client is None:
|
|
867
890
|
return {
|
|
868
891
|
"connected": False,
|
|
869
892
|
"error": "Splice gRPC not connected",
|
|
@@ -970,13 +993,8 @@ async def splice_download_sample(
|
|
|
970
993
|
"""
|
|
971
994
|
import shutil
|
|
972
995
|
|
|
973
|
-
client =
|
|
974
|
-
|
|
975
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
976
|
-
except AttributeError:
|
|
977
|
-
pass
|
|
978
|
-
|
|
979
|
-
if client is None or not getattr(client, "connected", False):
|
|
996
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
997
|
+
if client is None:
|
|
980
998
|
return {
|
|
981
999
|
"ok": False,
|
|
982
1000
|
"error": "Splice gRPC not connected",
|
|
@@ -1096,13 +1114,8 @@ async def splice_preview_sample(
|
|
|
1096
1114
|
import urllib.request
|
|
1097
1115
|
import urllib.error
|
|
1098
1116
|
|
|
1099
|
-
client =
|
|
1100
|
-
|
|
1101
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
1102
|
-
except AttributeError:
|
|
1103
|
-
pass
|
|
1104
|
-
|
|
1105
|
-
if client is None or not getattr(client, "connected", False):
|
|
1117
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
1118
|
+
if client is None:
|
|
1106
1119
|
return {"ok": False, "error": "Splice gRPC not connected"}
|
|
1107
1120
|
|
|
1108
1121
|
# Two-stage lookup: SampleInfo is the fast path but only returns
|
|
@@ -1184,14 +1197,10 @@ async def splice_preview_sample(
|
|
|
1184
1197
|
# ────────────────────────────────────────────────────────────────────────
|
|
1185
1198
|
|
|
1186
1199
|
|
|
1187
|
-
def _require_splice_client(ctx: Context) -> tuple[object, Optional[dict]]:
|
|
1200
|
+
async def _require_splice_client(ctx: Context) -> tuple[object, Optional[dict]]:
|
|
1188
1201
|
"""Fetch the Splice client from context, or return an error dict."""
|
|
1189
|
-
client =
|
|
1190
|
-
|
|
1191
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
1192
|
-
except AttributeError:
|
|
1193
|
-
pass
|
|
1194
|
-
if client is None or not getattr(client, "connected", False):
|
|
1202
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
1203
|
+
if client is None:
|
|
1195
1204
|
return None, {"ok": False, "error": "Splice gRPC not connected"}
|
|
1196
1205
|
return client, None
|
|
1197
1206
|
|
|
@@ -1215,7 +1224,7 @@ async def splice_list_collections(
|
|
|
1215
1224
|
],
|
|
1216
1225
|
}
|
|
1217
1226
|
"""
|
|
1218
|
-
client, err = _require_splice_client(ctx)
|
|
1227
|
+
client, err = await _require_splice_client(ctx)
|
|
1219
1228
|
if err:
|
|
1220
1229
|
return err
|
|
1221
1230
|
total, collections = await client.list_collections(
|
|
@@ -1244,7 +1253,7 @@ async def splice_search_in_collection(
|
|
|
1244
1253
|
`splice_catalog_hunt` — you can feed them straight into
|
|
1245
1254
|
`splice_preview_sample` or `splice_download_sample`.
|
|
1246
1255
|
"""
|
|
1247
|
-
client, err = _require_splice_client(ctx)
|
|
1256
|
+
client, err = await _require_splice_client(ctx)
|
|
1248
1257
|
if err:
|
|
1249
1258
|
return err
|
|
1250
1259
|
total, samples = await client.collection_samples(
|
|
@@ -1271,7 +1280,7 @@ async def splice_add_to_collection(
|
|
|
1271
1280
|
and web UI immediately. Use this to let LivePilot "save for later"
|
|
1272
1281
|
items it finds during composition work.
|
|
1273
1282
|
"""
|
|
1274
|
-
client, err = _require_splice_client(ctx)
|
|
1283
|
+
client, err = await _require_splice_client(ctx)
|
|
1275
1284
|
if err:
|
|
1276
1285
|
return err
|
|
1277
1286
|
if not file_hashes:
|
|
@@ -1289,7 +1298,7 @@ async def splice_remove_from_collection(
|
|
|
1289
1298
|
ctx: Context, collection_uuid: str, file_hashes: list[str],
|
|
1290
1299
|
) -> dict:
|
|
1291
1300
|
"""Remove one or more samples from a user Collection (server-side)."""
|
|
1292
|
-
client, err = _require_splice_client(ctx)
|
|
1301
|
+
client, err = await _require_splice_client(ctx)
|
|
1293
1302
|
if err:
|
|
1294
1303
|
return err
|
|
1295
1304
|
if not file_hashes:
|
|
@@ -1307,7 +1316,7 @@ async def splice_remove_from_collection(
|
|
|
1307
1316
|
@mcp.tool()
|
|
1308
1317
|
async def splice_create_collection(ctx: Context, name: str) -> dict:
|
|
1309
1318
|
"""Create a new user Collection. Returns the new UUID on success."""
|
|
1310
|
-
client, err = _require_splice_client(ctx)
|
|
1319
|
+
client, err = await _require_splice_client(ctx)
|
|
1311
1320
|
if err:
|
|
1312
1321
|
return err
|
|
1313
1322
|
name = (name or "").strip()
|
|
@@ -1347,7 +1356,7 @@ async def splice_list_presets(
|
|
|
1347
1356
|
],
|
|
1348
1357
|
}
|
|
1349
1358
|
"""
|
|
1350
|
-
client, err = _require_splice_client(ctx)
|
|
1359
|
+
client, err = await _require_splice_client(ctx)
|
|
1351
1360
|
if err:
|
|
1352
1361
|
return err
|
|
1353
1362
|
total, presets = await client.list_purchased_presets(
|
|
@@ -1371,7 +1380,7 @@ async def splice_preset_info(
|
|
|
1371
1380
|
plugin_name: str = "",
|
|
1372
1381
|
) -> dict:
|
|
1373
1382
|
"""Fetch metadata for a single preset (uuid, file_hash, or plugin_name)."""
|
|
1374
|
-
client, err = _require_splice_client(ctx)
|
|
1383
|
+
client, err = await _require_splice_client(ctx)
|
|
1375
1384
|
if err:
|
|
1376
1385
|
return err
|
|
1377
1386
|
if not (uuid or file_hash or plugin_name):
|
|
@@ -1394,7 +1403,7 @@ async def splice_download_preset(ctx: Context, uuid: str) -> dict:
|
|
|
1394
1403
|
"""
|
|
1395
1404
|
from ..splice_client.client import CREDIT_HARD_FLOOR
|
|
1396
1405
|
|
|
1397
|
-
client, err = _require_splice_client(ctx)
|
|
1406
|
+
client, err = await _require_splice_client(ctx)
|
|
1398
1407
|
if err:
|
|
1399
1408
|
return err
|
|
1400
1409
|
if not uuid:
|
|
@@ -1437,7 +1446,7 @@ async def splice_pack_info(ctx: Context, pack_uuid: str) -> dict:
|
|
|
1437
1446
|
Useful for discovering related samples by pack, or surfacing pack-level
|
|
1438
1447
|
genre/provider info that search results omit.
|
|
1439
1448
|
"""
|
|
1440
|
-
client, err = _require_splice_client(ctx)
|
|
1449
|
+
client, err = await _require_splice_client(ctx)
|
|
1441
1450
|
if err:
|
|
1442
1451
|
return err
|
|
1443
1452
|
if not pack_uuid:
|
|
@@ -1469,19 +1478,15 @@ async def splice_pack_info(ctx: Context, pack_uuid: str) -> dict:
|
|
|
1469
1478
|
# ────────────────────────────────────────────────────────────────────────
|
|
1470
1479
|
|
|
1471
1480
|
|
|
1472
|
-
def _build_http_bridge(ctx: Context):
|
|
1481
|
+
async def _build_http_bridge(ctx: Context):
|
|
1473
1482
|
"""Construct the HTTPS bridge with the current gRPC client attached.
|
|
1474
1483
|
|
|
1475
1484
|
Returns (bridge, err_dict). On success err_dict is None.
|
|
1476
1485
|
"""
|
|
1477
1486
|
from ..splice_client.http_bridge import SpliceHTTPBridge
|
|
1478
1487
|
|
|
1479
|
-
client =
|
|
1480
|
-
|
|
1481
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
1482
|
-
except AttributeError:
|
|
1483
|
-
pass
|
|
1484
|
-
if client is None or not getattr(client, "connected", False):
|
|
1488
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
1489
|
+
if client is None:
|
|
1485
1490
|
return None, {
|
|
1486
1491
|
"ok": False,
|
|
1487
1492
|
"error": "Splice gRPC not connected — session token unreachable",
|
|
@@ -1520,7 +1525,7 @@ async def splice_describe_sound(
|
|
|
1520
1525
|
instrument/tags/pack_name/files. Use the uuid with
|
|
1521
1526
|
`splice_download_sample(uuid)` to pull the audio file.
|
|
1522
1527
|
"""
|
|
1523
|
-
bridge, err = _build_http_bridge(ctx)
|
|
1528
|
+
bridge, err = await _build_http_bridge(ctx)
|
|
1524
1529
|
if err:
|
|
1525
1530
|
return err
|
|
1526
1531
|
from ..splice_client.http_bridge import SpliceHTTPError
|
|
@@ -1572,7 +1577,7 @@ async def splice_generate_variation(
|
|
|
1572
1577
|
duration/tags/pack_name/files). Use the uuid of any result with
|
|
1573
1578
|
`splice_download_sample()` to pull the audio.
|
|
1574
1579
|
"""
|
|
1575
|
-
bridge, err = _build_http_bridge(ctx)
|
|
1580
|
+
bridge, err = await _build_http_bridge(ctx)
|
|
1576
1581
|
if err:
|
|
1577
1582
|
return err
|
|
1578
1583
|
from ..splice_client.http_bridge import SpliceHTTPError
|
|
@@ -1631,12 +1636,8 @@ async def splice_http_diagnose(ctx: Context) -> dict:
|
|
|
1631
1636
|
# diagnostic is worse than no diagnostic.
|
|
1632
1637
|
session_token_available = False
|
|
1633
1638
|
session_token_error = None
|
|
1634
|
-
grpc_client =
|
|
1635
|
-
|
|
1636
|
-
grpc_client = ctx.lifespan_context.get("splice_client")
|
|
1637
|
-
except AttributeError:
|
|
1638
|
-
pass
|
|
1639
|
-
if grpc_client is None or not getattr(grpc_client, "connected", False):
|
|
1639
|
+
grpc_client = await _ensure_splice_client_connected(ctx)
|
|
1640
|
+
if grpc_client is None:
|
|
1640
1641
|
session_token_error = "Splice gRPC not connected"
|
|
1641
1642
|
else:
|
|
1642
1643
|
# Connection is up; confirm a token actually comes back.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.1",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
5
|
"description": "Agentic production system for Ableton Live 12 — 426 tools, 52 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
6
6
|
"author": "Pilot Studio",
|
|
@@ -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.17.
|
|
8
|
+
__version__ = "1.17.1"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.17.
|
|
9
|
+
"version": "1.17.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.17.
|
|
14
|
+
"version": "1.17.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|