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,151 @@
1
+ """
2
+ LivePilot - Transport domain handlers (10 commands).
3
+ """
4
+
5
+ from .router import register
6
+
7
+
8
+ @register("get_session_info")
9
+ def get_session_info(song, params):
10
+ """Return comprehensive session state."""
11
+ tracks_info = []
12
+ for i, track in enumerate(song.tracks):
13
+ tracks_info.append({
14
+ "index": i,
15
+ "name": track.name,
16
+ "color_index": track.color_index,
17
+ "has_midi_input": track.has_midi_input,
18
+ "has_audio_input": track.has_audio_input,
19
+ "mute": track.mute,
20
+ "solo": track.solo,
21
+ "arm": track.arm,
22
+ })
23
+
24
+ return_tracks_info = []
25
+ for i, track in enumerate(song.return_tracks):
26
+ return_tracks_info.append({
27
+ "index": i,
28
+ "name": track.name,
29
+ "color_index": track.color_index,
30
+ "mute": track.mute,
31
+ "solo": track.solo,
32
+ })
33
+
34
+ scenes_info = []
35
+ for i, scene in enumerate(song.scenes):
36
+ scenes_info.append({
37
+ "index": i,
38
+ "name": scene.name,
39
+ "color_index": scene.color_index,
40
+ "tempo": scene.tempo if scene.tempo > 0 else None,
41
+ })
42
+
43
+ return {
44
+ "tempo": song.tempo,
45
+ "signature_numerator": song.signature_numerator,
46
+ "signature_denominator": song.signature_denominator,
47
+ "is_playing": song.is_playing,
48
+ "song_length": song.song_length,
49
+ "current_song_time": song.current_song_time,
50
+ "loop": song.loop,
51
+ "loop_start": song.loop_start,
52
+ "loop_length": song.loop_length,
53
+ "metronome": song.metronome,
54
+ "record_mode": song.record_mode,
55
+ "session_record": song.session_record,
56
+ "track_count": len(list(song.tracks)),
57
+ "return_track_count": len(list(song.return_tracks)),
58
+ "scene_count": len(list(song.scenes)),
59
+ "tracks": tracks_info,
60
+ "return_tracks": return_tracks_info,
61
+ "scenes": scenes_info,
62
+ }
63
+
64
+
65
+ @register("set_tempo")
66
+ def set_tempo(song, params):
67
+ """Set the song tempo in BPM."""
68
+ tempo = float(params["tempo"])
69
+ if tempo < 20 or tempo > 999:
70
+ raise ValueError("Tempo must be between 20 and 999 BPM")
71
+ song.tempo = tempo
72
+ return {"tempo": song.tempo}
73
+
74
+
75
+ @register("set_time_signature")
76
+ def set_time_signature(song, params):
77
+ """Set the song time signature."""
78
+ numerator = int(params["numerator"])
79
+ denominator = int(params["denominator"])
80
+ if numerator < 1 or numerator > 99:
81
+ raise ValueError("Numerator must be between 1 and 99")
82
+ if denominator not in (1, 2, 4, 8, 16):
83
+ raise ValueError("Denominator must be 1, 2, 4, 8, or 16")
84
+ song.signature_numerator = numerator
85
+ song.signature_denominator = denominator
86
+ return {
87
+ "signature_numerator": song.signature_numerator,
88
+ "signature_denominator": song.signature_denominator,
89
+ }
90
+
91
+
92
+ @register("start_playback")
93
+ def start_playback(song, params):
94
+ """Start playback from the beginning."""
95
+ song.start_playing()
96
+ return {"is_playing": True}
97
+
98
+
99
+ @register("stop_playback")
100
+ def stop_playback(song, params):
101
+ """Stop playback."""
102
+ song.stop_playing()
103
+ return {"is_playing": False}
104
+
105
+
106
+ @register("continue_playback")
107
+ def continue_playback(song, params):
108
+ """Continue playback from the current position."""
109
+ song.continue_playing()
110
+ return {"is_playing": True}
111
+
112
+
113
+ @register("toggle_metronome")
114
+ def toggle_metronome(song, params):
115
+ """Enable or disable the metronome."""
116
+ enabled = bool(params["enabled"])
117
+ song.metronome = enabled
118
+ return {"metronome": song.metronome}
119
+
120
+
121
+ @register("set_session_loop")
122
+ def set_session_loop(song, params):
123
+ """Enable/disable loop and optionally set loop start/length."""
124
+ # Set region FIRST — setting loop_start/loop_length can reset song.loop
125
+ if "loop_start" in params:
126
+ song.loop_start = float(params["loop_start"])
127
+ if "loop_length" in params:
128
+ song.loop_length = float(params["loop_length"])
129
+ # Set enabled LAST so it sticks
130
+ enabled = bool(params["enabled"])
131
+ song.loop = enabled
132
+ # Echo requested value — song.loop getter may return stale state
133
+ return {
134
+ "loop": enabled,
135
+ "loop_start": song.loop_start,
136
+ "loop_length": song.loop_length,
137
+ }
138
+
139
+
140
+ @register("undo")
141
+ def undo(song, params):
142
+ """Undo the last action."""
143
+ song.undo()
144
+ return {"undone": True}
145
+
146
+
147
+ @register("redo")
148
+ def redo(song, params):
149
+ """Redo the last undone action."""
150
+ song.redo()
151
+ return {"redone": True}
@@ -0,0 +1,123 @@
1
+ """
2
+ LivePilot - Error formatting, validation helpers, and JSON serialization.
3
+ """
4
+
5
+ import json
6
+
7
+
8
+ # ── Error codes ──────────────────────────────────────────────────────────────
9
+
10
+ INDEX_ERROR = "INDEX_ERROR"
11
+ NOT_FOUND = "NOT_FOUND"
12
+ INVALID_PARAM = "INVALID_PARAM"
13
+ STATE_ERROR = "STATE_ERROR"
14
+ TIMEOUT = "TIMEOUT"
15
+ INTERNAL = "INTERNAL"
16
+
17
+
18
+ # ── Response builders ────────────────────────────────────────────────────────
19
+
20
+ def success_response(request_id, result=None):
21
+ """Build a success JSON-RPC-style response."""
22
+ resp = {"id": request_id, "ok": True}
23
+ if result is not None:
24
+ resp["result"] = result
25
+ return resp
26
+
27
+
28
+ def error_response(request_id, message, code=INTERNAL):
29
+ """Build an error JSON-RPC-style response."""
30
+ return {
31
+ "id": request_id,
32
+ "ok": False,
33
+ "error": {
34
+ "code": code,
35
+ "message": str(message),
36
+ },
37
+ }
38
+
39
+
40
+ # ── Validation helpers ───────────────────────────────────────────────────────
41
+
42
+ MASTER_TRACK_INDEX = -1000
43
+
44
+
45
+ def get_track(song, track_index):
46
+ """Return a track by index (0-based). Supports negative indices for
47
+ return tracks: -1 = first return, -2 = second return, etc.
48
+ Use -1000 for the master track."""
49
+ if track_index == MASTER_TRACK_INDEX:
50
+ return song.master_track
51
+ tracks = list(song.tracks)
52
+ if track_index < 0:
53
+ return_tracks = list(song.return_tracks)
54
+ ri = abs(track_index) - 1
55
+ if ri >= len(return_tracks):
56
+ raise IndexError(
57
+ "Return track index %d out of range (0..%d)"
58
+ % (ri, len(return_tracks) - 1)
59
+ )
60
+ return return_tracks[ri]
61
+ if track_index >= len(tracks):
62
+ raise IndexError(
63
+ "Track index %d out of range (0..%d)"
64
+ % (track_index, len(tracks) - 1)
65
+ )
66
+ return tracks[track_index]
67
+
68
+
69
+ def get_clip_slot(song, track_index, clip_index):
70
+ """Return a clip slot by track + slot index."""
71
+ track = get_track(song, track_index)
72
+ slots = list(track.clip_slots)
73
+ if clip_index < 0 or clip_index >= len(slots):
74
+ raise IndexError(
75
+ "Clip slot index %d out of range (0..%d)"
76
+ % (clip_index, len(slots) - 1)
77
+ )
78
+ return slots[clip_index]
79
+
80
+
81
+ def get_clip(song, track_index, clip_index):
82
+ """Return a clip (must exist in the slot)."""
83
+ slot = get_clip_slot(song, track_index, clip_index)
84
+ clip = slot.clip
85
+ if clip is None:
86
+ raise ValueError(
87
+ "No clip in track %d, slot %d" % (track_index, clip_index)
88
+ )
89
+ return clip
90
+
91
+
92
+ def get_device(track, device_index):
93
+ """Return a device from a track by index."""
94
+ devices = list(track.devices)
95
+ if not devices:
96
+ raise IndexError(
97
+ "Track '%s' has no devices — load an instrument or effect first"
98
+ % track.name
99
+ )
100
+ if device_index < 0 or device_index >= len(devices):
101
+ raise IndexError(
102
+ "Device index %d out of range (0..%d) on track '%s'"
103
+ % (device_index, len(devices) - 1, track.name)
104
+ )
105
+ return devices[device_index]
106
+
107
+
108
+ def get_scene(song, scene_index):
109
+ """Return a scene by index."""
110
+ scenes = list(song.scenes)
111
+ if scene_index < 0 or scene_index >= len(scenes):
112
+ raise IndexError(
113
+ "Scene index %d out of range (0..%d)"
114
+ % (scene_index, len(scenes) - 1)
115
+ )
116
+ return scenes[scene_index]
117
+
118
+
119
+ # ── JSON serialization ───────────────────────────────────────────────────────
120
+
121
+ def serialize_json(data):
122
+ """Serialize *data* to a compact JSON string with a trailing newline."""
123
+ return json.dumps(data, separators=(",", ":"), ensure_ascii=True) + "\n"
@@ -0,0 +1,2 @@
1
+ # LivePilot MCP Server dependencies
2
+ fastmcp>=3.0.0,<4.0.0