livepilot 1.9.11 → 1.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +57 -0
  4. package/README.md +11 -10
  5. package/bin/livepilot.js +27 -9
  6. package/livepilot/.Codex-plugin/plugin.json +1 -1
  7. package/livepilot/.claude-plugin/plugin.json +1 -1
  8. package/livepilot/skills/livepilot-core/SKILL.md +6 -6
  9. package/livepilot/skills/livepilot-core/references/overview.md +1 -1
  10. package/livepilot/skills/livepilot-release/SKILL.md +4 -4
  11. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  12. package/m4l_device/livepilot_bridge.js +77 -3
  13. package/mcp_server/__init__.py +1 -1
  14. package/mcp_server/connection.py +5 -1
  15. package/mcp_server/curves.py +5 -1
  16. package/mcp_server/m4l_bridge.py +37 -20
  17. package/mcp_server/memory/technique_store.py +30 -2
  18. package/mcp_server/server.py +10 -2
  19. package/mcp_server/tools/analyzer.py +12 -1
  20. package/mcp_server/tools/arrangement.py +20 -5
  21. package/mcp_server/tools/automation.py +52 -0
  22. package/mcp_server/tools/devices.py +2 -3
  23. package/mcp_server/tools/generative.py +4 -0
  24. package/mcp_server/tools/midi_io.py +22 -4
  25. package/mcp_server/tools/theory.py +8 -1
  26. package/mcp_server/tools/transport.py +16 -6
  27. package/package.json +1 -1
  28. package/remote_script/LivePilot/__init__.py +3 -3
  29. package/remote_script/LivePilot/arrangement.py +12 -18
  30. package/remote_script/LivePilot/browser.py +19 -8
  31. package/remote_script/LivePilot/clip_automation.py +5 -4
  32. package/remote_script/LivePilot/clips.py +14 -6
  33. package/remote_script/LivePilot/devices.py +6 -3
  34. package/remote_script/LivePilot/server.py +20 -7
  35. package/remote_script/LivePilot/tracks.py +7 -2
  36. package/remote_script/LivePilot/transport.py +3 -3
  37. package/requirements.txt +6 -1
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "livepilot",
12
12
  "description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
13
- "version": "1.9.11",
13
+ "version": "1.9.13",
14
14
  "author": {
15
15
  "name": "Pilot Studio"
16
16
  },
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # LivePilot v1.9.11 — Ableton Live 12
1
+ # LivePilot v1.9.13 — Ableton Live 12
2
2
 
3
3
  ## Project
4
4
  - **Repo:** This directory (LivePilot)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.13 — Security Hardening + Startup Safety (April 2026)
4
+
5
+ - Fix(P2): `--setup-flucoma` now pins to a known release tag (v1.0.7) instead of unpinned `latest`, prints SHA256 checksum for verification, and selects the platform-specific zip
6
+ - Fix(P2): memory subsystem now uses lazy initialization — `TechniqueStore` defers directory creation to first tool call instead of blocking server startup when HOME is read-only
7
+ - Fix(P3): `--status` and `--doctor` now return exit 0 when Ableton is reachable but another client is connected (STATE_ERROR = reachable, not failure)
8
+ - Fix(P3): negative `limit` values on `memory_recall` and `memory_list` now raise `ValueError` instead of using Python negative slicing
9
+ - Fix: Remote Script no longer logs "Server started" before bind succeeds — "Listening on..." is logged from the server loop after successful bind
10
+ - Fix: `requirements.txt` now documents dev dependencies (pytest, pytest-asyncio) as comments
11
+ - Verification: 145 tests passing, 178 tools confirmed
12
+
13
+ ## 1.9.12 — Deep Audit: 21 Fixes Across 15 Files (April 2026)
14
+
15
+ **Full codebase audit — 5 critical, 10 important, 6 doc/test fixes.**
16
+
17
+ ### Critical Fixes
18
+ - Fix(P1): `capture_stop` no longer deadlocks — `cancel_capture_future` removed lock acquisition that blocked behind `send_capture`
19
+ - Fix(P1): `import_midi_to_clip` now distinguishes empty-slot NOT_FOUND from INDEX_ERROR/TIMEOUT instead of swallowing all AbletonConnectionErrors
20
+ - Fix(P1): capture audio files now write to `~/Documents/LivePilot/captures/` (stable path) instead of beside the .amxd preset
21
+ - Fix(P1): `check_flucoma` now uses `Folder.end` to detect FluCoMa — `typelist` check was always true
22
+ - Fix(P1): CI workflow updated to `actions/checkout@v4` + `actions/setup-python@v5` (v6 doesn't exist)
23
+
24
+ ### Safety & Validation
25
+ - Fix(P2): 5 automation tools now validate `track_index >= 0` and `clip_index >= 0` (matching all peer modules)
26
+ - Fix(P2): `cmd_stop_scrub` now checks `cursor_a.id === 0` for empty clip slots (matching all peer bridge functions)
27
+ - Fix(P2): `cmd_get_selected` now resolves return tracks (negative indices) and master track (-1000)
28
+ - Fix(P2): `duplicate_track` uses count-before/after delta for correct group track duplication index
29
+ - Fix(P2): `create_arrangement_clip` locates first clip by `start_time` instead of stale index after trim pass
30
+ - Fix(P2): `get_session_info` reuses already-built lists instead of re-iterating `song.tracks`/`song.scenes`
31
+ - Fix(P2): client disconnect race — socket now closes before clearing `_client_connected` flag
32
+
33
+ ### Tests
34
+ - Fix: transport validation tests now import production `_validate_tempo`/`_validate_time_signature` instead of testing local copies
35
+ - Fix: added `load_sample_to_simpler` to analyzer tool contract (was 28/29)
36
+ - Fix: removed duplicate `test_release_quick_verify_checks_both_plugin_manifests`
37
+ - New: 5 automation negative tests (index validation, parameter_type validation)
38
+
39
+ ### Documentation
40
+ - Fix: `docs/manual/index.md` domain map — Tracks 14→17, Devices 12→15, Scenes 8→12
41
+ - Fix: README perception split — 145+33 → 149+29 (actual analyzer tool count is 29)
42
+ - Fix: M4L_BRIDGE.md command count — 22→28 (6 commands undocumented)
43
+ - Fix: tool-reference.md MIDI docs — `export_clip_midi` and `import_midi_to_clip` parameter tables matched to actual signatures
44
+
45
+ ### Deferred (documented, low-impact)
46
+ - Timed-out commands still execute on main thread (needs cancellation token redesign)
47
+ - Chunked UDP reassembly fragile on packet loss (loopback mitigates)
48
+ - Diatonic transpose octave correction edge case (needs musical test suite)
49
+ - `cmd_map_plugin_param` reports false success (LiveAPI lacks Configure mapping API)
50
+
51
+ Verification: 145 tests passing (non-fastmcp), 178 tools confirmed, 15 files changed
52
+
3
53
  ## 1.9.11 — Session Diagnostics + Client Conflict Clarity (March 2026)
4
54
 
5
55
  **Live-tested against the open Ableton set after reloading the updated Remote Script.**
@@ -11,6 +61,13 @@
11
61
  - Fix(P2): `--status` and TCP timeout paths now explain when another LivePilot client appears to be connected instead of only reporting a generic timeout
12
62
  - Docs: beat/sounddesign/core skill guidance now includes device-health checks, sample-dependent plugin cautions, and pitch-audit discipline from the live stress-test sessions
13
63
  - Verification: `292 passed`, `npm pack --dry-run` passed, live set diagnostics succeeded, analyzer bridge streamed on the master track, and conflict reproduction now reports the competing client PID
64
+ - Fix(P1): brownian automation curve reflection loop now has 100-iteration guard with hard clamp fallback — high volatility values could previously hang the server
65
+ - Fix(P1): schema coercion now recurses into array `items` so `list[float]` params benefit from string-to-number widening for MCP clients that serialize as strings
66
+ - Fix(P1): `apply_automation_shape` and `apply_automation_recipe` now validate `parameter_type` and required companion params before sending to Ableton
67
+ - Fix(P2): Remote Script `AssertionError` fallbacks now return STATE_ERROR instead of running LOM calls on the TCP thread during ControlSurface disconnection
68
+ - Fix(P2): M4L bridge ping version corrected to 1.9.11; `check_flucoma` now probes disk for FluCoMa package instead of returning hardcoded `true`
69
+ - Verification: deep audit across 45+ files (3 parallel agents), 292 unit tests + 15 live integration tests against Ableton session, all passing
70
+
14
71
  ## 1.9.10 — Analyzer Capture Finalization + Release Sync (March 2026)
15
72
 
16
73
  **Live-tested in Ableton after a full analyzer rebuild and master-track validation.**
package/README.md CHANGED
@@ -1,10 +1,11 @@
1
- <p align="center">
2
- <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/banner-dark.svg">
4
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/banner-light.svg">
5
- <img alt="LivePilot" src="docs/assets/banner-light.svg" width="600">
6
- </picture>
7
- </p>
1
+ ```
2
+ ██╗ ██╗██╗ ██╗███████╗██████╗ ██╗██╗ ██████╗ ████████╗
3
+ ██║ ██║██║ ██║██╔════╝██╔══██╗██║██║ ██╔═══██╗╚══██╔══╝
4
+ ██║ ██║██║ ██║█████╗ ██████╔╝██║██║ ██║ ██║ ██║
5
+ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔═══╝ ██║██║ ██║ ██║ ██║
6
+ ███████╗██║ ╚████╔╝ ███████╗██║ ██║███████╗╚██████╔╝ ██║
7
+ ╚══════╝╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝
8
+ ```
8
9
 
9
10
  <p align="center">
10
11
  <a href="https://github.com/dreamrec/LivePilot/actions"><img src="https://img.shields.io/github/actions/workflow/status/dreamrec/LivePilot/ci.yml?style=flat-square&label=CI" alt="CI"></a>
@@ -90,7 +91,7 @@ All three feed into 178 deterministic tools that execute on Ableton's main threa
90
91
  | Tracks | 17 | create MIDI/audio/return, delete, duplicate, arm, mute, solo, color, freeze, flatten |
91
92
  | Clips | 11 | create, delete, duplicate, fire, stop, loop, launch mode, warp mode, quantize |
92
93
  | Notes | 8 | add/get/remove/modify MIDI notes, transpose, duplicate, per-note probability |
93
- | Devices | 12 | load by name or URI, get/set parameters, batch edit, racks, chains, presets |
94
+ | Devices | 15 | load by name or URI, get/set parameters, batch edit, racks, chains, presets, plugin deep control |
94
95
  | Scenes | 12 | create, delete, duplicate, fire, name, color, tempo, scene matrix |
95
96
  | Browser | 4 | search library, browse tree, load items, filter by category |
96
97
  | Mixing | 11 | volume, pan, sends, routing, meters, return tracks, master, full mix snapshot |
@@ -98,13 +99,13 @@ All three feed into 178 deterministic tools that execute on Ableton's main threa
98
99
 
99
100
  <br>
100
101
 
101
- ### Perception — 32 tools `[M4L]`
102
+ ### Perception — 29 tools `[M4L]`
102
103
 
103
104
  The M4L Analyzer sits on the master track. UDP 9880 carries spectral data
104
105
  from Max to the server. OSC 9881 sends commands back.
105
106
 
106
107
  > [!TIP]
107
- > All 146 core tools work without the analyzer — it adds 32 more and closes the feedback loop.
108
+ > All 149 core tools work without the analyzer — it adds 29 more and closes the feedback loop.
108
109
 
109
110
  ```
110
111
  SPECTRAL ─────── 8-band frequency decomposition (sub → air)
package/bin/livepilot.js CHANGED
@@ -95,7 +95,7 @@ function ensureVenv(systemPython, prefixArgs) {
95
95
  // Check if venv already exists and has our deps
96
96
  if (fs.existsSync(venvPy)) {
97
97
  try {
98
- execFileSync(venvPy, ["-c", "import fastmcp; import midiutil; import pretty_midi; import numpy; import pyloudnorm; import soundfile; import scipy"], {
98
+ execFileSync(venvPy, ["-c", "import fastmcp; import midiutil; import pretty_midi; import numpy; import pyloudnorm; import soundfile; import scipy; import mutagen"], {
99
99
  encoding: "utf-8",
100
100
  timeout: 10000,
101
101
  stdio: "pipe",
@@ -159,12 +159,16 @@ function checkStatus() {
159
159
  console.log(" Ableton Live: connected on %s:%d", HOST, PORT);
160
160
  ok = true;
161
161
  } else if (resp.ok === false && resp.error && resp.error.code === "STATE_ERROR") {
162
+ // Ableton IS reachable — it just has another client connected.
163
+ // Report as reachable (exit 0) so --status and --doctor don't
164
+ // falsely report failure in a healthy single-client deployment.
162
165
  console.log(
163
- " Ableton Live: reachable, but another LivePilot client is already connected"
166
+ " Ableton Live: reachable on %s:%d (another LivePilot client is connected)", HOST, PORT
164
167
  );
165
168
  if (resp.error.message) {
166
169
  console.log(" Detail: %s", resp.error.message);
167
170
  }
171
+ ok = true;
168
172
  } else {
169
173
  console.log(" Ableton Live: unexpected response:", JSON.stringify(resp));
170
174
  }
@@ -346,10 +350,15 @@ async function setupFlucoma() {
346
350
  }
347
351
 
348
352
  console.log("FluCoMa not found. Downloading from GitHub...");
353
+ const crypto = require("crypto");
354
+
355
+ // Pin to a known release tag for reproducibility
356
+ const FLUCOMA_TAG = "1.0.7";
357
+ const FLUCOMA_URL = `https://api.github.com/repos/flucoma/flucoma-max/releases/tags/${FLUCOMA_TAG}`;
349
358
 
350
- // Fetch latest release info
359
+ // Fetch pinned release info
351
360
  const releaseInfo = await new Promise((resolve, reject) => {
352
- https.get("https://api.github.com/repos/flucoma/flucoma-max/releases/latest", {
361
+ https.get(FLUCOMA_URL, {
353
362
  headers: { "User-Agent": "LivePilot" }
354
363
  }, (res) => {
355
364
  if (res.statusCode === 302 || res.statusCode === 301) {
@@ -368,13 +377,14 @@ async function setupFlucoma() {
368
377
  }).on("error", reject);
369
378
  });
370
379
 
371
- const zipAsset = releaseInfo.assets.find(a => a.name.endsWith(".zip"));
380
+ const platform = process.platform === "darwin" ? "Mac" : "Windows";
381
+ const zipAsset = releaseInfo.assets.find(a => a.name.endsWith(".zip") && a.name.includes(platform));
372
382
  if (!zipAsset) {
373
- console.error("Error: no zip asset found in FluCoMa release");
383
+ console.error("Error: no %s zip asset found in FluCoMa release %s", platform, FLUCOMA_TAG);
374
384
  process.exit(1);
375
385
  }
376
386
 
377
- console.log("Downloading %s (%sMB)...", zipAsset.name,
387
+ console.log("Downloading %s (v%s, %sMB)...", zipAsset.name, FLUCOMA_TAG,
378
388
  Math.round(zipAsset.size / 1024 / 1024));
379
389
 
380
390
  // Download to temp
@@ -399,6 +409,13 @@ async function setupFlucoma() {
399
409
  download(downloadUrl, 0);
400
410
  });
401
411
 
412
+ // Verify download integrity via SHA256 of the zip file
413
+ const hash = crypto.createHash("sha256");
414
+ hash.update(fs.readFileSync(zipPath));
415
+ const sha256 = hash.digest("hex");
416
+ console.log("SHA256: %s", sha256);
417
+ console.log("Verify this matches the checksum on https://github.com/flucoma/flucoma-max/releases/tag/%s", FLUCOMA_TAG);
418
+
402
419
  console.log("Extracting to %s...", packagesDir);
403
420
  fs.mkdirSync(packagesDir, { recursive: true });
404
421
 
@@ -417,8 +434,9 @@ async function setupFlucoma() {
417
434
  });
418
435
  }
419
436
 
420
- // macOS: strip quarantine
437
+ // macOS: strip quarantine on FluCoMa externals only (not on arbitrary paths)
421
438
  if (process.platform === "darwin" && fs.existsSync(flucomaDir)) {
439
+ console.log("Removing macOS quarantine from FluCoMa externals...");
422
440
  try {
423
441
  execFileSync("xattr", ["-d", "-r", "com.apple.quarantine", flucomaDir], {
424
442
  stdio: "pipe",
@@ -434,7 +452,7 @@ async function setupFlucoma() {
434
452
 
435
453
  if (fs.existsSync(flucomaDir)) {
436
454
  console.log("");
437
- console.log("FluCoMa installed successfully!");
455
+ console.log("FluCoMa v%s installed successfully!", FLUCOMA_TAG);
438
456
  console.log("Restart Ableton Live for real-time DSP tools.");
439
457
  } else {
440
458
  console.error("Error: FluCoMa directory not found after extraction.");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.11",
3
+ "version": "1.9.13",
4
4
  "description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.11",
3
+ "version": "1.9.13",
4
4
  "description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
@@ -138,8 +138,8 @@ Never skip levels. The user's question determines the entry point, but always st
138
138
  ### Transport (12)
139
139
  `get_session_info` · `set_tempo` · `set_time_signature` · `start_playback` · `stop_playback` · `continue_playback` · `toggle_metronome` · `set_session_loop` · `undo` · `redo` · `get_recent_actions` · `get_session_diagnostics`
140
140
 
141
- ### Tracks (14)
142
- `get_track_info` · `create_midi_track` · `create_audio_track` · `create_return_track` · `delete_track` · `duplicate_track` · `set_track_name` · `set_track_color` · `set_track_mute` · `set_track_solo` · `set_track_arm` · `stop_track_clips` · `set_group_fold` · `set_track_input_monitoring`
141
+ ### Tracks (17)
142
+ `get_track_info` · `create_midi_track` · `create_audio_track` · `create_return_track` · `delete_track` · `duplicate_track` · `set_track_name` · `set_track_color` · `set_track_mute` · `set_track_solo` · `set_track_arm` · `stop_track_clips` · `set_group_fold` · `set_track_input_monitoring` · `freeze_track` · `flatten_track` · `get_freeze_status`
143
143
 
144
144
  ### Clips (11)
145
145
  `get_clip_info` · `create_clip` · `delete_clip` · `duplicate_clip` · `fire_clip` · `stop_clip` · `set_clip_name` · `set_clip_color` · `set_clip_loop` · `set_clip_launch` · `set_clip_warp_mode`
@@ -147,11 +147,11 @@ Never skip levels. The user's question determines the entry point, but always st
147
147
  ### Notes (8)
148
148
  `add_notes` · `get_notes` · `remove_notes` · `remove_notes_by_id` · `modify_notes` · `duplicate_notes` · `transpose_notes` · `quantize_clip`
149
149
 
150
- ### Devices (12)
151
- `get_device_info` · `get_device_parameters` · `set_device_parameter` · `batch_set_parameters` · `toggle_device` · `delete_device` · `load_device_by_uri` · `find_and_load_device` · `set_simpler_playback_mode` · `get_rack_chains` · `set_chain_volume` · `get_device_presets`
150
+ ### Devices (15)
151
+ `get_device_info` · `get_device_parameters` · `set_device_parameter` · `batch_set_parameters` · `toggle_device` · `delete_device` · `load_device_by_uri` · `find_and_load_device` · `set_simpler_playback_mode` · `get_rack_chains` · `set_chain_volume` · `get_device_presets` · `get_plugin_parameters` · `map_plugin_parameter` · `get_plugin_presets`
152
152
 
153
- ### Scenes (8)
154
- `get_scenes_info` · `create_scene` · `delete_scene` · `duplicate_scene` · `fire_scene` · `set_scene_name` · `set_scene_color` · `set_scene_tempo`
153
+ ### Scenes (12)
154
+ `get_scenes_info` · `create_scene` · `delete_scene` · `duplicate_scene` · `fire_scene` · `set_scene_name` · `set_scene_color` · `set_scene_tempo` · `get_scene_matrix` · `fire_scene_clips` · `stop_all_clips` · `get_playing_clips`
155
155
 
156
156
  ### Mixing (11)
157
157
  `set_track_volume` · `set_track_pan` · `set_track_send` · `get_return_tracks` · `get_master_track` · `set_master_volume` · `get_track_routing` · `set_track_routing` · `get_track_meters` · `get_master_meters` · `get_mix_snapshot`
@@ -1,4 +1,4 @@
1
- # LivePilot v1.9.11 — Architecture & Tool Reference
1
+ # LivePilot v1.9.13 — Architecture & Tool Reference
2
2
 
3
3
  Agentic production system for Ableton Live 12. 178 tools across 17 domains. Device atlas (280+ devices), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O.
4
4
 
@@ -29,12 +29,12 @@ Run this checklist EVERY time the user says "update everything", "push", "releas
29
29
  ## 2. Tool Count (must ALL match)
30
30
 
31
31
  Current: **178 tools across 17 domains**.
32
- Core (no M4L): **139**. Analyzer (M4L): **29**. Perception (offline): **4**.
32
+ Core (no M4L): **149**. Analyzer (M4L): **29**. Perception (offline): **4**.
33
33
 
34
34
  Verify: `grep -rc "@mcp.tool" mcp_server/tools/ | grep -v ":0" | awk -F: '{sum+=$2} END{print sum}'`
35
35
 
36
36
  Files that reference tool count:
37
- - [ ] `README.md` — header, PERCEPTION section ("139 core...29 analyzer"), Analyzer table header "(29)", Perception table header "(4)"
37
+ - [ ] `README.md` — header, PERCEPTION section ("149 core...29 analyzer"), Analyzer table header "(29)", Perception table header "(4)"
38
38
  - [ ] `package.json` → `"description"` (178 tools, 17 domains)
39
39
  - [ ] `server.json` → `"description"`
40
40
  - [ ] `livepilot/.Codex-plugin/plugin.json` → `"description"` (primary Codex manifest)
@@ -44,10 +44,10 @@ Files that reference tool count:
44
44
  - [ ] `livepilot/skills/livepilot-core/SKILL.md` — "178 tools across 17 domains", Analyzer (29), Perception (4)
45
45
  - [ ] `livepilot/skills/livepilot-core/references/overview.md` — "178 tools across 17 domains"
46
46
  - [ ] `docs/manual/index.md` — domain table: Analyzer (29), Perception (4)
47
- - [ ] `docs/manual/getting-started.md` — "139 core tools...29 analyzer"
47
+ - [ ] `docs/manual/getting-started.md` — "149 core tools...29 analyzer"
48
48
  - [ ] `docs/manual/tool-reference.md` — all domains present with correct counts
49
49
  - [ ] `docs/TOOL_REFERENCE.md` — all domains present
50
- - [ ] `docs/M4L_BRIDGE.md` — "139 core tools...29 analyzer"
50
+ - [ ] `docs/M4L_BRIDGE.md` — "149 core tools...29 analyzer"
51
51
  - [ ] `docs/social-banner.html`
52
52
  - [ ] `mcp_server/tools/analyzer.py` → module docstring
53
53
  - [ ] `tests/test_tools_contract.py` → expected total count
Binary file
@@ -84,7 +84,7 @@ function anything() {
84
84
  function dispatch(cmd, args) {
85
85
  switch(cmd) {
86
86
  case "ping":
87
- send_response({"ok": true, "version": "1.9.10"});
87
+ send_response({"ok": true, "version": "1.9.13"});
88
88
  break;
89
89
  case "get_params":
90
90
  cmd_get_params(args);
@@ -150,7 +150,7 @@ function dispatch(cmd, args) {
150
150
  cmd_capture_stop();
151
151
  break;
152
152
  case "check_flucoma":
153
- send_response({"flucoma_available": true, "version": "1.0.9"});
153
+ cmd_check_flucoma();
154
154
  break;
155
155
  // ── Phase 2: Clip & Display ──
156
156
  case "scrub_clip":
@@ -458,6 +458,22 @@ function cmd_get_chains_deep(args) {
458
458
  }
459
459
  }
460
460
 
461
+ function cmd_check_flucoma() {
462
+ // Check if FluCoMa externals are installed.
463
+ // Max JS cannot reliably probe the object search path at runtime,
464
+ // so we check if the FluCoMa package folder exists on disk.
465
+ try {
466
+ var pkg_path = max.appsupportpath + "/Packages/FluidCorpusManipulation";
467
+ var f = new Folder(pkg_path);
468
+ var available = !f.end; // end === true means folder not found
469
+ f.close();
470
+ send_response({"flucoma_available": available});
471
+ } catch (e) {
472
+ // Can't probe — report unknown rather than lying
473
+ send_response({"flucoma_available": false, "probe_error": String(e)});
474
+ }
475
+ }
476
+
461
477
  function cmd_get_track_cpu(args) {
462
478
  try {
463
479
  var results = [];
@@ -511,6 +527,25 @@ function cmd_get_selected() {
511
527
  break;
512
528
  }
513
529
  }
530
+ // Check return tracks if not found in main tracks
531
+ if (result.selected_track === -1) {
532
+ cursor_a.goto("live_set");
533
+ var rtc = cursor_a.getcount("return_tracks");
534
+ for (var j = 0; j < rtc; j++) {
535
+ cursor_a.goto("live_set return_tracks " + j);
536
+ if (cursor_a.get("name").toString() === result.selected_track_name) {
537
+ result.selected_track = -(j + 1); // -1, -2, ... convention
538
+ break;
539
+ }
540
+ }
541
+ }
542
+ // Check master track if still not found
543
+ if (result.selected_track === -1) {
544
+ cursor_a.goto("live_set master_track");
545
+ if (cursor_a.get("name").toString() === result.selected_track_name) {
546
+ result.selected_track = -1000; // master convention
547
+ }
548
+ }
514
549
  } catch(e) {}
515
550
 
516
551
  // Selected scene
@@ -1067,6 +1102,10 @@ function cmd_stop_scrub(args) {
1067
1102
  var path = build_track_path(track_idx) + " clip_slots " + clip_idx + " clip";
1068
1103
 
1069
1104
  cursor_a.goto(path);
1105
+ if (cursor_a.id === 0) {
1106
+ send_response({"error": "No clip at track " + track_idx + " slot " + clip_idx});
1107
+ return;
1108
+ }
1070
1109
  try {
1071
1110
  cursor_a.call("stop_scrub");
1072
1111
  send_response({"ok": true});
@@ -1165,6 +1204,15 @@ function cmd_capture_audio(args) {
1165
1204
  var duration_ms = parseInt(args[0]) || 10000;
1166
1205
  var requested_name = args[1] ? args[1].toString().trim() : "";
1167
1206
 
1207
+ // Sanitize filename — strip any directory components (defense-in-depth)
1208
+ if (requested_name.length > 0) {
1209
+ requested_name = _safe_filename(requested_name);
1210
+ if (!requested_name || requested_name.length === 0) {
1211
+ send_response({"error": "Invalid capture filename (path traversal blocked)"});
1212
+ return;
1213
+ }
1214
+ }
1215
+
1168
1216
  // Generate a timestamped filename if none provided
1169
1217
  var d = new Date();
1170
1218
  var ts = d.getFullYear() + "_"
@@ -1172,7 +1220,7 @@ function cmd_capture_audio(args) {
1172
1220
  + pad2(d.getDate()) + "_"
1173
1221
  + pad2(d.getHours()) + pad2(d.getMinutes()) + pad2(d.getSeconds());
1174
1222
  capture_filename = requested_name.length > 0 ? requested_name : ("capture_" + ts + ".wav");
1175
- capture_file_path = _join_path(_get_patcher_dir(), capture_filename);
1223
+ capture_file_path = _join_path(_get_captures_dir(), capture_filename);
1176
1224
 
1177
1225
  // Calculate sample count from duration and current sample rate
1178
1226
  var num_samples = Math.ceil((duration_ms / 1000.0) * current_sample_rate);
@@ -1438,6 +1486,22 @@ function build_device_path(track_idx, device_idx) {
1438
1486
  }
1439
1487
  }
1440
1488
 
1489
+ function _get_captures_dir() {
1490
+ // Stable captures directory: ~/Documents/LivePilot/captures/
1491
+ // max.appsupportpath = "/Users/<name>/Library/Application Support/Cycling '74"
1492
+ // Walk up to get home directory
1493
+ try {
1494
+ var support = max.appsupportpath;
1495
+ var parts = support.split("/");
1496
+ // /Users/<name>/Library/... → first 3 parts = /Users/<name>
1497
+ var home = parts.slice(0, 3).join("/");
1498
+ return home + "/Documents/LivePilot/captures/";
1499
+ } catch (e) {
1500
+ // Fallback to patcher directory if home detection fails
1501
+ return _get_patcher_dir();
1502
+ }
1503
+ }
1504
+
1441
1505
  function _get_patcher_dir() {
1442
1506
  try {
1443
1507
  var filepath = this.patcher && this.patcher.filepath ? this.patcher.filepath.toString() : "";
@@ -1450,6 +1514,16 @@ function _get_patcher_dir() {
1450
1514
  }
1451
1515
  }
1452
1516
 
1517
+ function _safe_filename(name) {
1518
+ // Strip directory components and reject traversal attempts.
1519
+ // This is defense-in-depth — Python should sanitize first.
1520
+ if (!name || name.length === 0) return name;
1521
+ var slash = Math.max(name.lastIndexOf("/"), name.lastIndexOf("\\"));
1522
+ if (slash >= 0) name = name.substring(slash + 1);
1523
+ if (name === "." || name === ".." || name.length === 0) return "";
1524
+ return name;
1525
+ }
1526
+
1453
1527
  function _join_path(dir, file) {
1454
1528
  if (!dir) return file;
1455
1529
  var last = dir.charAt(dir.length - 1);
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.9.11"
2
+ __version__ = "1.9.13"
@@ -213,7 +213,7 @@ class AbletonConnection:
213
213
  raise AbletonConnectionError(f"Failed to send command: {exc}") from exc
214
214
 
215
215
  # Read until newline, preserving any trailing bytes in _recv_buf
216
- buf = getattr(self, "_recv_buf", b"")
216
+ buf = self._recv_buf
217
217
  try:
218
218
  while b"\n" not in buf:
219
219
  chunk = self._socket.recv(4096)
@@ -222,6 +222,10 @@ class AbletonConnection:
222
222
  self.disconnect()
223
223
  raise AbletonConnectionError("Connection closed by Ableton")
224
224
  buf += chunk
225
+ if len(buf) > 10 * 1024 * 1024: # 10 MB
226
+ self._recv_buf = b""
227
+ self.disconnect()
228
+ raise AbletonConnectionError("Response too large (>10 MB)")
225
229
  except socket.timeout as exc:
226
230
  self._recv_buf = buf
227
231
  self.disconnect()
@@ -366,11 +366,15 @@ def _brownian(duration: float, density: int, start: float = 0.5,
366
366
  step = drift / density + volatility * _det_random(i, seed)
367
367
  value += step
368
368
  # Soft boundary reflection (bounce off 0/1 until within range)
369
- while value > 1.0 or value < 0.0:
369
+ for _ in range(100):
370
+ if 0.0 <= value <= 1.0:
371
+ break
370
372
  if value > 1.0:
371
373
  value = 2.0 - value
372
374
  if value < 0.0:
373
375
  value = -value
376
+ else:
377
+ value = max(0.0, min(1.0, value))
374
378
  return points
375
379
 
376
380
 
@@ -141,6 +141,7 @@ class SpectralReceiver(asyncio.DatagramProtocol):
141
141
  def __init__(self, cache: SpectralCache):
142
142
  self.cache = cache
143
143
  self._chunks: dict[str, dict] = {} # Reassembly buffer for chunked responses
144
+ self._chunk_times: dict[str, float] = {} # Monotonic timestamp per chunk sequence
144
145
  self._chunk_id = 0
145
146
  self._response_callback: Optional[asyncio.Future] = None
146
147
  self._capture_future: Optional[asyncio.Future] = None
@@ -284,6 +285,7 @@ class SpectralReceiver(asyncio.DatagramProtocol):
284
285
  key = str(self._chunk_id)
285
286
  if key not in self._chunks:
286
287
  self._chunks[key] = {"parts": {}, "total": total}
288
+ self._chunk_times[key] = time.monotonic()
287
289
 
288
290
  self._chunks[key]["parts"][index] = encoded
289
291
 
@@ -293,8 +295,16 @@ class SpectralReceiver(asyncio.DatagramProtocol):
293
295
  for i in range(total):
294
296
  full += self._chunks[key]["parts"][i]
295
297
  del self._chunks[key]
298
+ self._chunk_times.pop(key, None)
296
299
  self._handle_response(full)
297
300
 
301
+ # Evict incomplete chunk sequences older than 30 seconds
302
+ now = time.monotonic()
303
+ stale = [k for k, t in self._chunk_times.items() if now - t > 30.0]
304
+ for k in stale:
305
+ self._chunks.pop(k, None)
306
+ self._chunk_times.pop(k, None)
307
+
298
308
  def _handle_capture_complete(self, encoded: str) -> None:
299
309
  """Decode a /capture_complete OSC message and resolve _capture_future."""
300
310
  try:
@@ -359,30 +369,37 @@ class M4LBridge:
359
369
  if not self.cache.is_connected:
360
370
  return {"error": "LivePilot Analyzer not connected. Drop it on the master track."}
361
371
 
362
- # Cancel any stale capture future before creating a new one
363
- if self.receiver and self.receiver._capture_future and not self.receiver._capture_future.done():
364
- self.receiver._capture_future.cancel()
372
+ async with self._cmd_lock:
373
+ # Cancel any stale capture future before creating a new one
374
+ if self.receiver and self.receiver._capture_future and not self.receiver._capture_future.done():
375
+ self.receiver._capture_future.cancel()
365
376
 
366
- loop = asyncio.get_running_loop()
367
- future = loop.create_future()
368
- if self.receiver:
369
- self.receiver.set_capture_future(future)
377
+ loop = asyncio.get_running_loop()
378
+ future = loop.create_future()
379
+ if self.receiver:
380
+ self.receiver.set_capture_future(future)
370
381
 
371
- osc_data = self._build_osc(command, args)
372
- self._sock.sendto(osc_data, self._m4l_addr)
382
+ osc_data = self._build_osc(command, args)
383
+ self._sock.sendto(osc_data, self._m4l_addr)
373
384
 
374
- try:
375
- result = await asyncio.wait_for(future, timeout=timeout)
376
- return result
377
- except asyncio.TimeoutError:
378
- # Clean up the dangling future
379
- if self.receiver:
380
- self.receiver._capture_future = None
381
- return {"error": "M4L capture timeout — device may be busy or removed"}
385
+ try:
386
+ result = await asyncio.wait_for(future, timeout=timeout)
387
+ return result
388
+ except asyncio.TimeoutError:
389
+ # Clean up the dangling future
390
+ if self.receiver:
391
+ self.receiver._capture_future = None
392
+ return {"error": "M4L capture timeout — device may be busy or removed"}
382
393
 
383
- def cancel_capture_future(self) -> None:
384
- """Cancel any in-progress capture future (called by capture_stop)."""
385
- if self.receiver and self.receiver._capture_future and not self.receiver._capture_future.done():
394
+ async def cancel_capture_future(self) -> None:
395
+ """Cancel any in-progress capture future (called by capture_stop).
396
+
397
+ Does NOT acquire _cmd_lock — send_capture holds it during recording.
398
+ Cancelling the future causes send_capture's wait_for to raise
399
+ CancelledError, which releases the lock naturally.
400
+ """
401
+ if self.receiver and self.receiver._capture_future \
402
+ and not self.receiver._capture_future.done():
386
403
  self.receiver._capture_future.cancel()
387
404
  self.receiver._capture_future = None
388
405