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,113 @@
1
+ {
2
+ "live_version": "12.3.0",
3
+ "capabilities": {
4
+ "read_state": true,
5
+ "set_transport": true,
6
+ "create_track": true,
7
+ "create_scene": true,
8
+ "create_clip": true,
9
+ "insert_notes": true,
10
+ "set_parameter": true,
11
+ "subscribe": true
12
+ },
13
+ "song": {
14
+ "id": "song:current",
15
+ "name": "Fixture Set",
16
+ "tempo": 124,
17
+ "time_signature_numerator": 4,
18
+ "time_signature_denominator": 4,
19
+ "is_playing": false,
20
+ "arrangement_position_beats": 0,
21
+ "loop": {
22
+ "enabled": false,
23
+ "start_beats": 0,
24
+ "length_beats": 16
25
+ }
26
+ },
27
+ "scenes": [
28
+ {
29
+ "id": "scene:1",
30
+ "index": 0,
31
+ "name": "Intro"
32
+ },
33
+ {
34
+ "id": "scene:2",
35
+ "index": 1,
36
+ "name": "Drop"
37
+ }
38
+ ],
39
+ "tracks": [
40
+ {
41
+ "id": "track:1",
42
+ "index": 0,
43
+ "name": "Drums",
44
+ "type": "midi",
45
+ "color": 1,
46
+ "arm": false,
47
+ "mute": false,
48
+ "solo": false,
49
+ "devices": [
50
+ {
51
+ "id": "device:track:1:1",
52
+ "name": "Drum Rack",
53
+ "class_name": "InstrumentGroupDevice",
54
+ "parameters": [
55
+ {
56
+ "id": "parameter:device:track:1:1:1",
57
+ "name": "Macro 1",
58
+ "value": 0.5,
59
+ "min": 0,
60
+ "max": 1,
61
+ "display_value": "64"
62
+ }
63
+ ]
64
+ }
65
+ ],
66
+ "session_clips": [
67
+ {
68
+ "id": "clip:session:track:1:slot:1",
69
+ "slot_index": 0,
70
+ "name": "Beat A",
71
+ "length_beats": 4,
72
+ "is_playing": false,
73
+ "notes": [
74
+ {
75
+ "pitch": 36,
76
+ "start_beats": 0,
77
+ "duration_beats": 1,
78
+ "velocity": 100
79
+ }
80
+ ]
81
+ }
82
+ ]
83
+ },
84
+ {
85
+ "id": "track:2",
86
+ "index": 1,
87
+ "name": "Bass",
88
+ "type": "midi",
89
+ "color": 2,
90
+ "arm": false,
91
+ "mute": false,
92
+ "solo": false,
93
+ "devices": [
94
+ {
95
+ "id": "device:track:2:1",
96
+ "name": "Operator",
97
+ "class_name": "Operator",
98
+ "parameters": [
99
+ {
100
+ "id": "parameter:device:track:2:1:1",
101
+ "name": "Volume",
102
+ "value": 0.65,
103
+ "min": 0,
104
+ "max": 1,
105
+ "display_value": "-3.1 dB"
106
+ }
107
+ ]
108
+ }
109
+ ],
110
+ "session_clips": []
111
+ }
112
+ ]
113
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./bridge/client.js";
2
+ export * from "./bridge/server.js";
3
+ export * from "./runtime/fixture-runtime.js";
@@ -0,0 +1,356 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ function clone(value) {
7
+ return JSON.parse(JSON.stringify(value));
8
+ }
9
+
10
+ const defaultFixturePath = fileURLToPath(
11
+ new URL("../fixtures/default-live-set.json", import.meta.url)
12
+ );
13
+
14
+ export class FixtureLiveRuntime extends EventEmitter {
15
+ constructor(fixtureState) {
16
+ super();
17
+ this.state = fixtureState;
18
+ }
19
+
20
+ static async fromFixture(fixturePath = defaultFixturePath) {
21
+ const payload = JSON.parse(await readFile(fixturePath, "utf8"));
22
+ return new FixtureLiveRuntime(payload);
23
+ }
24
+
25
+ get liveVersion() {
26
+ return this.state.live_version;
27
+ }
28
+
29
+ get capabilities() {
30
+ return clone(this.state.capabilities);
31
+ }
32
+
33
+ async execute(message) {
34
+ switch (message.operation) {
35
+ case "hello":
36
+ return {
37
+ bridge: "laive-fixture-runtime",
38
+ protocol_version: "0.1.0",
39
+ live_version: this.liveVersion
40
+ };
41
+ case "capabilities":
42
+ return this.capabilities;
43
+ case "health":
44
+ return {
45
+ status: "ok",
46
+ fixture_loaded: true,
47
+ track_count: this.state.tracks.length
48
+ };
49
+ case "get":
50
+ return this.handleGet(message.target, message.arguments);
51
+ case "set":
52
+ return this.handleSet(message.target, message.arguments, message.dry_run);
53
+ case "call":
54
+ return this.handleCall(message.target, message.arguments, message.dry_run);
55
+ default:
56
+ throw new Error(`Unsupported runtime operation: ${message.operation}`);
57
+ }
58
+ }
59
+
60
+ handleGet(target = "song") {
61
+ switch (target) {
62
+ case "song":
63
+ return clone(this.state.song);
64
+ case "tracks":
65
+ return clone(this.state.tracks);
66
+ case "scenes":
67
+ return clone(this.state.scenes);
68
+ default:
69
+ return this.lookupTarget(target);
70
+ }
71
+ }
72
+
73
+ handleSet(target, args, dryRun) {
74
+ if (target === "song.tempo") {
75
+ const tempo = Number(args.value);
76
+ if (!Number.isFinite(tempo) || tempo <= 0) {
77
+ throw new Error("tempo must be a positive number");
78
+ }
79
+ if (!dryRun) {
80
+ this.state.song.tempo = tempo;
81
+ this.emit("event", {
82
+ topic: "transport.changed",
83
+ payload: {
84
+ tempo
85
+ }
86
+ });
87
+ }
88
+ return {
89
+ target,
90
+ applied: !dryRun,
91
+ value: tempo
92
+ };
93
+ }
94
+
95
+ if (target?.startsWith("parameter:")) {
96
+ return this.setParameter(target, args, dryRun);
97
+ }
98
+
99
+ throw new Error(`Unsupported set target: ${target}`);
100
+ }
101
+
102
+ handleCall(target, args, dryRun) {
103
+ switch (target) {
104
+ case "transport.play":
105
+ if (!dryRun) {
106
+ this.state.song.is_playing = true;
107
+ this.emit("event", {
108
+ topic: "transport.changed",
109
+ payload: { is_playing: true }
110
+ });
111
+ }
112
+ return { target, applied: !dryRun, is_playing: true };
113
+ case "transport.stop":
114
+ if (!dryRun) {
115
+ this.state.song.is_playing = false;
116
+ this.emit("event", {
117
+ topic: "transport.changed",
118
+ payload: { is_playing: false }
119
+ });
120
+ }
121
+ return { target, applied: !dryRun, is_playing: false };
122
+ case "create_track":
123
+ return this.createTrack(args, dryRun);
124
+ case "create_scene":
125
+ return this.createScene(args, dryRun);
126
+ case "create_clip":
127
+ return this.createClip(args, dryRun);
128
+ case "insert_notes":
129
+ return this.insertNotes(args, dryRun);
130
+ case "fire_clip":
131
+ return this.fireClip(args, dryRun);
132
+ default:
133
+ throw new Error(`Unsupported call target: ${target}`);
134
+ }
135
+ }
136
+
137
+ lookupTarget(target) {
138
+ if (!target) {
139
+ throw new Error("target is required");
140
+ }
141
+
142
+ if (target.startsWith("track:")) {
143
+ return clone(this.findTrack(target));
144
+ }
145
+
146
+ if (target.startsWith("device:")) {
147
+ return clone(this.findDevice(target));
148
+ }
149
+
150
+ if (target.startsWith("parameter:")) {
151
+ return clone(this.findParameter(target));
152
+ }
153
+
154
+ if (target.startsWith("clip:")) {
155
+ return clone(this.findClip(target));
156
+ }
157
+
158
+ throw new Error(`Unknown target: ${target}`);
159
+ }
160
+
161
+ findTrack(trackId) {
162
+ const track = this.state.tracks.find((item) => item.id === trackId);
163
+ if (!track) {
164
+ throw new Error(`Track not found: ${trackId}`);
165
+ }
166
+ return track;
167
+ }
168
+
169
+ findDevice(deviceId) {
170
+ for (const track of this.state.tracks) {
171
+ const device = track.devices.find((item) => item.id === deviceId);
172
+ if (device) {
173
+ return device;
174
+ }
175
+ }
176
+ throw new Error(`Device not found: ${deviceId}`);
177
+ }
178
+
179
+ findParameter(parameterId) {
180
+ for (const track of this.state.tracks) {
181
+ for (const device of track.devices) {
182
+ const parameter = device.parameters.find((item) => item.id === parameterId);
183
+ if (parameter) {
184
+ return parameter;
185
+ }
186
+ }
187
+ }
188
+ throw new Error(`Parameter not found: ${parameterId}`);
189
+ }
190
+
191
+ findClip(clipId) {
192
+ for (const track of this.state.tracks) {
193
+ const clip = track.session_clips.find((item) => item.id === clipId);
194
+ if (clip) {
195
+ return clip;
196
+ }
197
+ }
198
+ throw new Error(`Clip not found: ${clipId}`);
199
+ }
200
+
201
+ setParameter(parameterId, args, dryRun) {
202
+ const parameter = this.findParameter(parameterId);
203
+ const value = Number(args.value);
204
+ if (!Number.isFinite(value)) {
205
+ throw new Error("parameter value must be numeric");
206
+ }
207
+ if (!dryRun) {
208
+ parameter.value = Math.min(parameter.max, Math.max(parameter.min, value));
209
+ this.emit("event", {
210
+ topic: "parameters.changed",
211
+ payload: { parameter_id: parameterId, value: parameter.value }
212
+ });
213
+ }
214
+ return {
215
+ target: parameterId,
216
+ applied: !dryRun,
217
+ value: Math.min(parameter.max, Math.max(parameter.min, value))
218
+ };
219
+ }
220
+
221
+ createTrack(args, dryRun) {
222
+ const nextIndex = this.state.tracks.length;
223
+ const nextId = `track:${nextIndex + 1}`;
224
+ const track = {
225
+ id: nextId,
226
+ index: nextIndex,
227
+ name: args.name ?? `Track ${nextIndex + 1}`,
228
+ type: args.type ?? "midi",
229
+ color: args.color ?? 0,
230
+ arm: false,
231
+ mute: false,
232
+ solo: false,
233
+ devices: [],
234
+ session_clips: []
235
+ };
236
+
237
+ if (!dryRun) {
238
+ this.state.tracks.push(track);
239
+ this.emit("event", {
240
+ topic: "tracks.changed",
241
+ payload: { action: "created", track }
242
+ });
243
+ }
244
+
245
+ return {
246
+ applied: !dryRun,
247
+ track
248
+ };
249
+ }
250
+
251
+ createScene(args, dryRun) {
252
+ const nextIndex = this.state.scenes.length;
253
+ const scene = {
254
+ id: `scene:${nextIndex + 1}`,
255
+ index: nextIndex,
256
+ name: args.name ?? `Scene ${nextIndex + 1}`
257
+ };
258
+
259
+ if (!dryRun) {
260
+ this.state.scenes.push(scene);
261
+ this.emit("event", {
262
+ topic: "clips.changed",
263
+ payload: { action: "scene-created", scene }
264
+ });
265
+ }
266
+
267
+ return {
268
+ applied: !dryRun,
269
+ scene
270
+ };
271
+ }
272
+
273
+ createClip(args, dryRun) {
274
+ const track = this.findTrack(args.track_id);
275
+ const nextIndex = track.session_clips.length;
276
+ const clip = {
277
+ id: `clip:session:${track.id}:slot:${nextIndex + 1}`,
278
+ slot_index: nextIndex,
279
+ name: args.name ?? `Clip ${nextIndex + 1}`,
280
+ length_beats: args.length_beats ?? 4,
281
+ is_playing: false,
282
+ notes: []
283
+ };
284
+
285
+ if (!dryRun) {
286
+ track.session_clips.push(clip);
287
+ this.emit("event", {
288
+ topic: "clips.changed",
289
+ payload: {
290
+ action: "clip-created",
291
+ track_id: track.id,
292
+ clip
293
+ }
294
+ });
295
+ }
296
+
297
+ return {
298
+ applied: !dryRun,
299
+ clip
300
+ };
301
+ }
302
+
303
+ insertNotes(args, dryRun) {
304
+ const clip = this.findClip(args.clip_id);
305
+ const notes = Array.isArray(args.notes) ? clone(args.notes) : [];
306
+
307
+ if (!dryRun) {
308
+ clip.notes.push(...notes);
309
+ this.emit("event", {
310
+ topic: "clips.changed",
311
+ payload: {
312
+ action: "notes-inserted",
313
+ clip_id: clip.id,
314
+ notes
315
+ }
316
+ });
317
+ }
318
+
319
+ return {
320
+ applied: !dryRun,
321
+ clip_id: clip.id,
322
+ note_count: clip.notes.length
323
+ };
324
+ }
325
+
326
+ fireClip(args, dryRun) {
327
+ const clip = this.findClip(args.clip_id);
328
+
329
+ if (!dryRun) {
330
+ for (const track of this.state.tracks) {
331
+ for (const candidate of track.session_clips) {
332
+ candidate.is_playing = candidate.id === clip.id;
333
+ }
334
+ }
335
+ this.emit("event", {
336
+ topic: "clips.changed",
337
+ payload: {
338
+ action: "clip-fired",
339
+ clip_id: clip.id
340
+ }
341
+ });
342
+ }
343
+
344
+ return {
345
+ applied: !dryRun,
346
+ clip_id: clip.id
347
+ };
348
+ }
349
+ }
350
+
351
+ export function resolveFixturePath(inputPath) {
352
+ if (!inputPath) {
353
+ return defaultFixturePath;
354
+ }
355
+ return path.resolve(process.cwd(), inputPath);
356
+ }
@@ -0,0 +1,45 @@
1
+ # `@laive/live-sidecar-m4l`
2
+
3
+ This package contains the source assets and shipped `.amxd` for the `laive` Max MIDI Effect sidecar project.
4
+
5
+ What is included:
6
+
7
+ - a testable Node-side runtime core under `src/`
8
+ - a source Max project under `project/`
9
+ - a Node for Max entry script at `project/code/laive-sidecar-node.js`
10
+ - a patcher source file at `project/patchers/laive-sidecar.maxpat`
11
+ - a shipped Max for Live device at `device/laive-sidecar.amxd`
12
+ - a staging helper that copies the sidecar project into a portable folder for delivery
13
+
14
+ This package does **not** generate a finished `.amxd` automatically. The deliverable here is the importable source project and patcher assets that can be opened in Max / Max for Live, inspected, and saved as a device by the user.
15
+
16
+ ## Layout
17
+
18
+ - `src/contracts.js`
19
+ - `src/workflows.js`
20
+ - `src/runtime.js`
21
+ - `src/package-sidecar.js`
22
+ - `project/laive-sidecar.maxproj`
23
+ - `project/patchers/laive-sidecar.maxpat`
24
+ - `project/code/laive-sidecar-node.js`
25
+ - `project/data/laive-sidecar.manifest.json`
26
+ - `device/laive-sidecar.amxd`
27
+
28
+ ## Package The Source Project
29
+
30
+ From this package directory:
31
+
32
+ ```sh
33
+ npm test
34
+ npm run package:project
35
+ ```
36
+
37
+ The staging helper copies the source project into the repository-level `artifacts/live-sidecar-m4l/`.
38
+
39
+ ## Manual Max For Live Steps
40
+
41
+ 1. Preferred end-user path: use the shipped `device/laive-sidecar.amxd`.
42
+ 2. Developer path: open the staged `laive-sidecar/laive-sidecar.maxproj` in Max if you need the source project.
43
+ 3. Open `laive-sidecar/patchers/laive-sidecar.maxpat` if you need the source patcher.
44
+ 4. Confirm the `node.script` object points at `../code/laive-sidecar-node.js`.
45
+ 5. Drop the `.amxd` onto a MIDI track in Live and validate transport, context, and note workflows.
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ function createCapabilityMap(overrides) {
4
+ return Object.assign(
5
+ {
6
+ noteEditing: true,
7
+ objectObservation: true,
8
+ deviceIntrospection: true,
9
+ realtimeAnalysis: false,
10
+ browserInsertion: false
11
+ },
12
+ overrides || {}
13
+ );
14
+ }
15
+
16
+ const workflows = {
17
+ snapshotSelectionContext: {
18
+ description: "Read selected track, clip, and device context from the official Live API.",
19
+ requiredCapabilities: ["objectObservation"],
20
+ queryPaths: ["live_set view selected_track", "live_set view detail_clip"]
21
+ },
22
+ replaceClipNotes: {
23
+ description: "Apply a note payload to a target MIDI clip in a single sidecar transaction.",
24
+ requiredCapabilities: ["noteEditing"],
25
+ target: "clip"
26
+ },
27
+ observeDeviceParameters: {
28
+ description: "Start a parameter observation stream for the selected device.",
29
+ requiredCapabilities: ["objectObservation", "deviceIntrospection"],
30
+ target: "device:selected"
31
+ }
32
+ };
33
+
34
+ function loadMaxApi() {
35
+ try {
36
+ return require("max-api");
37
+ } catch (_error) {
38
+ return {
39
+ post(message) {
40
+ process.stdout.write(String(message) + "\n");
41
+ },
42
+ outlet(message) {
43
+ process.stdout.write(JSON.stringify(message) + "\n");
44
+ },
45
+ addHandlers(handlers) {
46
+ global.__laiveMaxHandlers = handlers;
47
+ }
48
+ };
49
+ }
50
+ }
51
+
52
+ const Max = loadMaxApi();
53
+ const capabilities = createCapabilityMap();
54
+
55
+ function emit(type, payload) {
56
+ Max.outlet({
57
+ type,
58
+ payload
59
+ });
60
+ }
61
+
62
+ function normalizeMessage(rawMessage) {
63
+ if (typeof rawMessage !== "string") {
64
+ return rawMessage;
65
+ }
66
+
67
+ const trimmed = rawMessage.trim();
68
+ if (!trimmed) {
69
+ return {};
70
+ }
71
+
72
+ if (trimmed === "hello" || trimmed === "capabilities" || trimmed === "list_workflows") {
73
+ return {
74
+ command: trimmed
75
+ };
76
+ }
77
+
78
+ const looseCommandMatch = trimmed.match(/^\{\s*command\s*:\s*([A-Za-z0-9_:-]+)\s*\}$/);
79
+ if (looseCommandMatch) {
80
+ return {
81
+ command: looseCommandMatch[1]
82
+ };
83
+ }
84
+
85
+ return rawMessage;
86
+ }
87
+
88
+ async function handleMessage(rawMessage) {
89
+ let message = normalizeMessage(rawMessage);
90
+
91
+ if (typeof message === "string") {
92
+ try {
93
+ message = JSON.parse(message);
94
+ } catch (error) {
95
+ emit("error", {
96
+ code: "invalid_json",
97
+ message: error.message
98
+ });
99
+ return;
100
+ }
101
+ }
102
+
103
+ switch (message.command) {
104
+ case "hello":
105
+ emit("hello", {
106
+ runtime: "laive-sidecar-node",
107
+ version: "0.1.0"
108
+ });
109
+ return;
110
+ case "capabilities":
111
+ emit("capabilities", capabilities);
112
+ return;
113
+ case "list_workflows":
114
+ emit("query", {
115
+ workflows: Object.keys(workflows).map(function (name) {
116
+ return {
117
+ name: name,
118
+ description: workflows[name].description
119
+ };
120
+ })
121
+ });
122
+ return;
123
+ default:
124
+ emit("error", {
125
+ code: "unknown_command",
126
+ message: "Unknown command: " + message.command
127
+ });
128
+ }
129
+ }
130
+
131
+ Max.post("laive-sidecar Node for Max source loaded");
132
+ Max.addHandlers({
133
+ message: handleMessage,
134
+ hello: function () {
135
+ return handleMessage("hello");
136
+ },
137
+ capabilities: function () {
138
+ return handleMessage("capabilities");
139
+ },
140
+ list_workflows: function () {
141
+ return handleMessage("list_workflows");
142
+ },
143
+ bang: function () {
144
+ emit("hello", {
145
+ runtime: "laive-sidecar-node",
146
+ version: "0.1.0"
147
+ });
148
+ }
149
+ });
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "laive-sidecar",
3
+ "project_file": "laive-sidecar.maxproj",
4
+ "patcher": "laive-sidecar.maxpat",
5
+ "node_script": "laive-sidecar-node.js",
6
+ "source_only": true,
7
+ "description": "Source project for the laive Max for Live sidecar."
8
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "laive-sidecar",
3
+ "version": 1,
4
+ "creationdate": 3856982400,
5
+ "modificationdate": 3857056952,
6
+ "viewrect": [65.0, 101.0, 320.0, 220.0],
7
+ "autoorganize": 1,
8
+ "hideprojectwindow": 0,
9
+ "showdependencies": 1,
10
+ "autolocalize": 0,
11
+ "contents": {
12
+ "patchers": {
13
+ "laive-sidecar.maxpat": {
14
+ "kind": "patcher",
15
+ "local": 1,
16
+ "toplevel": 1
17
+ }
18
+ },
19
+ "code": {
20
+ "laive-sidecar-node.js": {
21
+ "kind": "javascript",
22
+ "local": 1
23
+ }
24
+ }
25
+ },
26
+ "layout": {},
27
+ "searchpath": {},
28
+ "detailsvisible": 0,
29
+ "amxdtype": 1835887981,
30
+ "readonly": 0,
31
+ "devpathtype": 0,
32
+ "devpath": ".",
33
+ "sortmode": 0,
34
+ "viewmode": 0,
35
+ "includepackages": 0
36
+ }