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.
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +674 -0
- package/README.md +219 -0
- package/bin/laive.mjs +340 -0
- package/package.json +66 -0
- package/packages/als-parser/src/index.js +2 -0
- package/packages/als-parser/src/read.js +16 -0
- package/packages/als-parser/src/summarize.js +116 -0
- package/packages/common/src/index.js +3 -0
- package/packages/common/src/jsonl.js +41 -0
- package/packages/common/src/protocol.js +94 -0
- package/packages/common/src/validation.js +121 -0
- package/packages/live-bridge-remote-script/README.md +22 -0
- package/packages/live-bridge-remote-script/python/laive/__init__.py +7 -0
- package/packages/live-bridge-remote-script/python/laive/control_surface.py +208 -0
- package/packages/live-bridge-remote-script/python/laive/fake_live.py +168 -0
- package/packages/live-bridge-remote-script/python/laive/listeners.py +46 -0
- package/packages/live-bridge-remote-script/python/laive/live_access.py +272 -0
- package/packages/live-bridge-remote-script/python/laive/protocol.py +87 -0
- package/packages/live-bridge-remote-script/python/laive/server.py +130 -0
- package/packages/live-bridge-remote-script/python/laive/task_queue.py +47 -0
- package/packages/live-bridge-remote-script/src/bridge/client.js +113 -0
- package/packages/live-bridge-remote-script/src/bridge/server.js +189 -0
- package/packages/live-bridge-remote-script/src/cli/client.js +75 -0
- package/packages/live-bridge-remote-script/src/cli/server.js +51 -0
- package/packages/live-bridge-remote-script/src/fixtures/default-live-set.json +113 -0
- package/packages/live-bridge-remote-script/src/index.js +3 -0
- package/packages/live-bridge-remote-script/src/runtime/fixture-runtime.js +356 -0
- package/packages/live-sidecar-m4l/README.md +45 -0
- package/packages/live-sidecar-m4l/device/laive-sidecar.amxd +0 -0
- package/packages/live-sidecar-m4l/project/code/laive-sidecar-node.js +149 -0
- package/packages/live-sidecar-m4l/project/data/laive-sidecar.manifest.json +8 -0
- package/packages/live-sidecar-m4l/project/laive-sidecar.maxproj +36 -0
- package/packages/live-sidecar-m4l/project/patchers/laive-sidecar.maxpat +172 -0
- package/packages/live-sidecar-m4l/src/contracts.js +35 -0
- package/packages/live-sidecar-m4l/src/index.js +19 -0
- package/packages/live-sidecar-m4l/src/install-sidecar-device.js +15 -0
- package/packages/live-sidecar-m4l/src/package-sidecar.js +5 -0
- package/packages/live-sidecar-m4l/src/project.js +132 -0
- package/packages/live-sidecar-m4l/src/runtime.js +96 -0
- package/packages/live-sidecar-m4l/src/workflows.js +95 -0
- package/packages/mcp-server/src/cli.js +113 -0
- package/packages/mcp-server/src/default-tools.js +253 -0
- package/packages/mcp-server/src/errors.js +24 -0
- package/packages/mcp-server/src/index.js +10 -0
- package/packages/mcp-server/src/server.js +96 -0
- package/packages/mcp-server/src/session.js +475 -0
- package/packages/mcp-server/src/tool-registry.js +41 -0
- package/packages/state-engine/src/engine.js +566 -0
- package/packages/state-engine/src/ids.js +57 -0
- package/packages/state-engine/src/index.js +40 -0
- package/packages/state-engine/src/normalize.js +357 -0
- package/packages/state-engine/src/queries.js +154 -0
- package/packages/state-engine/src/replay.js +60 -0
- package/packages/ui-automation/src/executor.js +87 -0
- package/packages/ui-automation/src/guards.js +21 -0
- package/packages/ui-automation/src/helper.js +186 -0
- package/packages/ui-automation/src/index.js +20 -0
- package/packages/ui-automation/src/macos.js +82 -0
- package/packages/ui-automation/src/package-ui-helper.js +5 -0
- package/packages/ui-automation/src/workflows.js +72 -0
- package/scripts/install-remote-script.py +7 -0
- package/scripts/install-ui-helper.mjs +14 -0
- package/scripts/package-remote-script.py +7 -0
- package/scripts/package-ui-helper.mjs +4 -0
- 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
|
+
}
|