livepilot 1.0.0

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 (64) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +409 -0
  4. package/bin/livepilot.js +390 -0
  5. package/installer/install.js +95 -0
  6. package/installer/paths.js +79 -0
  7. package/mcp_server/__init__.py +2 -0
  8. package/mcp_server/__main__.py +5 -0
  9. package/mcp_server/connection.py +210 -0
  10. package/mcp_server/memory/__init__.py +5 -0
  11. package/mcp_server/memory/technique_store.py +296 -0
  12. package/mcp_server/server.py +87 -0
  13. package/mcp_server/tools/__init__.py +1 -0
  14. package/mcp_server/tools/arrangement.py +407 -0
  15. package/mcp_server/tools/browser.py +86 -0
  16. package/mcp_server/tools/clips.py +218 -0
  17. package/mcp_server/tools/devices.py +256 -0
  18. package/mcp_server/tools/memory.py +198 -0
  19. package/mcp_server/tools/mixing.py +121 -0
  20. package/mcp_server/tools/notes.py +269 -0
  21. package/mcp_server/tools/scenes.py +89 -0
  22. package/mcp_server/tools/tracks.py +175 -0
  23. package/mcp_server/tools/transport.py +117 -0
  24. package/package.json +37 -0
  25. package/plugin/agents/livepilot-producer/AGENT.md +62 -0
  26. package/plugin/commands/beat.md +18 -0
  27. package/plugin/commands/memory.md +22 -0
  28. package/plugin/commands/mix.md +15 -0
  29. package/plugin/commands/session.md +13 -0
  30. package/plugin/commands/sounddesign.md +16 -0
  31. package/plugin/plugin.json +19 -0
  32. package/plugin/skills/livepilot-core/SKILL.md +208 -0
  33. package/plugin/skills/livepilot-core/references/ableton-workflow-patterns.md +831 -0
  34. package/plugin/skills/livepilot-core/references/device-atlas/00-index.md +110 -0
  35. package/plugin/skills/livepilot-core/references/device-atlas/distortion-and-character.md +687 -0
  36. package/plugin/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +753 -0
  37. package/plugin/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +525 -0
  38. package/plugin/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +402 -0
  39. package/plugin/skills/livepilot-core/references/device-atlas/midi-tools.md +963 -0
  40. package/plugin/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +874 -0
  41. package/plugin/skills/livepilot-core/references/device-atlas/space-and-depth.md +571 -0
  42. package/plugin/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +714 -0
  43. package/plugin/skills/livepilot-core/references/device-atlas/synths-native.md +953 -0
  44. package/plugin/skills/livepilot-core/references/m4l-devices.md +352 -0
  45. package/plugin/skills/livepilot-core/references/memory-guide.md +107 -0
  46. package/plugin/skills/livepilot-core/references/midi-recipes.md +402 -0
  47. package/plugin/skills/livepilot-core/references/mixing-patterns.md +578 -0
  48. package/plugin/skills/livepilot-core/references/overview.md +209 -0
  49. package/plugin/skills/livepilot-core/references/sound-design.md +392 -0
  50. package/remote_script/LivePilot/__init__.py +42 -0
  51. package/remote_script/LivePilot/arrangement.py +693 -0
  52. package/remote_script/LivePilot/browser.py +424 -0
  53. package/remote_script/LivePilot/clips.py +211 -0
  54. package/remote_script/LivePilot/devices.py +596 -0
  55. package/remote_script/LivePilot/diagnostics.py +198 -0
  56. package/remote_script/LivePilot/mixing.py +194 -0
  57. package/remote_script/LivePilot/notes.py +339 -0
  58. package/remote_script/LivePilot/router.py +74 -0
  59. package/remote_script/LivePilot/scenes.py +99 -0
  60. package/remote_script/LivePilot/server.py +293 -0
  61. package/remote_script/LivePilot/tracks.py +268 -0
  62. package/remote_script/LivePilot/transport.py +151 -0
  63. package/remote_script/LivePilot/utils.py +123 -0
  64. package/requirements.txt +2 -0
@@ -0,0 +1,198 @@
1
+ """
2
+ LivePilot - Session diagnostics handler (1 command).
3
+
4
+ Analyzes the current session and flags potential issues:
5
+ armed tracks, mute/solo leftovers, empty clips, unnamed tracks,
6
+ empty scenes, and device-less instrument tracks.
7
+ """
8
+
9
+ from .router import register
10
+
11
+
12
+ # Default track names that indicate the user hasn't renamed them.
13
+ # Ableton auto-names tracks like "1-MIDI", "2-Audio", "3-MIDI", etc.
14
+ _DEFAULT_NAME_PATTERNS = frozenset([
15
+ "MIDI", "Audio", "Inst", "Return",
16
+ ])
17
+
18
+
19
+ def _looks_default_name(name):
20
+ """Check if a track name looks like an Ableton default."""
21
+ stripped = name.strip()
22
+ # Pattern: "N-Type" or just "Type" (e.g., "1-MIDI", "MIDI", "2-Audio")
23
+ for part in stripped.split("-"):
24
+ part = part.strip()
25
+ if part.isdigit():
26
+ continue
27
+ if part in _DEFAULT_NAME_PATTERNS:
28
+ return True
29
+ return False
30
+
31
+
32
+ @register("get_session_diagnostics")
33
+ def get_session_diagnostics(song, params):
34
+ """Analyze the session and return a diagnostic report."""
35
+ issues = []
36
+ stats = {
37
+ "track_count": 0,
38
+ "return_track_count": 0,
39
+ "scene_count": 0,
40
+ "total_clips": 0,
41
+ "empty_scenes": 0,
42
+ }
43
+
44
+ tracks = list(song.tracks)
45
+ scenes = list(song.scenes)
46
+ return_tracks = list(song.return_tracks)
47
+
48
+ stats["track_count"] = len(tracks)
49
+ stats["return_track_count"] = len(return_tracks)
50
+ stats["scene_count"] = len(scenes)
51
+
52
+ # ── Track-level checks ─────────────────────────────────────────────
53
+
54
+ armed_tracks = []
55
+ soloed_tracks = []
56
+ muted_tracks = []
57
+ unnamed_tracks = []
58
+ empty_tracks = [] # no clips at all
59
+ no_device_midi_tracks = [] # MIDI tracks with no instruments
60
+ track_slots = [] # cached clip_slots per track (avoid re-evaluating LOM tuple)
61
+
62
+ for i, track in enumerate(tracks):
63
+ # Armed check
64
+ if track.arm:
65
+ armed_tracks.append({"index": i, "name": track.name})
66
+
67
+ # Solo check
68
+ if track.solo:
69
+ soloed_tracks.append({"index": i, "name": track.name})
70
+
71
+ # Muted tracks (informational, only flag if many)
72
+ if track.mute:
73
+ muted_tracks.append({"index": i, "name": track.name})
74
+
75
+ # Unnamed / default name check
76
+ if _looks_default_name(track.name):
77
+ unnamed_tracks.append({"index": i, "name": track.name})
78
+
79
+ # Cache clip_slots once per track
80
+ slots = list(track.clip_slots)
81
+ track_slots.append(slots)
82
+
83
+ # Clip count
84
+ clip_count = 0
85
+ for slot in slots:
86
+ if slot.has_clip:
87
+ clip_count += 1
88
+ stats["total_clips"] += clip_count
89
+
90
+ if clip_count == 0:
91
+ empty_tracks.append({"index": i, "name": track.name})
92
+
93
+ # MIDI track with no devices (no instrument loaded)
94
+ if track.has_midi_input and len(list(track.devices)) == 0:
95
+ no_device_midi_tracks.append({"index": i, "name": track.name})
96
+
97
+ # Build issues from checks
98
+ if armed_tracks:
99
+ issues.append({
100
+ "type": "armed_tracks",
101
+ "severity": "warning",
102
+ "message": "%d track(s) left armed" % len(armed_tracks),
103
+ "details": armed_tracks,
104
+ })
105
+
106
+ if soloed_tracks:
107
+ issues.append({
108
+ "type": "soloed_tracks",
109
+ "severity": "warning",
110
+ "message": "%d track(s) soloed — other tracks are silenced" % len(soloed_tracks),
111
+ "details": soloed_tracks,
112
+ })
113
+
114
+ if len(muted_tracks) > len(tracks) * 0.5 and len(muted_tracks) > 2:
115
+ issues.append({
116
+ "type": "many_muted",
117
+ "severity": "info",
118
+ "message": "%d of %d tracks muted — consider cleaning up unused tracks" % (
119
+ len(muted_tracks), len(tracks)
120
+ ),
121
+ "details": muted_tracks,
122
+ })
123
+
124
+ if unnamed_tracks:
125
+ issues.append({
126
+ "type": "unnamed_tracks",
127
+ "severity": "info",
128
+ "message": "%d track(s) have default names" % len(unnamed_tracks),
129
+ "details": unnamed_tracks,
130
+ })
131
+
132
+ if empty_tracks:
133
+ issues.append({
134
+ "type": "empty_tracks",
135
+ "severity": "info",
136
+ "message": "%d track(s) have no clips" % len(empty_tracks),
137
+ "details": empty_tracks,
138
+ })
139
+
140
+ if no_device_midi_tracks:
141
+ issues.append({
142
+ "type": "no_instrument",
143
+ "severity": "warning",
144
+ "message": "%d MIDI track(s) have no instrument loaded" % len(no_device_midi_tracks),
145
+ "details": no_device_midi_tracks,
146
+ })
147
+
148
+ # ── Scene-level checks ──────────────────────────────────────────────
149
+
150
+ empty_scenes = []
151
+ for i, scene in enumerate(scenes):
152
+ has_any_clip = False
153
+ for slots in track_slots:
154
+ if i < len(slots) and slots[i].has_clip:
155
+ has_any_clip = True
156
+ break
157
+ if not has_any_clip:
158
+ empty_scenes.append({"index": i, "name": scene.name})
159
+
160
+ stats["empty_scenes"] = len(empty_scenes)
161
+
162
+ if empty_scenes and len(empty_scenes) > 1:
163
+ issues.append({
164
+ "type": "empty_scenes",
165
+ "severity": "info",
166
+ "message": "%d scene(s) have no clips across any track" % len(empty_scenes),
167
+ "details": empty_scenes[:10], # Cap at 10 to avoid huge payloads
168
+ })
169
+
170
+ # ── Return track checks ─────────────────────────────────────────────
171
+
172
+ soloed_returns = []
173
+ for i, track in enumerate(return_tracks):
174
+ if track.solo:
175
+ soloed_returns.append({"index": i, "name": track.name})
176
+
177
+ if soloed_returns:
178
+ issues.append({
179
+ "type": "soloed_returns",
180
+ "severity": "warning",
181
+ "message": "%d return track(s) soloed" % len(soloed_returns),
182
+ "details": soloed_returns,
183
+ })
184
+
185
+ # ── Summary ─────────────────────────────────────────────────────────
186
+
187
+ severity_counts = {"warning": 0, "info": 0}
188
+ for issue in issues:
189
+ severity_counts[issue["severity"]] = severity_counts.get(issue["severity"], 0) + 1
190
+
191
+ return {
192
+ "healthy": len(issues) == 0,
193
+ "issue_count": len(issues),
194
+ "warnings": severity_counts.get("warning", 0),
195
+ "info": severity_counts.get("info", 0),
196
+ "issues": issues,
197
+ "stats": stats,
198
+ }
@@ -0,0 +1,194 @@
1
+ """
2
+ LivePilot - Mixing domain handlers (8 commands).
3
+ """
4
+
5
+ from .router import register
6
+ from .utils import get_track
7
+
8
+
9
+ @register("set_track_volume")
10
+ def set_track_volume(song, params):
11
+ """Set the volume of a track."""
12
+ track_index = int(params["track_index"])
13
+ track = get_track(song, track_index)
14
+ volume = float(params["volume"])
15
+ track.mixer_device.volume.value = volume
16
+ return {"index": track_index, "volume": track.mixer_device.volume.value}
17
+
18
+
19
+ @register("set_track_pan")
20
+ def set_track_pan(song, params):
21
+ """Set the panning of a track."""
22
+ track_index = int(params["track_index"])
23
+ track = get_track(song, track_index)
24
+ pan = float(params["pan"])
25
+ track.mixer_device.panning.value = pan
26
+ return {"index": track_index, "pan": track.mixer_device.panning.value}
27
+
28
+
29
+ @register("set_track_send")
30
+ def set_track_send(song, params):
31
+ """Set a send value on a track."""
32
+ track_index = int(params["track_index"])
33
+ track = get_track(song, track_index)
34
+ send_index = int(params["send_index"])
35
+ sends = list(track.mixer_device.sends)
36
+ if send_index < 0 or send_index >= len(sends):
37
+ raise IndexError(
38
+ "Send index %d out of range (0..%d)"
39
+ % (send_index, len(sends) - 1)
40
+ )
41
+ sends[send_index].value = float(params["value"])
42
+ return {
43
+ "index": track_index,
44
+ "send_index": send_index,
45
+ "value": sends[send_index].value,
46
+ }
47
+
48
+
49
+ @register("get_return_tracks")
50
+ def get_return_tracks(song, params):
51
+ """Return info about all return tracks."""
52
+ result = []
53
+ for i, track in enumerate(song.return_tracks):
54
+ result.append({
55
+ "index": i,
56
+ "name": track.name,
57
+ "color_index": track.color_index,
58
+ "volume": track.mixer_device.volume.value,
59
+ "panning": track.mixer_device.panning.value,
60
+ })
61
+ return {"return_tracks": result}
62
+
63
+
64
+ @register("get_master_track")
65
+ def get_master_track(song, params):
66
+ """Return info about the master track."""
67
+ master = song.master_track
68
+ devices = []
69
+ for i, device in enumerate(master.devices):
70
+ devices.append({
71
+ "index": i,
72
+ "name": device.name,
73
+ "class_name": device.class_name,
74
+ "is_active": device.is_active,
75
+ })
76
+ return {
77
+ "name": master.name,
78
+ "volume": master.mixer_device.volume.value,
79
+ "panning": master.mixer_device.panning.value,
80
+ "devices": devices,
81
+ }
82
+
83
+
84
+ @register("set_master_volume")
85
+ def set_master_volume(song, params):
86
+ """Set the master track volume."""
87
+ volume = float(params["volume"])
88
+ song.master_track.mixer_device.volume.value = volume
89
+ return {"volume": song.master_track.mixer_device.volume.value}
90
+
91
+
92
+ @register("get_track_routing")
93
+ def get_track_routing(song, params):
94
+ """Get the input/output routing for a track."""
95
+ track_index = int(params["track_index"])
96
+ track = get_track(song, track_index)
97
+ result = {"index": track_index}
98
+ try:
99
+ result["input_routing_type"] = track.input_routing_type.display_name
100
+ except AttributeError:
101
+ result["input_routing_type"] = None
102
+ try:
103
+ result["input_routing_channel"] = track.input_routing_channel.display_name
104
+ except AttributeError:
105
+ result["input_routing_channel"] = None
106
+ try:
107
+ result["output_routing_type"] = track.output_routing_type.display_name
108
+ except AttributeError:
109
+ result["output_routing_type"] = None
110
+ try:
111
+ result["output_routing_channel"] = track.output_routing_channel.display_name
112
+ except AttributeError:
113
+ result["output_routing_channel"] = None
114
+ return result
115
+
116
+
117
+ @register("set_track_routing")
118
+ def set_track_routing(song, params):
119
+ """Set input/output routing for a track by display name."""
120
+ track_index = int(params["track_index"])
121
+ track = get_track(song, track_index)
122
+ if not any(k in params for k in ("input_type", "input_channel", "output_type", "output_channel")):
123
+ raise ValueError("At least one routing parameter must be provided")
124
+ result = {"index": track_index}
125
+
126
+ if "input_type" in params:
127
+ name = str(params["input_type"])
128
+ available = list(track.available_input_routing_types)
129
+ matched = None
130
+ for rt in available:
131
+ if rt.display_name == name:
132
+ matched = rt
133
+ break
134
+ if matched is None:
135
+ options = [rt.display_name for rt in available]
136
+ raise ValueError(
137
+ "Input routing type '%s' not found. Available: %s"
138
+ % (name, ", ".join(options))
139
+ )
140
+ track.input_routing_type = matched
141
+ result["input_routing_type"] = track.input_routing_type.display_name
142
+
143
+ if "input_channel" in params:
144
+ name = str(params["input_channel"])
145
+ available = list(track.available_input_routing_channels)
146
+ matched = None
147
+ for ch in available:
148
+ if ch.display_name == name:
149
+ matched = ch
150
+ break
151
+ if matched is None:
152
+ options = [ch.display_name for ch in available]
153
+ raise ValueError(
154
+ "Input routing channel '%s' not found. Available: %s"
155
+ % (name, ", ".join(options))
156
+ )
157
+ track.input_routing_channel = matched
158
+ result["input_routing_channel"] = track.input_routing_channel.display_name
159
+
160
+ if "output_type" in params:
161
+ name = str(params["output_type"])
162
+ available = list(track.available_output_routing_types)
163
+ matched = None
164
+ for rt in available:
165
+ if rt.display_name == name:
166
+ matched = rt
167
+ break
168
+ if matched is None:
169
+ options = [rt.display_name for rt in available]
170
+ raise ValueError(
171
+ "Output routing type '%s' not found. Available: %s"
172
+ % (name, ", ".join(options))
173
+ )
174
+ track.output_routing_type = matched
175
+ result["output_routing_type"] = track.output_routing_type.display_name
176
+
177
+ if "output_channel" in params:
178
+ name = str(params["output_channel"])
179
+ available = list(track.available_output_routing_channels)
180
+ matched = None
181
+ for ch in available:
182
+ if ch.display_name == name:
183
+ matched = ch
184
+ break
185
+ if matched is None:
186
+ options = [ch.display_name for ch in available]
187
+ raise ValueError(
188
+ "Output routing channel '%s' not found. Available: %s"
189
+ % (name, ", ".join(options))
190
+ )
191
+ track.output_routing_channel = matched
192
+ result["output_routing_channel"] = track.output_routing_channel.display_name
193
+
194
+ return result