livepilot 1.9.12 → 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.
@@ -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.12",
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.12 — 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,15 @@
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
+
3
13
  ## 1.9.12 — Deep Audit: 21 Fixes Across 15 Files (April 2026)
4
14
 
5
15
  **Full codebase audit — 5 critical, 10 important, 6 doc/test fixes.**
package/bin/livepilot.js CHANGED
@@ -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.12",
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.12",
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,4 +1,4 @@
1
- # LivePilot v1.9.12 — 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
 
@@ -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.12"});
87
+ send_response({"ok": true, "version": "1.9.13"});
88
88
  break;
89
89
  case "get_params":
90
90
  cmd_get_params(args);
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.9.12"
2
+ __version__ = "1.9.13"
@@ -26,10 +26,26 @@ class TechniqueStore:
26
26
  if base_dir is None:
27
27
  base_dir = os.path.join(os.path.expanduser("~"), ".livepilot", "memory")
28
28
  self._base_dir = Path(base_dir)
29
- self._base_dir.mkdir(parents=True, exist_ok=True)
30
29
  self._file = self._base_dir / "techniques.json"
31
30
  self._lock = threading.Lock()
32
-
31
+ self._initialized = False
32
+ self._data: dict = {"version": 1, "techniques": []}
33
+
34
+ def _ensure_initialized(self) -> None:
35
+ """Lazily create directory and load data on first access.
36
+
37
+ Deferred so that a read-only HOME doesn't crash the entire MCP
38
+ server at import time — memory tools just return errors instead.
39
+ """
40
+ if self._initialized:
41
+ return
42
+ try:
43
+ self._base_dir.mkdir(parents=True, exist_ok=True)
44
+ except OSError as exc:
45
+ raise RuntimeError(
46
+ f"Cannot create memory directory {self._base_dir}: {exc}. "
47
+ "Memory tools are unavailable."
48
+ ) from exc
33
49
  if self._file.exists():
34
50
  try:
35
51
  with open(self._file, "r") as f:
@@ -41,6 +57,7 @@ class TechniqueStore:
41
57
  else:
42
58
  self._data = {"version": 1, "techniques": []}
43
59
  self._flush()
60
+ self._initialized = True
44
61
 
45
62
  # ── persistence ──────────────────────────────────────────────
46
63
 
@@ -64,6 +81,7 @@ class TechniqueStore:
64
81
  tags: Optional[list[str]] = None,
65
82
  ) -> dict:
66
83
  """Create a new technique. Returns {id, name, type, summary}."""
84
+ self._ensure_initialized()
67
85
  if type not in VALID_TYPES:
68
86
  raise ValueError(
69
87
  f"INVALID_PARAM: type must be one of {sorted(VALID_TYPES)}, got '{type}'"
@@ -99,6 +117,7 @@ class TechniqueStore:
99
117
 
100
118
  def get(self, technique_id: str) -> dict:
101
119
  """Return full technique by id."""
120
+ self._ensure_initialized()
102
121
  with self._lock:
103
122
  for t in self._data["techniques"]:
104
123
  if t["id"] == technique_id:
@@ -113,6 +132,9 @@ class TechniqueStore:
113
132
  limit: int = 10,
114
133
  ) -> list[dict]:
115
134
  """Search techniques. Returns summaries (no payload)."""
135
+ self._ensure_initialized()
136
+ if limit < 0:
137
+ raise ValueError("INVALID_PARAM: limit must be >= 0")
116
138
  with self._lock:
117
139
  results = copy.deepcopy(self._data["techniques"])
118
140
 
@@ -150,6 +172,9 @@ class TechniqueStore:
150
172
  limit: int = 20,
151
173
  ) -> list[dict]:
152
174
  """List techniques as compact summaries."""
175
+ self._ensure_initialized()
176
+ if limit < 0:
177
+ raise ValueError("INVALID_PARAM: limit must be >= 0")
153
178
  if sort_by not in VALID_SORT_FIELDS:
154
179
  raise ValueError(
155
180
  f"INVALID_PARAM: sort_by must be one of {sorted(VALID_SORT_FIELDS)}, got '{sort_by}'"
@@ -179,6 +204,7 @@ class TechniqueStore:
179
204
  rating: Optional[int] = None,
180
205
  ) -> dict:
181
206
  """Set favorite flag and/or rating."""
207
+ self._ensure_initialized()
182
208
  if rating is not None and (rating < 0 or rating > 5):
183
209
  raise ValueError("INVALID_PARAM: rating must be between 0 and 5")
184
210
 
@@ -200,6 +226,7 @@ class TechniqueStore:
200
226
  qualities: Optional[dict] = None,
201
227
  ) -> dict:
202
228
  """Update technique fields. Qualities are merged (lists replaced)."""
229
+ self._ensure_initialized()
203
230
  with self._lock:
204
231
  t = self._find(technique_id)
205
232
  if name is not None:
@@ -216,6 +243,7 @@ class TechniqueStore:
216
243
 
217
244
  def delete(self, technique_id: str) -> dict:
218
245
  """Delete technique after creating a timestamped backup."""
246
+ self._ensure_initialized()
219
247
  with self._lock:
220
248
  t = self._find(technique_id)
221
249
  # backup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.12",
3
+ "version": "1.9.13",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
5
  "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",
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.9.12"
8
+ __version__ = "1.9.13"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from .server import LivePilotServer
@@ -34,8 +34,8 @@ class LivePilot(ControlSurface):
34
34
  ControlSurface.__init__(self, c_instance)
35
35
  self._server = LivePilotServer(self)
36
36
  self._server.start()
37
- self.log_message("LivePilot v%s initialized" % __version__)
38
- self.show_message("LivePilot: Listening on port 9878")
37
+ self.log_message("LivePilot v%s starting..." % __version__)
38
+ self.show_message("LivePilot v%s starting..." % __version__)
39
39
 
40
40
  def disconnect(self):
41
41
  """Called by Ableton when the script is unloaded."""
@@ -95,7 +95,8 @@ class LivePilotServer(object):
95
95
  self._thread = threading.Thread(target=self._server_loop)
96
96
  self._thread.daemon = True
97
97
  self._thread.start()
98
- self._log("Server started on %s:%d" % (self._host, self._port))
98
+ # Note: "Listening on ..." is logged from _server_loop after bind
99
+ # succeeds. Don't log "Server started" here — bind may still fail.
99
100
 
100
101
  def stop(self):
101
102
  """Shutdown the server gracefully."""
package/requirements.txt CHANGED
@@ -9,5 +9,8 @@ soundfile>=0.12.0
9
9
  scipy>=1.11.0
10
10
  mutagen>=1.47.0
11
11
 
12
+ # Development / testing (not required for runtime)
13
+ # pip install pytest pytest-asyncio
14
+
12
15
  # Optional: neo-Riemannian group theory (not required — harmony engine is pure Python)
13
16
  # pip install opycleid>=0.5.1