livepilot 1.10.7 → 1.10.8

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 (122) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +11 -9
  3. package/bin/livepilot.js +146 -28
  4. package/installer/install.js +117 -11
  5. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  6. package/m4l_device/livepilot_bridge.js +1 -1
  7. package/mcp_server/__init__.py +1 -1
  8. package/mcp_server/atlas/__init__.py +39 -7
  9. package/mcp_server/atlas/tools.py +56 -15
  10. package/mcp_server/composer/layer_planner.py +27 -0
  11. package/mcp_server/composer/prompt_parser.py +15 -6
  12. package/mcp_server/connection.py +11 -3
  13. package/mcp_server/corpus/__init__.py +14 -4
  14. package/mcp_server/m4l_bridge.py +48 -7
  15. package/mcp_server/runtime/execution_router.py +16 -2
  16. package/mcp_server/runtime/remote_commands.py +6 -0
  17. package/mcp_server/sample_engine/models.py +22 -3
  18. package/mcp_server/semantic_moves/__init__.py +1 -0
  19. package/mcp_server/semantic_moves/compiler.py +9 -1
  20. package/mcp_server/semantic_moves/device_creation_compilers.py +47 -0
  21. package/mcp_server/semantic_moves/mix_compilers.py +170 -0
  22. package/mcp_server/semantic_moves/mix_moves.py +1 -1
  23. package/mcp_server/semantic_moves/models.py +5 -0
  24. package/mcp_server/semantic_moves/tools.py +15 -4
  25. package/mcp_server/server.py +7 -3
  26. package/mcp_server/services/singletons.py +68 -0
  27. package/mcp_server/splice_client/client.py +29 -8
  28. package/mcp_server/tools/analyzer.py +7 -6
  29. package/mcp_server/tools/clips.py +1 -1
  30. package/mcp_server/tools/midi_io.py +10 -0
  31. package/mcp_server/tools/tracks.py +1 -1
  32. package/mcp_server/tools/transport.py +1 -1
  33. package/mcp_server/translation_engine/tools.py +8 -4
  34. package/package.json +25 -3
  35. package/remote_script/LivePilot/__init__.py +29 -9
  36. package/remote_script/LivePilot/arrangement.py +12 -2
  37. package/remote_script/LivePilot/browser.py +16 -6
  38. package/remote_script/LivePilot/devices.py +10 -5
  39. package/remote_script/LivePilot/notes.py +13 -2
  40. package/remote_script/LivePilot/server.py +51 -13
  41. package/remote_script/LivePilot/version_detect.py +7 -4
  42. package/server.json +20 -0
  43. package/.claude-plugin/marketplace.json +0 -21
  44. package/.mcp.json.disabled +0 -9
  45. package/.mcpbignore +0 -60
  46. package/AGENTS.md +0 -46
  47. package/BUGS.md +0 -1570
  48. package/CODE_OF_CONDUCT.md +0 -27
  49. package/CONTRIBUTING.md +0 -131
  50. package/SECURITY.md +0 -48
  51. package/livepilot/.Codex-plugin/plugin.json +0 -8
  52. package/livepilot/.claude-plugin/plugin.json +0 -8
  53. package/livepilot/agents/livepilot-producer/AGENT.md +0 -313
  54. package/livepilot/commands/arrange.md +0 -47
  55. package/livepilot/commands/beat.md +0 -77
  56. package/livepilot/commands/evaluate.md +0 -49
  57. package/livepilot/commands/memory.md +0 -22
  58. package/livepilot/commands/mix.md +0 -44
  59. package/livepilot/commands/perform.md +0 -42
  60. package/livepilot/commands/session.md +0 -13
  61. package/livepilot/commands/sounddesign.md +0 -43
  62. package/livepilot/skills/livepilot-arrangement/SKILL.md +0 -155
  63. package/livepilot/skills/livepilot-composition-engine/SKILL.md +0 -107
  64. package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +0 -97
  65. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +0 -102
  66. package/livepilot/skills/livepilot-core/SKILL.md +0 -184
  67. package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +0 -831
  68. package/livepilot/skills/livepilot-core/references/automation-atlas.md +0 -272
  69. package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +0 -110
  70. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +0 -687
  71. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +0 -753
  72. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +0 -525
  73. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +0 -402
  74. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +0 -963
  75. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +0 -874
  76. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +0 -571
  77. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +0 -714
  78. package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +0 -953
  79. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +0 -34
  80. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +0 -204
  81. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +0 -173
  82. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +0 -211
  83. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +0 -188
  84. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +0 -162
  85. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +0 -229
  86. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +0 -243
  87. package/livepilot/skills/livepilot-core/references/m4l-devices.md +0 -352
  88. package/livepilot/skills/livepilot-core/references/memory-guide.md +0 -107
  89. package/livepilot/skills/livepilot-core/references/midi-recipes.md +0 -402
  90. package/livepilot/skills/livepilot-core/references/mixing-patterns.md +0 -578
  91. package/livepilot/skills/livepilot-core/references/overview.md +0 -290
  92. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +0 -724
  93. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +0 -140
  94. package/livepilot/skills/livepilot-core/references/sound-design.md +0 -393
  95. package/livepilot/skills/livepilot-devices/SKILL.md +0 -169
  96. package/livepilot/skills/livepilot-evaluation/SKILL.md +0 -156
  97. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +0 -118
  98. package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +0 -121
  99. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +0 -110
  100. package/livepilot/skills/livepilot-mix-engine/SKILL.md +0 -123
  101. package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +0 -143
  102. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +0 -105
  103. package/livepilot/skills/livepilot-mixing/SKILL.md +0 -157
  104. package/livepilot/skills/livepilot-notes/SKILL.md +0 -130
  105. package/livepilot/skills/livepilot-performance-engine/SKILL.md +0 -122
  106. package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +0 -98
  107. package/livepilot/skills/livepilot-release/SKILL.md +0 -130
  108. package/livepilot/skills/livepilot-sample-engine/SKILL.md +0 -105
  109. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +0 -87
  110. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +0 -51
  111. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +0 -131
  112. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +0 -168
  113. package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +0 -119
  114. package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +0 -118
  115. package/livepilot/skills/livepilot-wonder/SKILL.md +0 -79
  116. package/m4l_device/LivePilot_Analyzer.amxd.pre-presentation-backup +0 -0
  117. package/m4l_device/LivePilot_Analyzer.maxpat +0 -2705
  118. package/m4l_device/LivePilot_Analyzer.maxproj +0 -53
  119. package/manifest.json +0 -91
  120. package/mcp_server/splice_client/protos/app_pb2.pyi +0 -1153
  121. package/scripts/generate_tool_catalog.py +0 -106
  122. package/scripts/sync_metadata.py +0 -349
package/CHANGELOG.md CHANGED
@@ -1,5 +1,131 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.10.8 — Deep audit fix pass (April 18 2026)
4
+
5
+ Outcome of a cross-subsystem audit (Remote Script, MCP server, M4L bridge,
6
+ Sample/Splice/Atlas, Composer/Router, Installer, Tests). 2104 → 2116
7
+ passing tests (added ~230 regression tests, 324 tools, 45 domains). New
8
+ MCP tool: `reload_atlas`. Three orphan mix moves
9
+ (`make_kick_bass_lock`, `create_buildup_tension`, `smooth_scene_handoff`)
10
+ now produce real executable plans instead of silent zero-step failures.
11
+ A family compiler handles all device-creation moves. CI now enforces
12
+ metadata drift and `.amxd` freeze parity — preventing the class of bug
13
+ that cost two prior releases.
14
+
15
+ ### Ship-stoppers
16
+
17
+ - **`capture_audio` backend annotation** fixed in `REDUCE_REPETITION` —
18
+ was declared `mcp_tool`, is actually `bridge_command` (matched your
19
+ memory note about the backend-annotation invariant).
20
+ - **Three orphan mix moves** (`make_kick_bass_lock`,
21
+ `create_buildup_tension`, `smooth_scene_handoff`) had no compilers
22
+ and produced silent zero-step plans — compilers added.
23
+ - **Seven device-creation orphan moves** fixed via a family-level
24
+ compiler that maps `plan_template` → `CompiledStep`.
25
+ - **`logger` used before definition** in `mcp_server/server.py`,
26
+ `mcp_server/tools/analyzer.py`, and
27
+ `mcp_server/translation_engine/tools.py` (the last one had the
28
+ definition buried inside a docstring — a genuine NameError on the
29
+ exception path). All three fixed; new regression test
30
+ `test_import_hygiene.py` will catch recurrences.
31
+ - **FluCoMa SHA256 bypass** removed — `ACCEPT_FIRST_RUN` sentinel is
32
+ gone. Verification is now mandatory; a fresh run with unpinned
33
+ hashes requires explicit
34
+ `LIVEPILOT_ALLOW_UNVERIFIED_FLUCOMA=1` opt-in.
35
+ - **FluCoMa Max 9 vs Max 8 path** fixed — detect whether Max 9 or Max 8
36
+ is actually installed instead of assuming the presence of
37
+ `Packages/` means the corresponding Max is installed. Fresh Max 9
38
+ machines were landing in the Max 8 legacy path.
39
+
40
+ ### Correctness
41
+
42
+ - **Remote Script TCP UTF-8 boundary corruption** — accumulate raw bytes,
43
+ decode only on newline-framed lines. Previously a multi-byte sequence
44
+ straddling a 4096-byte recv boundary silently produced `\uFFFD`.
45
+ - **`_command_queue.get_nowait()` race** on `AssertionError` — drain by
46
+ response-queue identity, not blind FIFO pop.
47
+ - **`toggle_device` silent `parameters[0]` fallback** removed — now
48
+ raises `STATE_ERROR` if the device has no "Device On" parameter.
49
+ - **`modify_notes` partial-batch mutation** — two-pass validate-then-apply
50
+ in both `notes.py` and `arrangement.py`.
51
+ - **Browser deep-scan audio-thread stall** — `DEEP_MAX` reduced 200k → 20k,
52
+ clearer error pointing to `search_browser`.
53
+ - **`_force_reload_handlers` silent swallow** — reload exceptions now log
54
+ through the ControlSurface so stale handlers are surfaced.
55
+ - **`version_detect` failure caching** — no longer pins the whole session
56
+ to (12,0,0) on a transient detect failure.
57
+ - **Atlas non-atomic write** — tmp + fsync + rename pattern.
58
+ - **Atlas and corpus check-then-set race** — wrapped in the shared
59
+ `services.singletons.Singleton` helper. Atlas also auto-reloads when
60
+ `device_atlas.json` mtime advances. New `reload_atlas` MCP tool forces
61
+ a manual refresh.
62
+ - **`time.sleep()` inside the TCP connection lock** — moved outside the
63
+ lock so other async handlers aren't blocked on the idle timer.
64
+ - **M4L bridge chunk ordering** — out-of-order first-chunk now starts a
65
+ new bucket with a warning instead of corrupting the previous
66
+ sequence's payload.
67
+ - **M4L bridge `receiver=None`** — fail fast with an explicit error
68
+ instead of sending OSC blind and waiting out the full timeout.
69
+ - **Sample critic `-1.0` sentinel** — `overall_score` now respects the
70
+ `available` flag and averages only usable critics.
71
+ - **Splice gRPC timeouts** added per-call (`SearchSamples`, `SampleInfo`,
72
+ `ValidateLogin`, `SyncSounds`, `DownloadSample`).
73
+ - **Installer path traversal** — `LIVEPILOT_INSTALL_PATH` validated
74
+ against allowed roots.
75
+ - **Installer overlay upgrade** — rename existing install to
76
+ `LivePilot.backup-<ts>/` before fresh copy, auto-prune old backups.
77
+ Stale files from renamed modules no longer survive upgrades.
78
+ - **Installer `process.exit` vs `try/catch` mismatch** — `install()`
79
+ now raises typed `InstallerAbort` so the `--setup` wizard can
80
+ continue past a recoverable failure instead of dying mid-run.
81
+ - **`step_results` non-dict drop** — warns instead of silently losing
82
+ the binding.
83
+ - **Composer `_KEY_RE`** — tightened to require either accidental or
84
+ explicit quality word; "dark ambient" no longer parses as D major.
85
+ - **Composer section-plan overshoot** — final pass trims oversized
86
+ sections so snapping can't push total past `duration_bars`.
87
+ - **`export_clip_midi` extension guard** — refuses to write
88
+ non-`.mid`/`.midi` files after path resolution.
89
+
90
+ ### CI + test hygiene
91
+
92
+ - `scripts/sync_metadata.py --check` is now a CI gate (three prior
93
+ drift releases were preventable).
94
+ - `.amxd` version-string guard in CI — refuses PRs where the frozen
95
+ bridge embeds a version that doesn't match the repo.
96
+ - `npm pack` cleanliness gate — fails on `.disabled`, `.backup`,
97
+ `.pre-*`, `.DS_Store`, or `.pyc` entries.
98
+ - `test_tools_contract` now asserts every tool has a non-empty
99
+ description (≥20 chars) and a schema.
100
+ - `test_move_annotations` silent-escape fixed — a declared backend
101
+ that classifies as `unknown` is now a hard failure.
102
+ - `test_bridge_parity` promoted from `INFO:` print to hard assertion.
103
+ - `test_corpus` adds a canary that fails when source files are absent
104
+ (previously 5 tests silently skipped).
105
+ - `tests/test_splice_client.py` scaffolded — credit floor, timeout
106
+ constants, port.conf parsing, graceful-degrade fallback (10 tests).
107
+ - `package.json` `files` allowlist added — npm pack is deterministic
108
+ (780 → 321 files, no dirty artifacts).
109
+ - CHANGELOG + `capability-modes.md` added to `VERSION_FILES` in
110
+ `sync_metadata.py`.
111
+
112
+ ### Deferred (follow-up PR)
113
+
114
+ - Mechanical `lifespan_context.setdefault(...)` sweep across 7 files
115
+ (eager constructor issue — small perf impact, not correctness).
116
+ - `safe_call` helper to replace the ~14 remaining `except Exception:
117
+ pass/return None` patterns.
118
+ - FastMCP tool description quality sweep (audit existing 324 tools for
119
+ copy-paste / below-threshold descriptions).
120
+
121
+ ### Release-process changes
122
+
123
+ - **FluCoMa SHA256 pinned** to `1a5cb73…6a2` (the universal zip containing
124
+ both macOS `.mxo` and Windows `.mxe64` externals). Previous releases
125
+ shipped with `"ACCEPT_FIRST_RUN"` sentinels that skipped verification.
126
+ - **`.amxd` refrozen** with matching `1.10.8` ping bytes. CI guard
127
+ (`amxd-freeze-drift`) enforces this on every push.
128
+
3
129
  ## 1.10.7 — npm .amxd parity + domain-count consistency (April 18 2026)
4
130
 
5
131
  Shipping release. Brings npm's tarball back in line with the fresh `.amxd`
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
- 323 tools. 45 domains. Device atlas. Splice integration. Auto-composition. Spectral perception. Technique memory.
20
+ 324 tools. 45 domains. Device atlas. Splice integration. Auto-composition. Spectral perception. Technique memory.
21
21
  </p>
22
22
 
23
23
  <br>
@@ -79,7 +79,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
79
79
  │ └─────────────────┼──────────────────┘ │
80
80
  │ ▼ │
81
81
  │ ┌─────────────────┐ │
82
- │ │ 323 MCP Tools │ │
82
+ │ │ 324 MCP Tools │ │
83
83
  │ │ 45 domains │ │
84
84
  │ └────────┬────────┘ │
85
85
  │ │ │
@@ -100,13 +100,13 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
100
100
 
101
101
  **MCP Server** (`mcp_server/`) — Python FastMCP server. Validates inputs, routes commands to the Remote Script over TCP, manages the M4L bridge, runs the atlas, sample engine, composer, and all intelligence engines. This is what your AI client connects to.
102
102
 
103
- **M4L Bridge** (`m4l_device/`) — Optional Max for Live Audio Effect on the master track. Provides deep LOM access through Max's LiveAPI that the ControlSurface API can't reach. UDP 9880 (M4L to server) carries spectral data and LiveAPI responses. OSC 9881 (server to M4L) sends commands. 36 bridge tools (backed by 27 bridge commands) for hidden parameters, Simpler internals, warp markers, and display values.
103
+ **M4L Bridge** (`m4l_device/`) — Optional Max for Live Audio Effect on the master track. Provides deep LOM access through Max's LiveAPI that the ControlSurface API can't reach. UDP 9880 (M4L to server) carries spectral data and LiveAPI responses. OSC 9881 (server to M4L) sends commands. 36 bridge tools (backed by 29 bridge commands) for hidden parameters, Simpler internals, warp markers, display values, and Simpler warp / Compressor sidechain writes that live on child objects Python can't reach.
104
104
 
105
105
  **Device Atlas** (`mcp_server/atlas/`) — In-memory indexed JSON database. 1305 devices with browser URIs, 81 enriched with YAML sonic intelligence profiles (mood, genre, texture, recommended chains). 6 indexes: by_id, by_name, by_uri, by_category, by_tag, by_genre. The AI never hallucinates a device name or preset — it always resolves against the atlas first.
106
106
 
107
107
  **Sample Engine** (`mcp_server/sample_engine/`) — Searches three sources simultaneously: BrowserSource (Ableton's library), SpliceSource (local Splice catalog via SQLite), FilesystemSource (user directories). Every result passes through a 6-critic fitness battery (key, tempo, spectral, genre, mood, technical). 29 processing techniques (Surgeon precision vs. Alchemist experimentation). Builds complete sample processing plans with warp, slice, and effect recommendations.
108
108
 
109
- **Splice Client** (`mcp_server/splice_client/`) — Reads Splice's local SQLite database (`sounds.db`) for searching downloaded samples with full metadata (key, BPM, genre, tags). A gRPC client for the Splice desktop API exists but is not yet wired into the server lifespan currently all Splice integration is local-only via SQLite. No API key needed.
109
+ **Splice Client** (`mcp_server/splice_client/`) — Searches Splice's catalog through two layers: the local SQLite database (`sounds.db`, already-downloaded samples) and the live gRPC API (full catalog, including samples you haven't downloaded yet). The gRPC client auto-detects Splice's dynamic port via `port.conf`, handles self-signed TLS, and enforces a 5-credit safety floor before any download. Per-call timeouts (5–10s) prevent a hung Splice process from stalling the MCP event loop. Graceful fallback to SQL-only if grpcio isn't installed. No API key needed — authentication comes from the running Splice desktop app.
110
110
 
111
111
  **Composer** (`mcp_server/composer/`) — Prompt-to-plan pipeline. Parses natural language ("dark minimal techno 128bpm with industrial textures") into a CompositionIntent (genre, mood, tempo, key). Plans layers using role templates (kick, bass, percussion, texture, lead, pad, fx). Compiles to a step-by-step plan of tool calls that the agent executes. Does not execute autonomously — returns the plan. 7 genre defaults (techno, house, ambient, hip-hop, dnb, dub, experimental).
112
112
 
@@ -120,7 +120,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
120
120
 
121
121
  ## The Intelligence Layer
122
122
 
123
- 12 engines sit on top of the 323 tools. They give the AI musical judgment, not just musical execution.
123
+ 12 engines sit on top of the 324 tools. They give the AI musical judgment, not just musical execution.
124
124
 
125
125
  ### SongBrain — What the Song Is
126
126
 
@@ -172,7 +172,7 @@ Every engine follows: **measure before → act → measure after → compare**.
172
172
 
173
173
  ## Tools
174
174
 
175
- 323 tools across 45 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
175
+ 324 tools across 45 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
176
176
 
177
177
  <br>
178
178
 
@@ -287,7 +287,7 @@ LivePilot reads Splice's local SQLite database to search your downloaded samples
287
287
 
288
288
  **How it works:** The Sample Engine's `SpliceSource` reads `~/Library/Application Support/com.splice.Splice/users/default/*/sounds.db` — Splice's local SQLite catalog of downloaded samples. Read-only, no network calls.
289
289
 
290
- **Requirements:** Splice desktop app installed with some downloaded samples. A gRPC client for Splice's live API exists in `mcp_server/splice_client/` but is not yet wired into the server runtime.
290
+ **Requirements:** Splice desktop app running (the MCP server talks to it over gRPC at a dynamic port advertised via `port.conf`, with self-signed TLS). For fully offline search, previously-downloaded samples are always searchable via the local SQLite fallback even if the Splice app isn't running.
291
291
 
292
292
  <br>
293
293
 
@@ -360,7 +360,7 @@ The V2 intelligence layer. These tools analyze, diagnose, plan, evaluate, and le
360
360
  | Creative Constraints | 5 | constraint activation, reference-inspired variants |
361
361
  | Preview Studio | 5 | variant creation, preview rendering, comparison, commit |
362
362
 
363
- > **[View all 323 tools →](docs/manual/tool-catalog.md)**
363
+ > **[View all 324 tools →](docs/manual/tool-catalog.md)**
364
364
 
365
365
  <br>
366
366
 
@@ -572,6 +572,8 @@ npx livepilot --version # Show version
572
572
  git clone https://github.com/dreamrec/LivePilot.git
573
573
  cd LivePilot
574
574
  python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
575
+ # Test runner is not in requirements.txt (runtime-only deps) — install it explicitly:
576
+ .venv/bin/pip install pytest pytest-asyncio
575
577
  .venv/bin/pytest tests/ -v
576
578
  ```
577
579
 
@@ -585,7 +587,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, code guidelines
585
587
 
586
588
  | Document | What's inside |
587
589
  |----------|---------------|
588
- | [Manual](docs/manual/index.md) | Complete reference: architecture, all 323 tools, workflows |
590
+ | [Manual](docs/manual/index.md) | Complete reference: architecture, all 324 tools, workflows |
589
591
  | [Intelligence Layer](docs/manual/intelligence.md) | How the 12 engines connect — conductor, moves, preview, evaluation |
590
592
  | [Device Atlas](docs/manual/device-atlas.md) | 1305 devices indexed — search, suggest, chain building |
591
593
  | [Samples & Slicing](docs/manual/samples.md) | 3-source search, fitness critics, slice workflows |
package/bin/livepilot.js CHANGED
@@ -397,6 +397,44 @@ async function doctor() {
397
397
  // FluCoMa installer
398
398
  // ---------------------------------------------------------------------------
399
399
 
400
+ /**
401
+ * Detect whether Max (major version) is installed on the system. Returns the
402
+ * highest installed major version number, or 0 if Max is not installed.
403
+ *
404
+ * macOS: checks /Applications/Max.app/Contents/Info.plist for CFBundleShortVersionString.
405
+ * Windows: checks standard install locations under Program Files.
406
+ */
407
+ function detectMaxMajorVersion() {
408
+ try {
409
+ if (process.platform === "darwin") {
410
+ const infoPlist = "/Applications/Max.app/Contents/Info.plist";
411
+ if (!fs.existsSync(infoPlist)) return 0;
412
+ const out = execFileSync("defaults", ["read", infoPlist, "CFBundleShortVersionString"], {
413
+ encoding: "utf-8",
414
+ timeout: 3000,
415
+ }).trim();
416
+ const m = out.match(/^(\d+)/);
417
+ return m ? parseInt(m[1], 10) : 0;
418
+ }
419
+ if (process.platform === "win32") {
420
+ const candidates = [
421
+ "C:\\Program Files\\Cycling '74\\Max 9",
422
+ "C:\\Program Files\\Cycling '74\\Max 8",
423
+ ];
424
+ for (const candidate of candidates) {
425
+ if (fs.existsSync(candidate)) {
426
+ const m = candidate.match(/Max (\d+)/);
427
+ if (m) return parseInt(m[1], 10);
428
+ }
429
+ }
430
+ return 0;
431
+ }
432
+ } catch {
433
+ return 0;
434
+ }
435
+ return 0;
436
+ }
437
+
400
438
  async function setupFlucoma() {
401
439
  const os = require("os");
402
440
  const https = require("https");
@@ -404,9 +442,10 @@ async function setupFlucoma() {
404
442
  const home = os.homedir();
405
443
 
406
444
  // Max 9 is the current release (the Ableton Live 12.3+ default); Max 8 is
407
- // the legacy path. Check both if FluCoMa is already installed in either,
408
- // Max will find it via its package search path. For fresh installs, prefer
409
- // Max 9. Users still on Max 8 get the legacy path.
445
+ // the legacy path. Select based on which major Max is actually installed
446
+ // NOT on whether the Packages directory exists. A fresh Max 9 install often
447
+ // has no Packages folder yet, so the old fs.existsSync() check silently
448
+ // steered fresh Max 9 machines onto the Max 8 legacy path.
410
449
  const docsBase = process.platform === "darwin"
411
450
  ? path.join(home, "Documents")
412
451
  : path.join(process.env.USERPROFILE || home, "Documents");
@@ -414,8 +453,24 @@ async function setupFlucoma() {
414
453
  const max9PackagesDir = path.join(docsBase, "Max 9", "Packages");
415
454
  const max8PackagesDir = path.join(docsBase, "Max 8", "Packages");
416
455
 
417
- // Prefer Max 9 if that directory exists, else Max 8
418
- const packagesDir = fs.existsSync(max9PackagesDir) ? max9PackagesDir : max8PackagesDir;
456
+ const maxMajor = detectMaxMajorVersion();
457
+ let packagesDir;
458
+ if (maxMajor >= 9) {
459
+ packagesDir = max9PackagesDir;
460
+ } else if (maxMajor === 8) {
461
+ packagesDir = max8PackagesDir;
462
+ } else {
463
+ // Max not detected — fall back to whichever Packages dir already exists,
464
+ // preferring Max 9. If neither exists, default to Max 9 (future-proof).
465
+ if (fs.existsSync(max9PackagesDir)) {
466
+ packagesDir = max9PackagesDir;
467
+ } else if (fs.existsSync(max8PackagesDir)) {
468
+ packagesDir = max8PackagesDir;
469
+ } else {
470
+ console.log("Could not detect Max installation. Defaulting to Max 9 Packages path.");
471
+ packagesDir = max9PackagesDir;
472
+ }
473
+ }
419
474
  const flucomaDir = path.join(packagesDir, "FluidCorpusManipulation");
420
475
 
421
476
  // Check BOTH locations for an existing install — a user may have Max 8
@@ -440,19 +495,29 @@ async function setupFlucoma() {
440
495
  return;
441
496
  }
442
497
 
443
- // Ensure the parent Packages directory exists for the install target
498
+ // Ensure the parent Packages directory exists for the install target — Max
499
+ // lazily creates Packages/ on first package install, so it may be absent on
500
+ // a fresh Max 9 system.
444
501
  fs.mkdirSync(packagesDir, { recursive: true });
445
502
 
446
503
  console.log("FluCoMa not found. Downloading from GitHub...");
447
504
  const crypto = require("crypto");
448
505
 
449
506
  // Pin to a known release tag for reproducibility and security.
450
- // SHA256 checksums are verified after download — update these when bumping the tag.
507
+ //
508
+ // IMPORTANT: FluCoMa 1.0.7 ships as a single universal zip that contains
509
+ // both the macOS externals (.mxo) and the Windows externals (.mxe64).
510
+ // There is ONE hash to pin, not two. If a future release reverts to
511
+ // per-platform zips, convert FLUCOMA_SHA256 back to a {Mac, Windows}
512
+ // dict and restore the platform-specific asset finder.
513
+ //
514
+ // To re-pin after a version bump:
515
+ // curl -L -o flucoma.zip <release-zip-url>
516
+ // shasum -a 256 flucoma.zip # macOS
517
+ // CertUtil -hashfile flucoma.zip SHA256 # Windows
518
+ // then paste the 64-char hex digest into FLUCOMA_SHA256 below.
451
519
  const FLUCOMA_TAG = "1.0.7";
452
- const FLUCOMA_SHA256 = {
453
- Mac: "ACCEPT_FIRST_RUN", // Set to actual hash after first verified download
454
- Windows: "ACCEPT_FIRST_RUN",
455
- };
520
+ const FLUCOMA_SHA256 = "1a5cb7340e8816a9983b981a5a84ddb95b63e6d71446f278b9dc81c3cc1206a2";
456
521
  const FLUCOMA_URL = `https://api.github.com/repos/flucoma/flucoma-max/releases/tags/${FLUCOMA_TAG}`;
457
522
 
458
523
  // Fetch pinned release info
@@ -476,12 +541,16 @@ async function setupFlucoma() {
476
541
  }).on("error", reject);
477
542
  });
478
543
 
479
- const platform = process.platform === "darwin" ? "Mac" : "Windows";
480
- const zipAsset = releaseInfo.assets.find(a => a.name.endsWith(".zip") && a.name.includes(platform));
544
+ // FluCoMa 1.0.7 publishes a single universal zip — pick the first .zip
545
+ // asset on the release page. If upstream starts shipping per-platform
546
+ // zips again, reintroduce the filename filter here.
547
+ const platform = process.platform === "darwin" ? "macOS" : "Windows";
548
+ const zipAsset = (releaseInfo.assets || []).find(a => a.name.endsWith(".zip"));
481
549
  if (!zipAsset) {
482
- console.error("Error: no %s zip asset found in FluCoMa release %s", platform, FLUCOMA_TAG);
550
+ console.error("Error: no .zip asset found in FluCoMa release %s", FLUCOMA_TAG);
483
551
  process.exit(1);
484
552
  }
553
+ console.log("Target platform: %s (zip contains externals for both)", platform);
485
554
 
486
555
  console.log("Downloading %s (v%s, %sMB)...", zipAsset.name, FLUCOMA_TAG,
487
556
  Math.round(zipAsset.size / 1024 / 1024));
@@ -512,22 +581,50 @@ async function setupFlucoma() {
512
581
  const hash = crypto.createHash("sha256");
513
582
  hash.update(fs.readFileSync(zipPath));
514
583
  const sha256 = hash.digest("hex");
515
- const expectedHash = FLUCOMA_SHA256[platform];
584
+ const expectedHash = FLUCOMA_SHA256;
516
585
  console.log("SHA256: %s", sha256);
517
586
 
518
- if (expectedHash && expectedHash !== "ACCEPT_FIRST_RUN") {
519
- if (sha256 !== expectedHash) {
520
- console.error("ERROR: SHA256 mismatch! Expected %s", expectedHash);
521
- console.error("The downloaded file may be corrupted or tampered with.");
522
- console.error("Aborting installation. Delete %s and retry.", zipPath);
587
+ const isPinned = expectedHash && expectedHash !== "UNPINNED"
588
+ && /^[0-9a-f]{64}$/i.test(expectedHash);
589
+
590
+ if (isPinned) {
591
+ if (sha256.toLowerCase() !== expectedHash.toLowerCase()) {
592
+ console.error("");
593
+ console.error(" SHA256 MISMATCH — refusing to install.");
594
+ console.error(" expected: %s", expectedHash);
595
+ console.error(" actual: %s", sha256);
596
+ console.error("");
597
+ console.error(" The downloaded file does not match the pinned hash.");
598
+ console.error(" Either the release changed upstream, or the download was tampered with.");
599
+ console.error(" Verify at https://github.com/flucoma/flucoma-max/releases/tag/%s", FLUCOMA_TAG);
600
+ console.error("");
523
601
  try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
524
602
  process.exit(1);
525
603
  }
526
604
  console.log("Checksum verified ✓");
527
605
  } else {
528
- // First run with this tag record the hash for future verification
529
- console.log("First download of v%srecord this SHA256 for future verification:", FLUCOMA_TAG);
530
- console.log("Update FLUCOMA_SHA256['%s'] in bin/livepilot.js to: '%s'", platform, sha256);
606
+ // Hash is not yet pinned. Require an explicit opt-in so unverified
607
+ // installs are never silent the previous "ACCEPT_FIRST_RUN" sentinel
608
+ // auto-accepted every run.
609
+ const allowUnverified = process.env.LIVEPILOT_ALLOW_UNVERIFIED_FLUCOMA === "1";
610
+ if (!allowUnverified) {
611
+ console.error("");
612
+ console.error(" FluCoMa SHA256 is not pinned.");
613
+ console.error(" Downloaded hash: %s", sha256);
614
+ console.error("");
615
+ console.error(" Refusing to install an unverified binary by default.");
616
+ console.error(" To proceed (and help pin the hash), re-run with:");
617
+ console.error(" LIVEPILOT_ALLOW_UNVERIFIED_FLUCOMA=1 npx livepilot --setup-flucoma");
618
+ console.error("");
619
+ console.error(" Then open a PR that sets FLUCOMA_SHA256 in bin/livepilot.js to:");
620
+ console.error(" '%s'", sha256);
621
+ console.error("");
622
+ try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
623
+ process.exit(1);
624
+ }
625
+ console.warn("⚠ Installing unverified FluCoMa — LIVEPILOT_ALLOW_UNVERIFIED_FLUCOMA=1 was set.");
626
+ console.warn(" Record this SHA256 in FLUCOMA_SHA256:");
627
+ console.warn(" '%s'", sha256);
531
628
  }
532
629
 
533
630
  console.log("Extracting to %s...", packagesDir);
@@ -601,9 +698,22 @@ async function setup() {
601
698
  console.log("");
602
699
  console.log("Step 2/5: Installing Remote Script...");
603
700
  try {
604
- const { install } = require(path.join(ROOT, "installer", "install.js"));
605
- install();
606
- console.log(" ✓ Remote Script installed");
701
+ const { install, InstallerAbort } = require(path.join(ROOT, "installer", "install.js"));
702
+ try {
703
+ install();
704
+ console.log(" ✓ Remote Script installed");
705
+ } catch (err) {
706
+ if (err instanceof InstallerAbort && err.recoverable) {
707
+ // Recoverable — don't bail the wizard. The user can rerun
708
+ // --install manually, and later steps (Python env, M4L Analyzer,
709
+ // diagnostics) may still succeed or at least inform them.
710
+ console.log(" ⚠ Skipped: %s", err.message.split("\n")[0]);
711
+ console.log(" (Continuing with remaining setup steps.)");
712
+ ok = false;
713
+ } else {
714
+ throw err;
715
+ }
716
+ }
607
717
  } catch (err) {
608
718
  console.log(" ✗ Failed: %s", err.message);
609
719
  ok = false;
@@ -720,8 +830,16 @@ async function main() {
720
830
 
721
831
  // --install
722
832
  if (flag === "--install") {
723
- const { install } = require(path.join(ROOT, "installer", "install.js"));
724
- install();
833
+ const { install, InstallerAbort } = require(path.join(ROOT, "installer", "install.js"));
834
+ try {
835
+ install();
836
+ } catch (err) {
837
+ if (err instanceof InstallerAbort) {
838
+ console.error(err.message);
839
+ process.exit(err.recoverable ? 2 : 1);
840
+ }
841
+ throw err;
842
+ }
725
843
  return;
726
844
  }
727
845
 
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const fs = require("fs");
4
+ const os = require("os");
4
5
  const path = require("path");
5
6
  const { findAbletonPaths } = require("./paths");
6
7
 
@@ -10,6 +11,60 @@ const SOURCE_DIR = path.join(ROOT, "remote_script", "LivePilot");
10
11
  // Files / dirs to skip during copy
11
12
  const SKIP = new Set(["__pycache__", ".DS_Store"]);
12
13
 
14
+ // How many previous backups to keep on disk before auto-pruning (the upgrade
15
+ // path renames the old LivePilot dir to LivePilot.backup-<ts>/ so the user can
16
+ // recover a manual edit).
17
+ const BACKUP_RETENTION = 3;
18
+
19
+ /**
20
+ * Typed installer error. Wrappers (e.g. the --setup wizard) can catch this
21
+ * and decide whether to continue with later steps (recoverable) or abort the
22
+ * whole wizard (non-recoverable). The previous version called process.exit(1)
23
+ * mid-function, which silently short-circuited the setup wizard — callers
24
+ * had try/catch expecting exceptions, so later steps (bootstrap, M4L install,
25
+ * diagnostics) were skipped without warning.
26
+ */
27
+ class InstallerAbort extends Error {
28
+ constructor(message, { recoverable = false } = {}) {
29
+ super(message);
30
+ this.name = "InstallerAbort";
31
+ this.recoverable = recoverable;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Validate that a user-supplied install destination is somewhere safe.
37
+ * Refuses to write outside of the user's home directory unless it matches
38
+ * one of the known Ableton Remote Scripts paths. This closes the path-
39
+ * traversal hole from `LIVEPILOT_INSTALL_PATH=/etc ...`.
40
+ */
41
+ function _assertSafeInstallPath(resolvedPath, candidates) {
42
+ const home = os.homedir();
43
+ const allowedPrefixes = [
44
+ home,
45
+ // Systemwide Ableton install paths that live outside $HOME on some platforms
46
+ "/Applications/Ableton",
47
+ "C:\\ProgramData\\Ableton",
48
+ ];
49
+ // The detected Ableton candidate paths are always considered safe
50
+ for (const c of candidates) {
51
+ if (path.resolve(c.path).startsWith(path.resolve(resolvedPath))) {
52
+ return;
53
+ }
54
+ if (path.resolve(resolvedPath).startsWith(path.resolve(c.path))) {
55
+ return;
56
+ }
57
+ }
58
+ const safe = allowedPrefixes.some((p) => resolvedPath.startsWith(path.resolve(p)));
59
+ if (!safe) {
60
+ throw new InstallerAbort(
61
+ `LIVEPILOT_INSTALL_PATH=${resolvedPath} is outside permitted directories. ` +
62
+ `Refusing to install. Allowed roots: ${allowedPrefixes.join(", ")}`,
63
+ { recoverable: false }
64
+ );
65
+ }
66
+ }
67
+
13
68
  /**
14
69
  * Recursively copy a directory, skipping __pycache__ and .DS_Store.
15
70
  */
@@ -28,22 +83,51 @@ function copyDirSync(src, dest) {
28
83
  }
29
84
  }
30
85
 
86
+ /**
87
+ * Prune old LivePilot.backup-<ts>/ dirs, keeping the most recent N.
88
+ */
89
+ function _pruneBackups(parentDir) {
90
+ try {
91
+ const entries = fs.readdirSync(parentDir, { withFileTypes: true });
92
+ const backups = entries
93
+ .filter((e) => e.isDirectory() && /^LivePilot\.backup-\d+$/.test(e.name))
94
+ .map((e) => e.name)
95
+ .sort(); // lexicographic — timestamps are monotonic, so this is age order
96
+ while (backups.length > BACKUP_RETENTION) {
97
+ const old = backups.shift();
98
+ try {
99
+ fs.rmSync(path.join(parentDir, old), { recursive: true, force: true });
100
+ } catch {
101
+ // best effort — don't let cleanup failure break an install
102
+ }
103
+ }
104
+ } catch {
105
+ // best effort
106
+ }
107
+ }
108
+
31
109
  /**
32
110
  * Install the LivePilot Remote Script into Ableton's Remote Scripts folder.
111
+ *
112
+ * Throws InstallerAbort on recoverable failures (auto-detect missing) or
113
+ * non-recoverable ones (path-traversal attempt). Never calls process.exit.
114
+ * This lets the setup wizard continue with later steps on a recoverable
115
+ * failure.
33
116
  */
34
117
  function install() {
35
118
  const candidates = findAbletonPaths();
36
119
 
37
120
  if (candidates.length === 0) {
38
- console.log("Could not auto-detect an Ableton Live Remote Scripts directory.");
39
- console.log("");
40
- console.log("Manual install:");
41
- console.log(" 1. Open Ableton Live > Preferences > File/Folder");
42
- console.log(" 2. Find the User Remote Scripts folder path");
43
- console.log(" 3. Copy the 'remote_script/LivePilot' folder into that directory");
44
- console.log(" 4. Restart Ableton Live");
45
- console.log(" 5. In Preferences > Link/Tempo/MIDI, set a Control Surface to 'LivePilot'");
46
- process.exit(1);
121
+ throw new InstallerAbort(
122
+ "Could not auto-detect an Ableton Live Remote Scripts directory.\n\n" +
123
+ "Manual install:\n" +
124
+ " 1. Open Ableton Live > Preferences > File/Folder\n" +
125
+ " 2. Find the User Remote Scripts folder path\n" +
126
+ " 3. Copy the 'remote_script/LivePilot' folder into that directory\n" +
127
+ " 4. Restart Ableton Live\n" +
128
+ " 5. In Preferences > Link/Tempo/MIDI, set a Control Surface to 'LivePilot'",
129
+ { recoverable: true }
130
+ );
47
131
  }
48
132
 
49
133
  // If multiple candidates exist, let the user choose via --install-path
@@ -51,7 +135,9 @@ function install() {
51
135
  let target;
52
136
  const explicitPath = process.env.LIVEPILOT_INSTALL_PATH;
53
137
  if (explicitPath) {
54
- target = { path: explicitPath, description: "explicit (LIVEPILOT_INSTALL_PATH)" };
138
+ const resolved = path.resolve(explicitPath);
139
+ _assertSafeInstallPath(resolved, candidates);
140
+ target = { path: resolved, description: "explicit (LIVEPILOT_INSTALL_PATH)" };
55
141
  } else if (candidates.length > 1) {
56
142
  console.log("Multiple Ableton Remote Scripts directories detected:");
57
143
  candidates.forEach((c, i) => {
@@ -73,6 +159,25 @@ function install() {
73
159
  // Ensure target base exists
74
160
  fs.mkdirSync(targetBase, { recursive: true });
75
161
 
162
+ // Clear-then-copy upgrade path. Overlay-copying on top of an existing
163
+ // install leaves stale files when a module is removed/renamed upstream.
164
+ // Instead, rename the previous install to a timestamped backup, copy
165
+ // fresh, then prune old backups. The rename (not delete) preserves any
166
+ // local edits the user may have made.
167
+ if (fs.existsSync(destDir)) {
168
+ const ts = new Date().toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
169
+ const backup = path.join(targetBase, `LivePilot.backup-${ts}`);
170
+ try {
171
+ fs.renameSync(destDir, backup);
172
+ console.log("Existing install backed up to: %s", backup);
173
+ } catch (e) {
174
+ throw new InstallerAbort(
175
+ `Could not back up previous LivePilot install at ${destDir}: ${e.message}`,
176
+ { recoverable: false }
177
+ );
178
+ }
179
+ }
180
+
76
181
  console.log("Installing LivePilot Remote Script...");
77
182
  console.log(" Source: %s", SOURCE_DIR);
78
183
  console.log(" Target: %s", destDir);
@@ -80,6 +185,7 @@ function install() {
80
185
  console.log("");
81
186
 
82
187
  copyDirSync(SOURCE_DIR, destDir);
188
+ _pruneBackups(targetBase);
83
189
 
84
190
  console.log("Done! Next steps:");
85
191
  console.log(" 1. Restart Ableton Live (or press Cmd+, to open Preferences)");
@@ -111,4 +217,4 @@ function uninstall() {
111
217
  }
112
218
  }
113
219
 
114
- module.exports = { install, uninstall };
220
+ module.exports = { install, uninstall, InstallerAbort };
Binary file
@@ -95,7 +95,7 @@ function anything() {
95
95
  function dispatch(cmd, args) {
96
96
  switch(cmd) {
97
97
  case "ping":
98
- send_response({"ok": true, "version": "1.10.6"});
98
+ send_response({"ok": true, "version": "1.10.8"});
99
99
  break;
100
100
  case "get_params":
101
101
  cmd_get_params(args);