@statelyai/sdk 0.3.0 → 0.4.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/README.md CHANGED
@@ -126,6 +126,39 @@ const embed = createStatelyEmbed({
126
126
  embed.mount(container);
127
127
  ```
128
128
 
129
+ ### Comments
130
+
131
+ Comments are optional and integrator-configured. Pass a `comments` object to
132
+ `embed.init()` when you want Liveblocks-backed commenting enabled.
133
+
134
+ ```ts
135
+ embed.init({
136
+ machine: machineConfig,
137
+ mode: 'editing',
138
+ comments: {
139
+ roomId: 'machine:checkout',
140
+ publicApiKey: 'pk_live_...',
141
+ userId: currentUserId ?? null,
142
+ },
143
+ });
144
+ ```
145
+
146
+ You can also use a custom auth endpoint instead of a public key:
147
+
148
+ ```ts
149
+ embed.init({
150
+ machine: machineConfig,
151
+ comments: {
152
+ roomId: 'machine:checkout',
153
+ authEndpoint: '/api/liveblocks-auth',
154
+ userId: currentUserId ?? null,
155
+ },
156
+ });
157
+ ```
158
+
159
+ `roomId` is required when comments are enabled. `userId` is optional and only
160
+ used for comment identity metadata.
161
+
129
162
  ## Module layout
130
163
 
131
164
  The SDK ships direct root exports for the common entry points:
@@ -171,6 +204,7 @@ Creates an embed instance.
171
204
  | `baseUrl` | `string` | **Required.** Base URL of the Stately app |
172
205
  | `apiKey` | `string` | API key for authentication (see [Authentication](#authentication)) |
173
206
  | `origin` | `string` | Custom target origin for postMessage |
207
+ | `assets` | `AssetConfig` | Asset upload configuration (see [Asset uploads](#asset-uploads)) |
174
208
  | `onReady` | `() => void` | Called when embed is ready |
175
209
  | `onLoaded` | `(graph) => void` | Called when machine is loaded |
176
210
  | `onChange` | `(graph, machineConfig) => void` | Called on every change |
@@ -208,9 +242,23 @@ embed.init({
208
242
  rightPanels: ['events'],
209
243
  activePanels: ['code'],
210
244
  },
245
+ comments: {
246
+ roomId: 'machine:checkout',
247
+ publicApiKey: 'pk_live_...',
248
+ },
211
249
  });
212
250
  ```
213
251
 
252
+ `comments` accepts:
253
+
254
+ | Field | Type | Description |
255
+ | ---------- | ---------- | ----------------------------------------------- |
256
+ | `roomId` | `string` | **Required.** Liveblocks room identifier |
257
+ | `publicApiKey` | `string` | Liveblocks public key |
258
+ | `authEndpoint` | `string` | Custom Liveblocks auth endpoint |
259
+ | `baseUrl` | `string` | Custom Liveblocks base URL for self-hosting |
260
+ | `userId` | `string \| null` | Optional user identity metadata |
261
+
214
262
  #### `embed.updateMachine(machine, format?)`
215
263
 
216
264
  Update the displayed machine.
@@ -248,3 +296,99 @@ Show a toast notification in the embed. Type: `'success' | 'error' | 'info' | 'w
248
296
  #### `embed.destroy()`
249
297
 
250
298
  Tear down the embed. Removes listeners, rejects pending promises, and removes the iframe if it was created via `mount()`.
299
+
300
+ ### Asset uploads
301
+
302
+ By default, dropped images are stored as base64 data URLs. To upload assets to your own storage, pass an `assets` config:
303
+
304
+ ```ts
305
+ const embed = createStatelyEmbed({
306
+ baseUrl: 'https://stately.ai',
307
+ assets: {
308
+ onUploadRequest: async (file, { stateNodeId }) => {
309
+ // upload `file` (a File object) to your storage
310
+ // return { url } at minimum
311
+ },
312
+ accept: ['image/*'], // MIME patterns (default: ['image/*'])
313
+ maxFileSize: 5 * 1024 * 1024, // bytes (default: 10 MB)
314
+ },
315
+ });
316
+ ```
317
+
318
+ | Option | Type | Description |
319
+ | --- | --- | --- |
320
+ | `onUploadRequest` | `(file: File, context: { stateNodeId: string }) => Promise<UploadResult>` | **Required.** Called when the editor needs to upload a file. Return the hosted URL. |
321
+ | `accept` | `string[]` | Accepted MIME types. Supports wildcards (`image/*`). Default: `['image/*']` |
322
+ | `maxFileSize` | `number` | Max file size in bytes. Default: `10_485_760` (10 MB) |
323
+
324
+ `UploadResult`:
325
+
326
+ ```ts
327
+ interface UploadResult {
328
+ url: string; // hosted URL (required)
329
+ name?: string; // display name (defaults to filename)
330
+ metadata?: Record<string, unknown>; // arbitrary metadata stored on the asset
331
+ }
332
+ ```
333
+
334
+ #### Example: AWS S3 (presigned URL)
335
+
336
+ ```ts
337
+ import { createStatelyEmbed } from '@statelyai/sdk';
338
+
339
+ const embed = createStatelyEmbed({
340
+ baseUrl: 'https://stately.ai',
341
+ assets: {
342
+ onUploadRequest: async (file) => {
343
+ // 1. Get a presigned upload URL from your backend
344
+ const { uploadUrl, publicUrl } = await fetch('/api/s3/presign', {
345
+ method: 'POST',
346
+ headers: { 'Content-Type': 'application/json' },
347
+ body: JSON.stringify({
348
+ filename: file.name,
349
+ contentType: file.type,
350
+ }),
351
+ }).then((r) => r.json());
352
+
353
+ // 2. PUT the file directly to S3
354
+ await fetch(uploadUrl, {
355
+ method: 'PUT',
356
+ headers: { 'Content-Type': file.type },
357
+ body: file,
358
+ });
359
+
360
+ return { url: publicUrl, name: file.name };
361
+ },
362
+ accept: ['image/*'],
363
+ maxFileSize: 10 * 1024 * 1024,
364
+ },
365
+ });
366
+ ```
367
+
368
+ #### Example: Cloudflare R2 (worker upload)
369
+
370
+ ```ts
371
+ import { createStatelyEmbed } from '@statelyai/sdk';
372
+
373
+ const embed = createStatelyEmbed({
374
+ baseUrl: 'https://stately.ai',
375
+ assets: {
376
+ onUploadRequest: async (file) => {
377
+ const form = new FormData();
378
+ form.append('file', file);
379
+
380
+ // Upload via a Cloudflare Worker that writes to R2
381
+ const { url } = await fetch('https://uploads.example.com/assets', {
382
+ method: 'POST',
383
+ body: form,
384
+ }).then((r) => r.json());
385
+
386
+ return { url, name: file.name };
387
+ },
388
+ accept: ['image/*', 'application/pdf'],
389
+ maxFileSize: 25 * 1024 * 1024,
390
+ },
391
+ });
392
+ ```
393
+
394
+ If `onUploadRequest` throws or rejects, the editor shows an error toast. If no `assets` config is provided, files are stored as inline base64 data URLs (no network requests).
package/dist/cli.d.mts ADDED
@@ -0,0 +1,63 @@
1
+ import "./graph-BfezxFKJ.mjs";
2
+ import { SyncPlan } from "./sync.mjs";
3
+ import { Command } from "@oclif/core";
4
+ import * as _oclif_core_interfaces0 from "@oclif/core/interfaces";
5
+
6
+ //#region src/cli.d.ts
7
+ declare function formatPlanSummary(plan: SyncPlan): string;
8
+ declare abstract class BaseSyncCommand extends Command {
9
+ static enableJsonFlag: boolean;
10
+ static flags: {
11
+ help: _oclif_core_interfaces0.BooleanFlag<void>;
12
+ 'fail-on-changes': _oclif_core_interfaces0.BooleanFlag<boolean>;
13
+ 'api-key': _oclif_core_interfaces0.OptionFlag<string, _oclif_core_interfaces0.CustomOptions>;
14
+ 'base-url': _oclif_core_interfaces0.OptionFlag<string, _oclif_core_interfaces0.CustomOptions>;
15
+ };
16
+ }
17
+ declare abstract class ParsedSyncCommand extends BaseSyncCommand {
18
+ protected parseSync<T extends typeof BaseSyncCommand>(command: T): Promise<_oclif_core_interfaces0.ParserOutput<{
19
+ help: void;
20
+ 'fail-on-changes': boolean;
21
+ 'api-key': string;
22
+ 'base-url': string;
23
+ }, {
24
+ [flag: string]: any;
25
+ }, {
26
+ [arg: string]: any;
27
+ }>>;
28
+ }
29
+ declare class PlanCommand extends ParsedSyncCommand {
30
+ static summary: string;
31
+ static description: string;
32
+ static args: {
33
+ source: _oclif_core_interfaces0.Arg<string, Record<string, unknown>>;
34
+ target: _oclif_core_interfaces0.Arg<string, Record<string, unknown>>;
35
+ };
36
+ run(): Promise<void>;
37
+ }
38
+ declare class DiffCommand extends ParsedSyncCommand {
39
+ static summary: string;
40
+ static description: string;
41
+ static args: {
42
+ source: _oclif_core_interfaces0.Arg<string, Record<string, unknown>>;
43
+ target: _oclif_core_interfaces0.Arg<string, Record<string, unknown>>;
44
+ };
45
+ run(): Promise<void>;
46
+ }
47
+ declare class PullCommand extends ParsedSyncCommand {
48
+ static summary: string;
49
+ static description: string;
50
+ static args: {
51
+ source: _oclif_core_interfaces0.Arg<string, Record<string, unknown>>;
52
+ target: _oclif_core_interfaces0.Arg<string, Record<string, unknown>>;
53
+ };
54
+ run(): Promise<void>;
55
+ }
56
+ declare const COMMANDS: {
57
+ plan: typeof PlanCommand;
58
+ diff: typeof DiffCommand;
59
+ pull: typeof PullCommand;
60
+ };
61
+ declare function run(argv?: string[]): Promise<void>;
62
+ //#endregion
63
+ export { COMMANDS, formatPlanSummary, run };
package/dist/cli.mjs ADDED
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ import { planSync, pullSync } from "./sync.mjs";
3
+ import path from "node:path";
4
+ import { Args, Command, Flags, flush, handle, run as run$1 } from "@oclif/core";
5
+
6
+ //#region src/cli.ts
7
+ function loadLocalEnv() {
8
+ if (typeof process.loadEnvFile !== "function") return;
9
+ const cwdEnvPath = path.join(process.cwd(), ".env.local");
10
+ try {
11
+ process.loadEnvFile(cwdEnvPath);
12
+ } catch {}
13
+ }
14
+ loadLocalEnv();
15
+ const sharedFlags = {
16
+ help: Flags.help({ char: "h" }),
17
+ "fail-on-changes": Flags.boolean({
18
+ default: false,
19
+ description: "Exit with a nonzero code when differences are found"
20
+ }),
21
+ "api-key": Flags.string({
22
+ description: "Stately API key used for remote source or target resolution",
23
+ default: process.env.STATELY_API_KEY ?? process.env.NEXT_PUBLIC_STATELY_API_KEY
24
+ }),
25
+ "base-url": Flags.string({
26
+ description: "Base URL for Stately Studio or a self-hosted deployment",
27
+ default: process.env.STATELY_API_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? process.env.NEXT_PUBLIC_STATELY_API_URL
28
+ })
29
+ };
30
+ function formatChangeList(label, items) {
31
+ if (items.length === 0) return null;
32
+ return `${label}: ${items.map((item) => item.id).join(", ")}`;
33
+ }
34
+ function formatPlanSummary(plan) {
35
+ const lines = [
36
+ `Plan: ${plan.source.locator} -> ${plan.target.locator}`,
37
+ `Source: ${plan.source.kind} (${plan.source.format})`,
38
+ `Target: ${plan.target.kind} (${plan.target.format})`,
39
+ `Has changes: ${plan.summary.hasChanges ? "yes" : "no"}`,
40
+ `Node changes: ${plan.summary.nodeChanges}`,
41
+ `Edge changes: ${plan.summary.edgeChanges}`
42
+ ];
43
+ const nodeSections = [
44
+ formatChangeList("Added nodes", plan.diff.nodes.added),
45
+ formatChangeList("Removed nodes", plan.diff.nodes.removed),
46
+ formatChangeList("Updated nodes", plan.diff.nodes.updated)
47
+ ].filter(Boolean);
48
+ const edgeSections = [
49
+ formatChangeList("Added edges", plan.diff.edges.added),
50
+ formatChangeList("Removed edges", plan.diff.edges.removed),
51
+ formatChangeList("Updated edges", plan.diff.edges.updated)
52
+ ].filter(Boolean);
53
+ lines.push(...nodeSections, ...edgeSections);
54
+ if (plan.warnings.length > 0) lines.push(`Warnings: ${plan.warnings.join("; ")}`);
55
+ return lines.join("\n");
56
+ }
57
+ var BaseSyncCommand = class extends Command {
58
+ static enableJsonFlag = false;
59
+ static flags = sharedFlags;
60
+ };
61
+ const sharedArgs = {
62
+ source: Args.string({
63
+ required: true,
64
+ description: "Source locator. Supports local file paths, URLs, or machine IDs."
65
+ }),
66
+ target: Args.string({
67
+ required: true,
68
+ description: "Target locator. Supports local file paths, URLs, or machine IDs."
69
+ })
70
+ };
71
+ var ParsedSyncCommand = class extends BaseSyncCommand {
72
+ async parseSync(command) {
73
+ return this.parse(command);
74
+ }
75
+ };
76
+ var PlanCommand = class PlanCommand extends ParsedSyncCommand {
77
+ static summary = "Plan semantic sync changes between a source and target.";
78
+ static description = "Resolves the source and target, normalizes both into graph form, and prints a semantic change summary.";
79
+ static args = sharedArgs;
80
+ async run() {
81
+ const { args, flags } = await this.parseSync(PlanCommand);
82
+ const plan = await planSync({
83
+ source: args.source,
84
+ target: args.target,
85
+ apiKey: flags["api-key"],
86
+ baseUrl: flags["base-url"]
87
+ });
88
+ this.log(formatPlanSummary(plan));
89
+ this.exit(flags["fail-on-changes"] && plan.summary.hasChanges ? 1 : 0);
90
+ }
91
+ };
92
+ var DiffCommand = class DiffCommand extends ParsedSyncCommand {
93
+ static summary = "Diff a source and target using semantic graph comparison.";
94
+ static description = "Like plan, but intended for diff-style usage and exit codes in scripts.";
95
+ static args = sharedArgs;
96
+ async run() {
97
+ const { args, flags } = await this.parseSync(DiffCommand);
98
+ const plan = await planSync({
99
+ source: args.source,
100
+ target: args.target,
101
+ apiKey: flags["api-key"],
102
+ baseUrl: flags["base-url"]
103
+ });
104
+ this.log(formatPlanSummary(plan));
105
+ this.exit(flags["fail-on-changes"] && plan.summary.hasChanges ? 1 : 0);
106
+ }
107
+ };
108
+ var PullCommand = class PullCommand extends ParsedSyncCommand {
109
+ static summary = "Pull a source locator into a local target file.";
110
+ static description = "Resolves the source, materializes it in the target format inferred from the target file, and writes the result locally.";
111
+ static args = sharedArgs;
112
+ async run() {
113
+ const { args, flags } = await this.parseSync(PullCommand);
114
+ const result = await pullSync({
115
+ source: args.source,
116
+ target: args.target,
117
+ apiKey: flags["api-key"],
118
+ baseUrl: flags["base-url"]
119
+ });
120
+ this.log(`Pulled: ${result.source.locator} -> ${result.outputPath}\nTarget: ${result.target.kind} (${result.target.format})`);
121
+ }
122
+ };
123
+ const COMMANDS = {
124
+ plan: PlanCommand,
125
+ diff: DiffCommand,
126
+ pull: PullCommand
127
+ };
128
+ async function run(argv = process.argv.slice(2)) {
129
+ const normalizedArgv = argv.length === 1 && argv[0] === "-h" ? ["--help"] : argv;
130
+ try {
131
+ await run$1(normalizedArgv, import.meta.url);
132
+ await flush();
133
+ } catch (error) {
134
+ await handle(error);
135
+ }
136
+ }
137
+ if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) run();
138
+
139
+ //#endregion
140
+ export { COMMANDS, formatPlanSummary, run };
package/dist/embed.d.mts CHANGED
@@ -1,10 +1,32 @@
1
- import { a as ExportCallOptions, c as InitOptions, i as EmbedMode, n as EmbedEventMap, o as ExportFormat, r as EmbedEventName, s as ExportFormatMap, t as EmbedEventHandler } from "./protocol-BC-_s3if.mjs";
1
+ import { a as EmbedMode, c as ExportFormatMap, d as UploadResult, i as EmbedEventName, l as InitOptions, n as EmbedEventHandler, o as ExportCallOptions, r as EmbedEventMap, s as ExportFormat, t as CommentsConfig } from "./protocol-BgXSkIuc.mjs";
2
2
 
3
3
  //#region src/embed.d.ts
4
+ interface AssetConfig {
5
+ /**
6
+ * Called when the embed needs to upload a file.
7
+ * Receives a real File object (reconstructed from serialized data).
8
+ * Throwing or rejecting will show an error toast in the embed.
9
+ */
10
+ onUploadRequest: (file: File, context: {
11
+ stateNodeId: string;
12
+ }) => Promise<UploadResult>;
13
+ /**
14
+ * Accepted MIME types. Supports wildcards (e.g. 'image/*').
15
+ * @default ['image/*']
16
+ */
17
+ accept?: string[];
18
+ /**
19
+ * Maximum file size in bytes.
20
+ * @default 10_485_760 (10MB)
21
+ */
22
+ maxFileSize?: number;
23
+ }
4
24
  interface StatelyEmbedOptions {
5
25
  baseUrl: string;
6
26
  apiKey?: string;
7
27
  origin?: string;
28
+ /** Asset upload configuration. If omitted, files are stored as base64 data URLs. */
29
+ assets?: AssetConfig;
8
30
  onReady?: () => void;
9
31
  onLoaded?: (graph: unknown) => void;
10
32
  onChange?: (graph: unknown, machineConfig: unknown) => void;
@@ -40,4 +62,4 @@ interface StatelyEmbed {
40
62
  }
41
63
  declare function createStatelyEmbed(options: StatelyEmbedOptions): StatelyEmbed;
42
64
  //#endregion
43
- export { type EmbedEventHandler, type EmbedEventMap, type EmbedEventName, type EmbedMode, type ExportCallOptions, type ExportFormat, type ExportFormatMap, type InitOptions, StatelyEmbed, StatelyEmbedOptions, createStatelyEmbed };
65
+ export { AssetConfig, type CommentsConfig, type EmbedEventHandler, type EmbedEventMap, type EmbedEventName, type EmbedMode, type ExportCallOptions, type ExportFormat, type ExportFormatMap, type InitOptions, StatelyEmbed, StatelyEmbedOptions, type UploadResult, createStatelyEmbed };
package/dist/embed.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as createPendingExportManager, o as toInitMessage, r as createEventRegistry, t as createPostMessageTransport } from "./transport-D352iKKa.mjs";
1
+ import { i as createPendingExportManager, o as toInitMessage, r as createEventRegistry, t as createPostMessageTransport } from "./transport-C1fRAuv-.mjs";
2
2
 
3
3
  //#region src/embed.ts
4
4
  function createStatelyEmbed(options) {
@@ -32,6 +32,12 @@ function createStatelyEmbed(options) {
32
32
  case "@statelyai.ready": {
33
33
  const ready = data;
34
34
  flush();
35
+ if (options.assets?.onUploadRequest) send({
36
+ type: "@statelyai.uploadCapabilities",
37
+ enabled: true,
38
+ accept: options.assets.accept,
39
+ maxFileSize: options.assets.maxFileSize
40
+ });
35
41
  options.onReady?.();
36
42
  events.emit("ready", { version: ready.version });
37
43
  break;
@@ -76,6 +82,47 @@ function createStatelyEmbed(options) {
76
82
  events.emit("error", err);
77
83
  break;
78
84
  }
85
+ case "@statelyai.uploadRequest": {
86
+ const req = data;
87
+ if (!options.assets?.onUploadRequest) {
88
+ send({
89
+ type: "@statelyai.error",
90
+ requestId: req.requestId,
91
+ code: "upload_not_configured",
92
+ message: "No upload handler configured"
93
+ });
94
+ break;
95
+ }
96
+ const maxSize = options.assets.maxFileSize ?? 10485760;
97
+ if (req.file.size > maxSize) {
98
+ send({
99
+ type: "@statelyai.error",
100
+ requestId: req.requestId,
101
+ code: "upload_file_too_large",
102
+ message: `File exceeds maximum size of ${maxSize} bytes`
103
+ });
104
+ break;
105
+ }
106
+ const binary = atob(req.file.data);
107
+ const bytes = new Uint8Array(binary.length);
108
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
109
+ const file = new File([bytes], req.file.name, { type: req.file.mimeType });
110
+ options.assets.onUploadRequest(file, { stateNodeId: req.stateNodeId }).then((result) => {
111
+ send({
112
+ type: "@statelyai.uploadResponse",
113
+ requestId: req.requestId,
114
+ result
115
+ });
116
+ }).catch((err) => {
117
+ send({
118
+ type: "@statelyai.error",
119
+ requestId: req.requestId,
120
+ code: "upload_failed",
121
+ message: err instanceof Error ? err.message : "Upload failed"
122
+ });
123
+ });
124
+ break;
125
+ }
79
126
  }
80
127
  }
81
128
  function replaceTransport(nextTransport) {
@@ -388,6 +388,6 @@ type StudioAction = DigraphAction;
388
388
  type StudioEventTypeData = EventTypeData;
389
389
  declare function toStudioMachine(graph: StatelyGraph): StudioMachine;
390
390
  declare function fromStudioMachine(studioMachine: StudioMachine): StatelyGraph;
391
- declare const studioMachineConverter: _statelyai_graph0.GraphFormatConverter<DigraphConfig>;
391
+ declare const studioMachineConverter: _statelyai_graph0.GraphFormatConverter<DigraphConfig, StatelyNodeData, StatelyEdgeData, StatelyGraphData>;
392
392
  //#endregion
393
393
  export { EventTypeData as C, DigraphNodeConfig as S, studioMachineConverter as _, StatelyGraphData as a, DigraphConfig as b, StatelyInvoke as c, StudioAction as d, StudioEdge as f, fromStudioMachine as g, StudioNode as h, StatelyGraph as i, StatelyNodeData as l, StudioMachine as m, StatelyActorImplementation as n, StatelyGuard as o, StudioEventTypeData as p, StatelyEdgeData as r, StatelyImplementation as s, StatelyAction as t, StatelyTagImplementation as u, toStudioMachine as v, StateNodeJSONData as w, DigraphEdgeConfig as x, DigraphAction as y };
package/dist/graph.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as studioMachineConverter, a as StatelyGraphData, c as StatelyInvoke, d as StudioAction, f as StudioEdge, g as fromStudioMachine, h as StudioNode, i as StatelyGraph, l as StatelyNodeData, m as StudioMachine, n as StatelyActorImplementation, o as StatelyGuard, p as StudioEventTypeData, r as StatelyEdgeData, s as StatelyImplementation, t as StatelyAction, u as StatelyTagImplementation, v as toStudioMachine } from "./graph-C-7ZK_nK.mjs";
1
+ import { _ as studioMachineConverter, a as StatelyGraphData, c as StatelyInvoke, d as StudioAction, f as StudioEdge, g as fromStudioMachine, h as StudioNode, i as StatelyGraph, l as StatelyNodeData, m as StudioMachine, n as StatelyActorImplementation, o as StatelyGuard, p as StudioEventTypeData, r as StatelyEdgeData, s as StatelyImplementation, t as StatelyAction, u as StatelyTagImplementation, v as toStudioMachine } from "./graph-BfezxFKJ.mjs";
2
2
  export { StatelyAction, StatelyActorImplementation, StatelyEdgeData, StatelyGraph, StatelyGraphData, StatelyGuard, StatelyImplementation, StatelyInvoke, StatelyNodeData, StatelyTagImplementation, StudioAction, StudioEdge, StudioEventTypeData, StudioMachine, StudioNode, fromStudioMachine, studioMachineConverter, toStudioMachine };