livepilot 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.1] — 2026-03-18
4
+
5
+ ### Fixes
6
+ - Browser search now enforces global 10,000 iteration budget across all categories (was resetting per-category)
7
+ - `_search_recursive` returns early when `max_results` is reached instead of scanning remaining siblings
8
+ - Narrowed 29 broad `except Exception` catches to specific types (`AttributeError`, `OSError`, `ValueError`) across browser, devices, mixing, and server modules
9
+ - Kept `except Exception` only at the 3 outermost dispatch boundaries (router, command processor, logger)
10
+
3
11
  ## [1.4.0] — 2026-03-18
4
12
 
5
13
  ### Added
package/README.md CHANGED
@@ -19,26 +19,33 @@ Every command goes through Ableton's official Live Object Model API. No hacks, n
19
19
 
20
20
  ---
21
21
 
22
- ## Build Your Own AI Producer
22
+ ## Train Your Own AI Producer
23
23
 
24
- LivePilot isn't just a tool collection — it's a framework for building **your own personalized AI production partner**.
24
+ LivePilot ships with 104 tools and a deep reference corpus genre-specific drum patterns, chord voicings, sound design recipes, mixing templates, song structures. Day one, it already knows how music works. But the tools are just the starting point.
25
25
 
26
- Out of the box, the agent ships with a deep reference corpus: genre-specific drum patterns, chord voicings, sound design recipes, mixing templates, song structures. It knows how music works. But that's just the starting point.
26
+ **The real product is the agent you build on top of them.**
27
27
 
28
- **The real power is training it to work like you.**
28
+ Every producer has a sonic identity — the swing amounts they reach for, the drum kits that feel right, the FX chains they keep coming back to, the way they EQ a vocal bus. Most AI tools ignore all of this. Every session starts blank. LivePilot is different: it has a technique memory system that turns your production decisions into a persistent, searchable stylistic palette the agent learns from over time.
29
29
 
30
- Every time you produce something you like — a beat, a device chain, a mixing setup — you tell the agent to remember it. It doesn't just store raw data. It writes a rich stylistic analysis: *what makes this groove feel the way it does, what the sonic texture is, what it pairs with, what artists it evokes.* Over sessions, your saved techniques become a **stylistic fingerprint** the agent understands and draws from.
30
+ Here's how you train it:
31
31
 
32
- Here's how you build your agent's palette:
32
+ 1. **Produce something you like** a beat, a device chain, a mixing setup, a synth patch
33
+ 2. **Tell the agent to save it** — "remember this groove" / "save this reverb chain"
34
+ 3. **The agent writes a stylistic analysis** — not just raw MIDI data, but *what makes it work*: the rhythmic feel, the sonic texture, the mood, what it pairs with, what artists it evokes
35
+ 4. **Your library grows** — rate and favorite the best techniques, tag them by genre or mood, build categories
36
+ 5. **The agent develops taste** — next time you say "make me a beat", it checks your library, reads your tendencies, and creates something new that sounds like you
33
37
 
34
- - **Save beats you like** "remember this groove" — the agent captures the pattern AND analyzes why it works
35
- - **Pin your go-to sounds** — "save this reverb chain" — instant recall next time, with context about when to use it
36
- - **Set preferences** — "I always want Utility on every track" — the agent adapts its workflow to match yours
37
- - **Curate over time** — rate, favorite, and refine saved techniques — the best ones float to the top
38
+ This isn't a preset recall system. The agent doesn't copy stored patterns it understands the *qualities* across your saved techniques (the swing you prefer, the harmonic language you gravitate toward, the density of your arrangements) and uses that understanding to inform new creative decisions.
38
39
 
39
- The next time you say "make me a beat", the agent checks your library. It doesn't copy — it reads your tendencies (the swing you like, the kits you reach for, your harmonic language) and creates something **new that sounds like you**. Say "ignore my history" and it starts fresh. Say "use that boom bap groove" and it replays exactly what you saved.
40
+ **Three modes, always under your control:**
40
41
 
41
- **You're not configuring software. You're building a creative relationship.**
42
+ - **Informed** (default) the agent consults your memory and lets it influence creative choices naturally
43
+ - **Fresh** — "ignore my history" / "something completely new" — blank slate, pure musical knowledge, zero influence from saved techniques
44
+ - **Explicit recall** — "use that boom bap beat I saved" — direct retrieval and replay
45
+
46
+ The memory is both a drawer and a personality. You put things in, you take things out, and the agent develops taste from what you've collected — but you can always tell it to forget everything and start fresh.
47
+
48
+ **You're not configuring software. You're building a creative partner that gets better the more you use it.**
42
49
 
43
50
  ---
44
51
 
@@ -286,21 +293,9 @@ What makes it different from a generic AI with tools is **what it knows**. The a
286
293
 
287
294
  But the shipped corpus is just the floor. The real value builds over time.
288
295
 
289
- ### Technique Memory — Your Evolving Stylistic Palette
290
-
291
- Most AI music tools are stateless — every session starts blank. LivePilot remembers.
292
-
293
- When you make something you like — a beat, a device chain, a mixing setup, a favorite preset — you tell the agent to save it. But it doesn't just store the raw MIDI data or parameter values. It writes a **stylistic analysis**: what makes this groove feel the way it does, what the sonic character is, what it pairs well with, what artists or styles it evokes. Over time, your saved techniques become a **stylistic fingerprint** — a palette the agent understands.
294
-
295
- The next time you say "make me a beat", the agent checks your library first. It doesn't copy a stored pattern — it reads across your saved techniques, understands your tendencies (the swing amounts you like, the kits you reach for, your harmonic language), and creates something **new that sounds like you**. It's the difference between an assistant that follows instructions and one that knows your taste.
296
-
297
- Three modes, always under your control:
298
-
299
- - **Informed** (default) — the agent consults your memory and lets it influence creative choices
300
- - **Fresh** — say "ignore my history" and the agent starts with a blank slate, pure musical knowledge
301
- - **Explicit recall** — say "use that boom bap beat I saved" for direct retrieval and replay
296
+ ### Technique Memory
302
297
 
303
- The memory is a drawer and a personality. You put things in, you take things out, and the agent develops taste from what you've collected but you can always override it.
298
+ 8 tools for saving, searching, and replaying production techniques. Every saved technique includes a rich stylistic analysis written by the agent not just raw data, but *what makes it work* and when to use it. See "Train Your Own AI Producer" above for how this shapes the agent over time.
304
299
 
305
300
  ### Core Skill
306
301
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
5
  "description": "AI copilot for Ableton Live 12 — 104 MCP tools for music production, sound design, and mixing",
6
6
  "author": "Pilot Studio",
@@ -17,19 +17,15 @@ You are LivePilot Producer — an autonomous music production agent for Ableton
17
17
  Given a high-level description, you:
18
18
 
19
19
  1. **Plan** — decide tempo, key, track layout, instrument choices, arrangement structure
20
- 1.5. **Consult memory** (unless user requests fresh exploration):
21
- Call `memory_recall` with a query matching the task (limit=5).
22
- Read the returned qualities and let them shape your plan kit choices,
23
- tempo range, rhythmic approach, sound palette. Don't copy be influenced.
24
- If the user says "fresh" / "ignore history" / "something new" skip this step.
25
- 2. **Build tracks** — create and name tracks with appropriate colors
26
- 3. **Load instruments** — find and load the right synths, drum kits, and samplers
27
- 4. **HEALTH CHECK** — verify every track actually produces sound (see below)
28
- 5. **Program patterns** — write MIDI notes that fit the genre and style
29
- 6. **Add effects** — load and configure effect chains for the desired sound
30
- 7. **HEALTH CHECK** — verify effects aren't pass-throughs (Dry/Wet > 0, Drive set, etc.)
31
- 8. **Mix** — balance volumes, set panning, configure sends
32
- 9. **Final verify** — `get_session_info`, fire scenes, confirm audio output
20
+ 2. **Consult memory** (unless user requests fresh exploration) — call `memory_recall` with a query matching the task (limit=5). Read the returned qualities and let them shape your plan: kit choices, tempo range, rhythmic approach, sound palette. Don't copy — be influenced. If the user says "fresh" / "ignore history" / "something new", skip this step entirely.
21
+ 3. **Build tracks** create and name tracks with appropriate colors
22
+ 4. **Load instruments** find and load the right synths, drum kits, and samplers
23
+ 5. **HEALTH CHECK** verify every track actually produces sound (see below)
24
+ 6. **Program patterns** write MIDI notes that fit the genre and style
25
+ 7. **Add effects** — load and configure effect chains for the desired sound
26
+ 8. **HEALTH CHECK** — verify effects aren't pass-throughs (Dry/Wet > 0, Drive set, etc.)
27
+ 9. **Mix** — balance volumes, set panning, configure sends
28
+ 10. **Final verify** — `get_session_info`, fire scenes, confirm audio output
33
29
 
34
30
  ## Mandatory Track Health Checks
35
31
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "AI copilot for Ableton Live 12 — 104 MCP tools for music production, sound design, and mixing",
5
5
  "author": "Pilot Studio",
6
6
  "skills": [
@@ -82,14 +82,17 @@ def _search_recursive(item, name_filter, loadable_only, results, depth, max_dept
82
82
  }
83
83
  try:
84
84
  entry["uri"] = child.uri
85
- except Exception:
85
+ except AttributeError:
86
86
  entry["uri"] = None
87
87
  results.append(entry)
88
88
  if child.is_folder:
89
+ before = len(results)
89
90
  _search_recursive(
90
91
  child, name_filter, loadable_only, results, depth + 1, max_depth,
91
92
  max_results
92
93
  )
94
+ if len(results) >= max_results:
95
+ return
93
96
 
94
97
 
95
98
  @register("get_browser_tree")
@@ -136,7 +139,7 @@ def get_browser_items(song, params):
136
139
  if child.is_loadable:
137
140
  try:
138
141
  entry["uri"] = child.uri
139
- except Exception:
142
+ except AttributeError:
140
143
  entry["uri"] = None
141
144
  result.append(entry)
142
145
  return {"path": path, "items": result}
@@ -186,7 +189,7 @@ def load_browser_item(song, params):
186
189
  for attr in category_attrs:
187
190
  try:
188
191
  categories.append(getattr(browser, attr))
189
- except Exception:
192
+ except AttributeError:
190
193
  pass
191
194
 
192
195
  _iterations = [0]
@@ -198,7 +201,7 @@ def load_browser_item(song, params):
198
201
  return None
199
202
  try:
200
203
  children = list(parent.children)
201
- except Exception:
204
+ except AttributeError:
202
205
  return None
203
206
  for child in children:
204
207
  _iterations[0] += 1
@@ -207,7 +210,7 @@ def load_browser_item(song, params):
207
210
  try:
208
211
  if child.uri == target_uri and child.is_loadable:
209
212
  return child
210
- except Exception:
213
+ except AttributeError:
211
214
  pass
212
215
  result = find_by_uri(child, target_uri, depth + 1)
213
216
  if result is not None:
@@ -215,7 +218,6 @@ def load_browser_item(song, params):
215
218
  return None
216
219
 
217
220
  for category in categories:
218
- _iterations[0] = 0
219
221
  found = find_by_uri(category, uri)
220
222
  if found is not None:
221
223
  song.view.selected_track = track
@@ -248,13 +250,14 @@ def load_browser_item(song, params):
248
250
  break
249
251
 
250
252
  target = device_name.lower()
253
+ _iterations[0] = 0
251
254
 
252
255
  def find_by_name(parent, depth=0):
253
256
  if depth > 8 or _iterations[0] > MAX_ITERATIONS:
254
257
  return None
255
258
  try:
256
259
  children = list(parent.children)
257
- except Exception:
260
+ except AttributeError:
258
261
  return None
259
262
  for child in children:
260
263
  _iterations[0] += 1
@@ -269,7 +272,6 @@ def load_browser_item(song, params):
269
272
  return None
270
273
 
271
274
  for category in categories:
272
- _iterations[0] = 0
273
275
  found = find_by_name(category)
274
276
  if found is not None:
275
277
  song.view.selected_track = track
@@ -311,14 +313,14 @@ def get_device_presets(song, params):
311
313
  return
312
314
  try:
313
315
  children = list(item.children)
314
- except Exception:
316
+ except AttributeError:
315
317
  return
316
318
  for child in children:
317
319
  if child.is_loadable and not child.is_folder:
318
320
  entry = {"name": child.name}
319
321
  try:
320
322
  entry["uri"] = child.uri
321
- except Exception:
323
+ except AttributeError:
322
324
  entry["uri"] = None
323
325
  results.append(entry)
324
326
  elif child.is_folder:
@@ -30,7 +30,7 @@ def get_device_info(song, params):
30
30
  }
31
31
  try:
32
32
  result["type"] = device.type
33
- except Exception:
33
+ except AttributeError:
34
34
  result["type"] = None
35
35
  return result
36
36
 
@@ -196,7 +196,7 @@ def load_device_by_uri(song, params):
196
196
  for attr in category_attrs:
197
197
  try:
198
198
  categories.append(getattr(browser, attr))
199
- except Exception:
199
+ except AttributeError:
200
200
  pass
201
201
 
202
202
  _iterations = [0]
@@ -208,7 +208,7 @@ def load_device_by_uri(song, params):
208
208
  return None
209
209
  try:
210
210
  children = list(parent.children)
211
- except Exception:
211
+ except AttributeError:
212
212
  return None
213
213
  for child in children:
214
214
  _iterations[0] += 1
@@ -217,7 +217,7 @@ def load_device_by_uri(song, params):
217
217
  try:
218
218
  if child.uri == target_uri and child.is_loadable:
219
219
  return child
220
- except Exception:
220
+ except AttributeError:
221
221
  pass
222
222
  result = find_by_uri(child, target_uri, depth + 1)
223
223
  if result is not None:
@@ -225,7 +225,6 @@ def load_device_by_uri(song, params):
225
225
  return None
226
226
 
227
227
  for category in categories:
228
- _iterations[0] = 0
229
228
  found = find_by_uri(category, uri)
230
229
  if found is not None:
231
230
  song.view.selected_track = track
@@ -252,13 +251,14 @@ def load_device_by_uri(song, params):
252
251
  break
253
252
 
254
253
  target = device_name.lower()
254
+ _iterations[0] = 0
255
255
 
256
256
  def find_by_name(parent, depth=0):
257
257
  if depth > 8 or _iterations[0] > MAX_ITERATIONS:
258
258
  return None
259
259
  try:
260
260
  children = list(parent.children)
261
- except Exception:
261
+ except AttributeError:
262
262
  return None
263
263
  for child in children:
264
264
  _iterations[0] += 1
@@ -273,7 +273,6 @@ def load_device_by_uri(song, params):
273
273
  return None
274
274
 
275
275
  for category in categories:
276
- _iterations[0] = 0
277
276
  found = find_by_name(category)
278
277
  if found is not None:
279
278
  song.view.selected_track = track
@@ -307,7 +306,7 @@ def find_and_load_device(song, params):
307
306
  return None
308
307
  try:
309
308
  children = list(item.children)
310
- except Exception:
309
+ except AttributeError:
311
310
  return None
312
311
  for child in children:
313
312
  iterations += 1
@@ -332,11 +331,11 @@ def find_and_load_device(song, params):
332
331
  for attr in category_attrs:
333
332
  try:
334
333
  categories.append(getattr(browser, attr))
335
- except Exception:
334
+ except AttributeError:
336
335
  pass
337
336
 
338
337
  for category in categories:
339
- iterations = 0 # Reset per category so each gets a fair search budget
338
+ iterations = 0
340
339
  found = search_children(category)
341
340
  if found is not None:
342
341
  song.view.selected_track = track
@@ -97,19 +97,19 @@ def get_track_routing(song, params):
97
97
  result = {"index": track_index}
98
98
  try:
99
99
  result["input_routing_type"] = track.input_routing_type.display_name
100
- except Exception:
100
+ except AttributeError:
101
101
  result["input_routing_type"] = None
102
102
  try:
103
103
  result["input_routing_channel"] = track.input_routing_channel.display_name
104
- except Exception:
104
+ except AttributeError:
105
105
  result["input_routing_channel"] = None
106
106
  try:
107
107
  result["output_routing_type"] = track.output_routing_type.display_name
108
- except Exception:
108
+ except AttributeError:
109
109
  result["output_routing_type"] = None
110
110
  try:
111
111
  result["output_routing_channel"] = track.output_routing_channel.display_name
112
- except Exception:
112
+ except AttributeError:
113
113
  result["output_routing_channel"] = None
114
114
  return result
115
115
 
@@ -84,7 +84,7 @@ class LivePilotServer(object):
84
84
  if self._server_socket:
85
85
  try:
86
86
  self._server_socket.close()
87
- except Exception:
87
+ except OSError:
88
88
  pass
89
89
  if self._thread and self._thread.is_alive():
90
90
  self._thread.join(timeout=3)
@@ -109,7 +109,7 @@ class LivePilotServer(object):
109
109
  self._server_socket.listen(2)
110
110
  self._server_socket.settimeout(1.0)
111
111
  self._log("Listening on %s:%d" % (self._host, self._port))
112
- except Exception as exc:
112
+ except OSError as exc:
113
113
  self._log("Failed to bind: %s" % exc)
114
114
  return
115
115
 
@@ -132,37 +132,37 @@ class LivePilotServer(object):
132
132
  }
133
133
  }) + "\n"
134
134
  client.sendall(reject.encode("utf-8"))
135
- except Exception:
135
+ except OSError:
136
136
  pass
137
137
  try:
138
138
  client.close()
139
- except Exception:
139
+ except OSError:
140
140
  pass
141
141
  continue
142
142
  self._client_connected = True
143
143
  self._log("Client connected from %s:%d" % addr)
144
144
  try:
145
145
  self._handle_client(client)
146
- except Exception as exc:
146
+ except OSError as exc:
147
147
  self._log("Client error: %s" % exc)
148
148
  finally:
149
149
  with self._client_lock:
150
150
  self._client_connected = False
151
151
  try:
152
152
  client.close()
153
- except Exception:
153
+ except OSError:
154
154
  pass
155
155
  self._log("Client disconnected")
156
156
  except socket.timeout:
157
157
  continue
158
- except Exception:
158
+ except OSError:
159
159
  if self._running:
160
160
  self._log("Accept error")
161
161
  break
162
162
 
163
163
  try:
164
164
  self._server_socket.close()
165
- except Exception:
165
+ except OSError:
166
166
  pass
167
167
 
168
168
  def _handle_client(self, client):
@@ -182,7 +182,7 @@ class LivePilotServer(object):
182
182
  self._process_line(client, line)
183
183
  except socket.timeout:
184
184
  continue
185
- except Exception as exc:
185
+ except OSError as exc:
186
186
  self._log("Recv error: %s" % exc)
187
187
  break
188
188
 
@@ -190,7 +190,7 @@ class LivePilotServer(object):
190
190
  """Parse one JSON command, queue it for main thread, wait for result."""
191
191
  try:
192
192
  command = json.loads(line)
193
- except Exception as exc:
193
+ except (ValueError, TypeError) as exc:
194
194
  resp = {
195
195
  "id": "unknown",
196
196
  "ok": False,
@@ -282,5 +282,5 @@ class LivePilotServer(object):
282
282
  from .utils import serialize_json
283
283
  try:
284
284
  client.sendall(serialize_json(response).encode("utf-8"))
285
- except Exception as exc:
285
+ except OSError as exc:
286
286
  self._log("Send error: %s" % exc)