laive-mcp 0.1.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 (67) hide show
  1. package/AGENTS.md +48 -0
  2. package/CHANGELOG.md +13 -0
  3. package/LICENSE +674 -0
  4. package/README.md +219 -0
  5. package/bin/laive.mjs +340 -0
  6. package/package.json +66 -0
  7. package/packages/als-parser/src/index.js +2 -0
  8. package/packages/als-parser/src/read.js +16 -0
  9. package/packages/als-parser/src/summarize.js +116 -0
  10. package/packages/common/src/index.js +3 -0
  11. package/packages/common/src/jsonl.js +41 -0
  12. package/packages/common/src/protocol.js +94 -0
  13. package/packages/common/src/validation.js +121 -0
  14. package/packages/live-bridge-remote-script/README.md +22 -0
  15. package/packages/live-bridge-remote-script/python/laive/__init__.py +7 -0
  16. package/packages/live-bridge-remote-script/python/laive/control_surface.py +208 -0
  17. package/packages/live-bridge-remote-script/python/laive/fake_live.py +168 -0
  18. package/packages/live-bridge-remote-script/python/laive/listeners.py +46 -0
  19. package/packages/live-bridge-remote-script/python/laive/live_access.py +272 -0
  20. package/packages/live-bridge-remote-script/python/laive/protocol.py +87 -0
  21. package/packages/live-bridge-remote-script/python/laive/server.py +130 -0
  22. package/packages/live-bridge-remote-script/python/laive/task_queue.py +47 -0
  23. package/packages/live-bridge-remote-script/src/bridge/client.js +113 -0
  24. package/packages/live-bridge-remote-script/src/bridge/server.js +189 -0
  25. package/packages/live-bridge-remote-script/src/cli/client.js +75 -0
  26. package/packages/live-bridge-remote-script/src/cli/server.js +51 -0
  27. package/packages/live-bridge-remote-script/src/fixtures/default-live-set.json +113 -0
  28. package/packages/live-bridge-remote-script/src/index.js +3 -0
  29. package/packages/live-bridge-remote-script/src/runtime/fixture-runtime.js +356 -0
  30. package/packages/live-sidecar-m4l/README.md +45 -0
  31. package/packages/live-sidecar-m4l/device/laive-sidecar.amxd +0 -0
  32. package/packages/live-sidecar-m4l/project/code/laive-sidecar-node.js +149 -0
  33. package/packages/live-sidecar-m4l/project/data/laive-sidecar.manifest.json +8 -0
  34. package/packages/live-sidecar-m4l/project/laive-sidecar.maxproj +36 -0
  35. package/packages/live-sidecar-m4l/project/patchers/laive-sidecar.maxpat +172 -0
  36. package/packages/live-sidecar-m4l/src/contracts.js +35 -0
  37. package/packages/live-sidecar-m4l/src/index.js +19 -0
  38. package/packages/live-sidecar-m4l/src/install-sidecar-device.js +15 -0
  39. package/packages/live-sidecar-m4l/src/package-sidecar.js +5 -0
  40. package/packages/live-sidecar-m4l/src/project.js +132 -0
  41. package/packages/live-sidecar-m4l/src/runtime.js +96 -0
  42. package/packages/live-sidecar-m4l/src/workflows.js +95 -0
  43. package/packages/mcp-server/src/cli.js +113 -0
  44. package/packages/mcp-server/src/default-tools.js +253 -0
  45. package/packages/mcp-server/src/errors.js +24 -0
  46. package/packages/mcp-server/src/index.js +10 -0
  47. package/packages/mcp-server/src/server.js +96 -0
  48. package/packages/mcp-server/src/session.js +475 -0
  49. package/packages/mcp-server/src/tool-registry.js +41 -0
  50. package/packages/state-engine/src/engine.js +566 -0
  51. package/packages/state-engine/src/ids.js +57 -0
  52. package/packages/state-engine/src/index.js +40 -0
  53. package/packages/state-engine/src/normalize.js +357 -0
  54. package/packages/state-engine/src/queries.js +154 -0
  55. package/packages/state-engine/src/replay.js +60 -0
  56. package/packages/ui-automation/src/executor.js +87 -0
  57. package/packages/ui-automation/src/guards.js +21 -0
  58. package/packages/ui-automation/src/helper.js +186 -0
  59. package/packages/ui-automation/src/index.js +20 -0
  60. package/packages/ui-automation/src/macos.js +82 -0
  61. package/packages/ui-automation/src/package-ui-helper.js +5 -0
  62. package/packages/ui-automation/src/workflows.js +72 -0
  63. package/scripts/install-remote-script.py +7 -0
  64. package/scripts/install-ui-helper.mjs +14 -0
  65. package/scripts/package-remote-script.py +7 -0
  66. package/scripts/package-ui-helper.mjs +4 -0
  67. package/scripts/remote_script_tooling.py +253 -0
@@ -0,0 +1,357 @@
1
+ import {
2
+ makeArrangementClipId,
3
+ makeDeviceId,
4
+ makeParameterId,
5
+ makeSceneId,
6
+ makeSessionClipId,
7
+ makeTrackId
8
+ } from "./ids.js";
9
+
10
+ function isoNow(value) {
11
+ if (typeof value === "string" && value.length > 0) {
12
+ return value;
13
+ }
14
+
15
+ return new Date().toISOString();
16
+ }
17
+
18
+ function toSourcePath(pathValue, fallback) {
19
+ if (typeof pathValue === "string" && pathValue.length > 0) {
20
+ return pathValue;
21
+ }
22
+
23
+ return fallback;
24
+ }
25
+
26
+ function bumpVersion(existingEntity) {
27
+ return existingEntity ? existingEntity.version + 1 : 1;
28
+ }
29
+
30
+ function createBaseEntity({
31
+ existingEntity,
32
+ id,
33
+ kind,
34
+ sourcePath,
35
+ observedAt,
36
+ source = "runtime"
37
+ }) {
38
+ return {
39
+ id,
40
+ kind,
41
+ source,
42
+ sourcePath,
43
+ version: bumpVersion(existingEntity),
44
+ lastObservedAt: isoNow(observedAt)
45
+ };
46
+ }
47
+
48
+ function asArray(value) {
49
+ return Array.isArray(value) ? value : [];
50
+ }
51
+
52
+ function pickFirst(...values) {
53
+ for (const value of values) {
54
+ if (value !== undefined) {
55
+ return value;
56
+ }
57
+ }
58
+
59
+ return undefined;
60
+ }
61
+
62
+ export function normalizeApplication(snapshot, existingEntity, options = {}) {
63
+ const observedAt = isoNow(options.observedAt ?? snapshot.observed_at);
64
+ return {
65
+ ...createBaseEntity({
66
+ existingEntity,
67
+ id: "application",
68
+ kind: "application",
69
+ sourcePath: "application",
70
+ observedAt
71
+ }),
72
+ name: snapshot.name ?? "Ableton Live",
73
+ versionLabel: pickFirst(snapshot.version, snapshot.versionLabel) ?? "unknown",
74
+ majorVersion: pickFirst(snapshot.major_version, snapshot.majorVersion) ?? null,
75
+ minorVersion: pickFirst(snapshot.minor_version, snapshot.minorVersion) ?? null,
76
+ bugfixVersion: pickFirst(snapshot.bugfix_version, snapshot.bugfixVersion) ?? null,
77
+ mode: snapshot.mode ?? null
78
+ };
79
+ }
80
+
81
+ export function normalizeSong(snapshot, existingEntity, options = {}) {
82
+ const observedAt = isoNow(options.observedAt ?? snapshot.observed_at);
83
+ return {
84
+ ...createBaseEntity({
85
+ existingEntity,
86
+ id: "song",
87
+ kind: "song",
88
+ sourcePath: "song",
89
+ observedAt
90
+ }),
91
+ name: snapshot.name ?? "Untitled Set",
92
+ tempo: snapshot.tempo ?? null,
93
+ timeSignatureNumerator:
94
+ pickFirst(snapshot.time_signature_numerator, snapshot.timeSignatureNumerator) ?? null,
95
+ timeSignatureDenominator:
96
+ pickFirst(snapshot.time_signature_denominator, snapshot.timeSignatureDenominator) ?? null,
97
+ isPlaying: Boolean(pickFirst(snapshot.is_playing, snapshot.isPlaying)),
98
+ isRecording: Boolean(pickFirst(snapshot.is_recording, snapshot.isRecording)),
99
+ overdub: Boolean(snapshot.overdub),
100
+ metronome: Boolean(snapshot.metronome),
101
+ loopEnabled: Boolean(pickFirst(snapshot.loop_enabled, snapshot.loopEnabled)),
102
+ arrangementPositionBeats:
103
+ pickFirst(snapshot.arrangement_position_beats, snapshot.arrangementPositionBeats) ?? null,
104
+ clipTriggerQuantization:
105
+ pickFirst(snapshot.clip_trigger_quantization, snapshot.clipTriggerQuantization) ?? null,
106
+ midiRecordingQuantization:
107
+ pickFirst(snapshot.midi_recording_quantization, snapshot.midiRecordingQuantization) ?? null
108
+ };
109
+ }
110
+
111
+ export function normalizeSelection(snapshot, existingEntity, options = {}) {
112
+ const observedAt = isoNow(options.observedAt ?? snapshot.observed_at);
113
+ const selectedTrackId =
114
+ snapshot.selected_track_id ??
115
+ snapshot.selectedTrackId ??
116
+ (snapshot.selected_track
117
+ ? makeTrackId(
118
+ snapshot.selected_track.section ?? "visible",
119
+ snapshot.selected_track.index ?? 0
120
+ )
121
+ : null);
122
+
123
+ const selectedSceneId =
124
+ snapshot.selected_scene_id ??
125
+ snapshot.selectedSceneId ??
126
+ (Number.isInteger(snapshot.selected_scene_index)
127
+ ? makeSceneId(snapshot.selected_scene_index)
128
+ : null);
129
+
130
+ return {
131
+ ...createBaseEntity({
132
+ existingEntity,
133
+ id: "selection",
134
+ kind: "selection",
135
+ sourcePath: "selection",
136
+ observedAt
137
+ }),
138
+ selectedTrackId,
139
+ selectedSceneId,
140
+ selectedClipId: pickFirst(snapshot.selected_clip_id, snapshot.selectedClipId) ?? null,
141
+ selectedDeviceId: pickFirst(snapshot.selected_device_id, snapshot.selectedDeviceId) ?? null,
142
+ detailView: pickFirst(snapshot.detail_view, snapshot.detailView) ?? null,
143
+ browserVisible: Boolean(pickFirst(snapshot.browser_visible, snapshot.browserVisible))
144
+ };
145
+ }
146
+
147
+ export function normalizeCapabilities(snapshot, existingEntity, options = {}) {
148
+ const observedAt = isoNow(options.observedAt ?? snapshot.observed_at);
149
+ return {
150
+ ...createBaseEntity({
151
+ existingEntity,
152
+ id: "capabilities",
153
+ kind: "capabilities",
154
+ sourcePath: "capabilities",
155
+ observedAt
156
+ }),
157
+ runtimeVersion: pickFirst(snapshot.runtime_version, snapshot.runtimeVersion) ?? null,
158
+ supportedCommands: asArray(
159
+ pickFirst(snapshot.supported_commands, snapshot.supportedCommands)
160
+ ),
161
+ supportedEvents: asArray(pickFirst(snapshot.supported_events, snapshot.supportedEvents)),
162
+ features: snapshot.features ?? {}
163
+ };
164
+ }
165
+
166
+ export function normalizeTrack(track, existingEntity, options = {}) {
167
+ const section = pickFirst(track.section, track.kind, track.trackSection) ?? "visible";
168
+ const index = pickFirst(track.track_index, track.trackIndex, track.index) ?? 0;
169
+ const trackId = track.id ?? makeTrackId(section, index);
170
+ const observedAt = isoNow(options.observedAt ?? track.observed_at);
171
+
172
+ return {
173
+ ...createBaseEntity({
174
+ existingEntity,
175
+ id: trackId,
176
+ kind: "track",
177
+ sourcePath: toSourcePath(track.path, `song.tracks.${section}.${index}`),
178
+ observedAt
179
+ }),
180
+ section,
181
+ index,
182
+ name: track.name ?? `Track ${index + 1}`,
183
+ color: track.color ?? null,
184
+ isGroup: Boolean(pickFirst(track.is_group, track.isGroup)),
185
+ groupTrackId: pickFirst(track.group_track_id, track.groupTrackId) ?? null,
186
+ armed: Boolean(track.armed),
187
+ muted: Boolean(track.muted),
188
+ soloed: Boolean(track.soloed),
189
+ frozen: Boolean(track.frozen),
190
+ monitoringState: pickFirst(track.monitoring_state, track.monitoringState) ?? null,
191
+ playingSlotIndex: pickFirst(track.playing_slot_index, track.playingSlotIndex) ?? null,
192
+ clipSlotCount:
193
+ pickFirst(track.clip_slot_count, track.clipSlotCount) ?? asArray(track.session_clips).length,
194
+ arrangementClipCount:
195
+ pickFirst(track.arrangement_clip_count, track.arrangementClipCount) ??
196
+ asArray(track.arrangement_clips).length,
197
+ sessionClipIds: [],
198
+ arrangementClipIds: [],
199
+ deviceIds: []
200
+ };
201
+ }
202
+
203
+ export function normalizeScene(scene, existingEntity, options = {}) {
204
+ const index = pickFirst(scene.scene_index, scene.sceneIndex, scene.index) ?? 0;
205
+ const sceneId = scene.id ?? makeSceneId(index);
206
+ const observedAt = isoNow(options.observedAt ?? scene.observed_at);
207
+
208
+ return {
209
+ ...createBaseEntity({
210
+ existingEntity,
211
+ id: sceneId,
212
+ kind: "scene",
213
+ sourcePath: toSourcePath(scene.path, `song.scenes.${index}`),
214
+ observedAt
215
+ }),
216
+ index,
217
+ name: scene.name ?? `Scene ${index + 1}`,
218
+ color: scene.color ?? null,
219
+ isTriggered: Boolean(pickFirst(scene.is_triggered, scene.isTriggered))
220
+ };
221
+ }
222
+
223
+ export function normalizeClip(clip, trackId, existingEntity, options = {}) {
224
+ const slotIndex = pickFirst(clip.slot_index, clip.slotIndex, clip.scene_index);
225
+ const clipIndex = pickFirst(clip.arrangement_index, clip.arrangementIndex, clip.index) ?? 0;
226
+ const location = clip.location ?? (Number.isInteger(slotIndex) ? "session" : "arrangement");
227
+ const clipId =
228
+ clip.id ??
229
+ (location === "session"
230
+ ? makeSessionClipId(trackId, slotIndex ?? 0)
231
+ : makeArrangementClipId(trackId, clipIndex));
232
+ const observedAt = isoNow(options.observedAt ?? clip.observed_at);
233
+
234
+ return {
235
+ ...createBaseEntity({
236
+ existingEntity,
237
+ id: clipId,
238
+ kind: "clip",
239
+ sourcePath:
240
+ location === "session"
241
+ ? toSourcePath(clip.path, `${trackId}.session_clips.${slotIndex ?? 0}`)
242
+ : toSourcePath(clip.path, `${trackId}.arrangement_clips.${clipIndex}`),
243
+ observedAt
244
+ }),
245
+ trackId,
246
+ location,
247
+ slotIndex: location === "session" ? slotIndex ?? 0 : null,
248
+ index: location === "arrangement" ? clipIndex : null,
249
+ name: clip.name ?? null,
250
+ color: clip.color ?? null,
251
+ isMidi: pickFirst(clip.is_midi, clip.isMidi) ?? null,
252
+ isAudio: pickFirst(clip.is_audio, clip.isAudio) ?? null,
253
+ isPlaying: Boolean(pickFirst(clip.is_playing, clip.isPlaying)),
254
+ isTriggered: Boolean(pickFirst(clip.is_triggered, clip.isTriggered)),
255
+ isRecording: Boolean(pickFirst(clip.is_recording, clip.isRecording)),
256
+ startBeats: pickFirst(clip.start_beats, clip.startBeats) ?? null,
257
+ endBeats: pickFirst(clip.end_beats, clip.endBeats) ?? null,
258
+ loopStartBeats: pickFirst(clip.loop_start_beats, clip.loopStartBeats) ?? null,
259
+ loopEndBeats: pickFirst(clip.loop_end_beats, clip.loopEndBeats) ?? null,
260
+ noteCount: pickFirst(clip.note_count, clip.noteCount) ?? null
261
+ };
262
+ }
263
+
264
+ export function normalizeDevice(device, trackId, existingEntity, options = {}) {
265
+ const index = pickFirst(device.device_index, device.deviceIndex, device.index) ?? 0;
266
+ const deviceId = device.id ?? makeDeviceId(trackId, index);
267
+ const observedAt = isoNow(options.observedAt ?? device.observed_at);
268
+
269
+ return {
270
+ ...createBaseEntity({
271
+ existingEntity,
272
+ id: deviceId,
273
+ kind: "device",
274
+ sourcePath: toSourcePath(device.path, `${trackId}.devices.${index}`),
275
+ observedAt
276
+ }),
277
+ trackId,
278
+ index,
279
+ name: device.name ?? `Device ${index + 1}`,
280
+ className: pickFirst(device.class_name, device.className) ?? null,
281
+ type: device.type ?? null,
282
+ canHaveChains: Boolean(pickFirst(device.can_have_chains, device.canHaveChains)),
283
+ isSelected: Boolean(pickFirst(device.is_selected, device.isSelected)),
284
+ isEnabled: pickFirst(device.is_enabled, device.isEnabled) ?? null,
285
+ parameterIds: []
286
+ };
287
+ }
288
+
289
+ export function normalizeParameter(parameter, deviceId, existingEntity, options = {}) {
290
+ const index =
291
+ pickFirst(parameter.parameter_index, parameter.parameterIndex, parameter.index) ?? 0;
292
+ const parameterId = parameter.id ?? makeParameterId(deviceId, index);
293
+ const observedAt = isoNow(options.observedAt ?? parameter.observed_at);
294
+
295
+ return {
296
+ ...createBaseEntity({
297
+ existingEntity,
298
+ id: parameterId,
299
+ kind: "parameter",
300
+ sourcePath: toSourcePath(parameter.path, `${deviceId}.parameters.${index}`),
301
+ observedAt
302
+ }),
303
+ deviceId,
304
+ index,
305
+ name: parameter.name ?? `Parameter ${index + 1}`,
306
+ value: parameter.value ?? null,
307
+ min: parameter.min ?? null,
308
+ max: parameter.max ?? null,
309
+ isQuantized: Boolean(pickFirst(parameter.is_quantized, parameter.isQuantized)),
310
+ displayValue: pickFirst(parameter.display_value, parameter.displayValue) ?? null,
311
+ unit: parameter.unit ?? null
312
+ };
313
+ }
314
+
315
+ export function normalizeTrackBundle(track, existingState = {}, options = {}) {
316
+ const normalizedTrack = normalizeTrack(track, existingState.track, options);
317
+
318
+ const sessionClips = asArray(track.session_clips).map((clip) =>
319
+ normalizeClip(clip, normalizedTrack.id, existingState.clips?.[clip.id], options)
320
+ );
321
+ const arrangementClips = asArray(track.arrangement_clips).map((clip) =>
322
+ normalizeClip(clip, normalizedTrack.id, existingState.clips?.[clip.id], options)
323
+ );
324
+ const devices = asArray(track.devices).map((device) => {
325
+ const normalizedDevice = normalizeDevice(
326
+ device,
327
+ normalizedTrack.id,
328
+ existingState.devices?.[device.id],
329
+ options
330
+ );
331
+ const parameters = asArray(device.parameters).map((parameter) =>
332
+ normalizeParameter(
333
+ parameter,
334
+ normalizedDevice.id,
335
+ existingState.parameters?.[parameter.id],
336
+ options
337
+ )
338
+ );
339
+
340
+ normalizedDevice.parameterIds = parameters.map((parameter) => parameter.id);
341
+ return {
342
+ device: normalizedDevice,
343
+ parameters
344
+ };
345
+ });
346
+
347
+ normalizedTrack.sessionClipIds = sessionClips.map((clip) => clip.id);
348
+ normalizedTrack.arrangementClipIds = arrangementClips.map((clip) => clip.id);
349
+ normalizedTrack.deviceIds = devices.map(({ device }) => device.id);
350
+
351
+ return {
352
+ track: normalizedTrack,
353
+ sessionClips,
354
+ arrangementClips,
355
+ devices
356
+ };
357
+ }
@@ -0,0 +1,154 @@
1
+ function valuesInOrder(ids, collection) {
2
+ return ids.map((id) => collection[id]).filter(Boolean);
3
+ }
4
+
5
+ export function summarizeProject(state) {
6
+ const tracks = valuesInOrder(state.trackOrder, state.tracks);
7
+ const scenes = valuesInOrder(state.sceneOrder, state.scenes);
8
+ const clips = Object.values(state.clips);
9
+ const playingClips = clips.filter((clip) => clip.isPlaying);
10
+
11
+ return {
12
+ snapshotVersion: state.meta.snapshotVersion,
13
+ lastUpdatedAt: state.meta.lastUpdatedAt,
14
+ dirtyPaths: [...state.meta.dirtyPaths],
15
+ application: state.application
16
+ ? {
17
+ versionLabel: state.application.versionLabel,
18
+ mode: state.application.mode
19
+ }
20
+ : null,
21
+ song: state.song
22
+ ? {
23
+ name: state.song.name,
24
+ tempo: state.song.tempo,
25
+ isPlaying: state.song.isPlaying,
26
+ isRecording: state.song.isRecording
27
+ }
28
+ : null,
29
+ counts: {
30
+ tracks: tracks.length,
31
+ visibleTracks: state.visibleTrackIds.length,
32
+ returnTracks: state.returnTrackIds.length,
33
+ scenes: scenes.length,
34
+ clips: clips.length,
35
+ devices: Object.keys(state.devices).length,
36
+ parameters: Object.keys(state.parameters).length,
37
+ playingClips: playingClips.length
38
+ },
39
+ playingClips: playingClips.map((clip) => ({
40
+ id: clip.id,
41
+ name: clip.name,
42
+ trackId: clip.trackId,
43
+ location: clip.location,
44
+ slotIndex: clip.slotIndex,
45
+ index: clip.index
46
+ }))
47
+ };
48
+ }
49
+
50
+ export function getSelectedContext(state) {
51
+ const selection = state.selection;
52
+
53
+ if (!selection) {
54
+ return null;
55
+ }
56
+
57
+ const track = selection.selectedTrackId
58
+ ? state.tracks[selection.selectedTrackId] ?? null
59
+ : null;
60
+ const clip = selection.selectedClipId
61
+ ? state.clips[selection.selectedClipId] ?? null
62
+ : null;
63
+ const device = selection.selectedDeviceId
64
+ ? state.devices[selection.selectedDeviceId] ?? null
65
+ : null;
66
+ const scene = selection.selectedSceneId
67
+ ? state.scenes[selection.selectedSceneId] ?? null
68
+ : null;
69
+
70
+ return {
71
+ selection,
72
+ track,
73
+ clip,
74
+ device,
75
+ scene
76
+ };
77
+ }
78
+
79
+ export function findTrack(state, query) {
80
+ const searchValue = String(query ?? "").trim().toLowerCase();
81
+
82
+ if (!searchValue) {
83
+ return null;
84
+ }
85
+
86
+ if (state.tracks[searchValue]) {
87
+ return state.tracks[searchValue];
88
+ }
89
+
90
+ return Object.values(state.tracks).find((track) => {
91
+ if (String(track.index) === searchValue) {
92
+ return true;
93
+ }
94
+
95
+ return track.name.toLowerCase() === searchValue;
96
+ }) ?? null;
97
+ }
98
+
99
+ export function listPlayingClips(state) {
100
+ return Object.values(state.clips)
101
+ .filter((clip) => clip.isPlaying)
102
+ .map((clip) => ({
103
+ clip,
104
+ track: state.tracks[clip.trackId] ?? null
105
+ }));
106
+ }
107
+
108
+ export function getTrackDetails(state, trackId) {
109
+ const track = state.tracks[trackId] ?? null;
110
+
111
+ if (!track) {
112
+ return null;
113
+ }
114
+
115
+ const sessionClips = valuesInOrder(track.sessionClipIds, state.clips);
116
+ const arrangementClips = valuesInOrder(track.arrangementClipIds, state.clips);
117
+ const devices = valuesInOrder(track.deviceIds, state.devices).map((device) => ({
118
+ ...device,
119
+ parameters: valuesInOrder(device.parameterIds, state.parameters)
120
+ }));
121
+
122
+ return {
123
+ track,
124
+ sessionClips,
125
+ arrangementClips,
126
+ devices
127
+ };
128
+ }
129
+
130
+ export function searchEntities(state, query) {
131
+ const searchValue = String(query ?? "").trim().toLowerCase();
132
+
133
+ if (!searchValue) {
134
+ return [];
135
+ }
136
+
137
+ const collections = [
138
+ ...Object.values(state.tracks),
139
+ ...Object.values(state.scenes),
140
+ ...Object.values(state.clips),
141
+ ...Object.values(state.devices),
142
+ ...Object.values(state.parameters)
143
+ ];
144
+
145
+ return collections.filter((entity) => {
146
+ if (entity.id.toLowerCase().includes(searchValue)) {
147
+ return true;
148
+ }
149
+
150
+ return String(entity.name ?? "")
151
+ .toLowerCase()
152
+ .includes(searchValue);
153
+ });
154
+ }
@@ -0,0 +1,60 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { applyEvent, applySnapshot, createInitialState } from "./engine.js";
3
+
4
+ function parseTraceText(text) {
5
+ const trimmed = text.trim();
6
+
7
+ if (!trimmed) {
8
+ return [];
9
+ }
10
+
11
+ if (trimmed.startsWith("[")) {
12
+ return JSON.parse(trimmed);
13
+ }
14
+
15
+ return trimmed
16
+ .split("\n")
17
+ .map((line) => line.trim())
18
+ .filter(Boolean)
19
+ .map((line) => JSON.parse(line));
20
+ }
21
+
22
+ export async function loadTraceFile(filePath) {
23
+ const content = await readFile(filePath, "utf8");
24
+ return parseTraceText(content);
25
+ }
26
+
27
+ export function replayTrace(entries, initialState = createInitialState()) {
28
+ let currentState = structuredClone(initialState);
29
+ const history = [];
30
+
31
+ for (const entry of entries) {
32
+ if (entry.type === "snapshot") {
33
+ currentState = applySnapshot(currentState, entry.payload, {
34
+ observedAt: entry.observed_at
35
+ });
36
+ } else if (entry.type === "event") {
37
+ currentState = applyEvent(
38
+ currentState,
39
+ {
40
+ event: entry.event,
41
+ name: entry.name,
42
+ payload: entry.payload,
43
+ observed_at: entry.observed_at
44
+ },
45
+ {
46
+ observedAt: entry.observed_at
47
+ }
48
+ );
49
+ }
50
+
51
+ history.push(structuredClone(currentState));
52
+ }
53
+
54
+ return {
55
+ state: currentState,
56
+ history
57
+ };
58
+ }
59
+
60
+ export { parseTraceText };
@@ -0,0 +1,87 @@
1
+ import {
2
+ activateApplication,
3
+ clickMenuPath,
4
+ getFrontmostApplication,
5
+ sendKeystroke,
6
+ typeText
7
+ } from "./macos.js";
8
+ import { assertSupportedLiveWindow, assertWorkflowAllowed } from "./guards.js";
9
+ import { getWorkflow } from "./workflows.js";
10
+
11
+ function resolveParameter(step, parameters) {
12
+ if (!step.parameter) {
13
+ return step.value;
14
+ }
15
+
16
+ if (!(step.parameter in parameters)) {
17
+ throw new Error(`Missing workflow parameter: ${step.parameter}`);
18
+ }
19
+
20
+ return parameters[step.parameter];
21
+ }
22
+
23
+ export function materializeWorkflow(name, parameters = {}) {
24
+ const workflow = getWorkflow(name);
25
+ const materializedSteps = workflow.steps.map((step) => ({
26
+ ...step,
27
+ resolvedValue: resolveParameter(step, parameters)
28
+ }));
29
+
30
+ return {
31
+ ...workflow,
32
+ steps: materializedSteps
33
+ };
34
+ }
35
+
36
+ export async function captureContext() {
37
+ return getFrontmostApplication();
38
+ }
39
+
40
+ export async function executeWorkflow(name, parameters = {}, options = {}) {
41
+ const workflow = materializeWorkflow(name, parameters);
42
+ assertWorkflowAllowed(workflow);
43
+
44
+ const context = options.context ?? (await captureContext());
45
+ if (workflow.guards.includes("app:ableton-live-frontmost")) {
46
+ assertSupportedLiveWindow(context);
47
+ }
48
+
49
+ const executedSteps = [];
50
+
51
+ for (const step of workflow.steps) {
52
+ switch (step.type) {
53
+ case "activate_app":
54
+ await activateApplication(step.appName);
55
+ break;
56
+ case "menu_click":
57
+ await clickMenuPath("Ableton Live", step.menuPath);
58
+ break;
59
+ case "keystroke":
60
+ await sendKeystroke(step.value, step.modifiers);
61
+ break;
62
+ case "type_text":
63
+ await typeText(step.resolvedValue);
64
+ break;
65
+ case "capture_context":
66
+ break;
67
+ case "focus_section":
68
+ case "set_text_field":
69
+ case "press_button":
70
+ case "wait_for_window":
71
+ break;
72
+ default:
73
+ throw new Error(`Unsupported workflow step: ${step.type}`);
74
+ }
75
+
76
+ executedSteps.push({
77
+ type: step.type,
78
+ resolvedValue: step.resolvedValue ?? null
79
+ });
80
+ }
81
+
82
+ return {
83
+ workflow: workflow.name,
84
+ context,
85
+ executedSteps
86
+ };
87
+ }
@@ -0,0 +1,21 @@
1
+ export function assertMacOS() {
2
+ if (process.platform !== "darwin") {
3
+ throw new Error("UI automation is currently supported on macOS only.");
4
+ }
5
+ }
6
+
7
+ export function assertSupportedLiveWindow(windowContext) {
8
+ if (!windowContext?.isFrontmost) {
9
+ throw new Error("Ableton Live must be the frontmost application for UI fallback.");
10
+ }
11
+
12
+ if (!windowContext?.appName?.includes("Live")) {
13
+ throw new Error("Focused application is not Ableton Live.");
14
+ }
15
+ }
16
+
17
+ export function assertWorkflowAllowed(workflow) {
18
+ if (!workflow?.allowFallback) {
19
+ throw new Error(`Workflow ${workflow?.name ?? "unknown"} is not allowed in fallback mode.`);
20
+ }
21
+ }