arcane-os 0.43.0 → 0.44.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.44.0
4
+
5
+ - Add stateless `generateImages` through `arcane-os/ai/twin-cloud` with
6
+ caller-selected FAL Flux Schnell or Stable Diffusion 3.5 Large. Preserve
7
+ complete prompts and provider parameters, and return every image as a Blob
8
+ in provider order with declared media metadata.
9
+ - Expose semantic progress and complete diagnostic callbacks, asynchronous
10
+ job polling, and cancellation across credentials, callbacks, requests,
11
+ response bodies and media downloads. Submit a paid generation request once;
12
+ never automatically replay an ambiguous submission.
13
+ - Keep image model selection, credentials, display and storage with the
14
+ application. No global model default, saved-history migration, durable media
15
+ storage or change to existing chat and speech behavior is introduced.
16
+
3
17
  ## 0.43.0
4
18
 
5
19
  - Add reusable rendered printing through `createPrintView`, preserving the
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
20
20
  and browser run contracts while retaining their selected application layout.
21
21
 
22
- This checkout defines the `0.43.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.44.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -105,6 +105,240 @@ export function normalizeAIRequestAbort(error){
105
105
  return normalized;
106
106
  }
107
107
 
108
+ /** Stateless image generation; model selection and provider parameters belong to the caller. */
109
+ export async function generateImages({
110
+ model,
111
+ prompt,
112
+ parameters = {},
113
+ twinKey,
114
+ getApiKey,
115
+ signal = null,
116
+ id = Date.now(),
117
+ onRequest = function observeImageRequest(){},
118
+ onResponse = function observeImageResponse(){},
119
+ onProgress = function observeImageProgress(){}
120
+ } = {}) {
121
+ if (signal?.aborted) throw normalizeAIRequestAbort(signal.reason);
122
+ const asynchronous = model === 'fal-ai/flux/schnell';
123
+ if (!asynchronous && model !== 'stable-diffusion-3.5-large') {
124
+ const error = new TypeError('TWiN generateImages requires a supported explicit image model.');
125
+ error.code = 'ARCANE_AI_IMAGE_MODEL_UNSUPPORTED';
126
+ throw error;
127
+ }
128
+ if (!is.string(prompt)) throw new TypeError('TWiN generateImages requires a prompt string.');
129
+ if (!parameters || !is.object(parameters) || is.array(parameters)) {
130
+ throw new TypeError('Image parameters must be an object of additional provider fields.');
131
+ }
132
+ if (Object.hasOwn(parameters, 'prompt') || (!asynchronous && Object.hasOwn(parameters, 'model'))) {
133
+ throw new TypeError('Supply the image prompt and model through their named arguments.');
134
+ }
135
+ const destination = asynchronous
136
+ ? 'https://inference.do-ai.run/v1/async-invoke'
137
+ : 'https://inference.do-ai.run/v1/images/generations';
138
+ const request = asynchronous
139
+ ? {model_id: model, input: {...parameters, prompt}}
140
+ : {...parameters, model, prompt};
141
+ // Observe the complete request without allowing a later callback to rewrite its payload.
142
+ const body = JSON.stringify(request);
143
+ const outputFormat = parameters.output_format;
144
+ const controller = new AbortController();
145
+ const operationSignal = controller.signal;
146
+ function cancelImageRequest() {controller.abort(signal.reason);}
147
+ signal?.addEventListener('abort', cancelImageRequest, {once: true});
148
+ if (signal?.aborted) cancelImageRequest();
149
+
150
+ function progress(stage, requestId) {
151
+ return settleImageOperation(
152
+ function publishImageProgress() {
153
+ return onProgress({stage, model, id, ...(requestId === undefined ? {} : {requestId})});
154
+ },
155
+ operationSignal
156
+ );
157
+ }
158
+
159
+ async function inference(url, options) {
160
+ const response = await settleImageOperation(
161
+ function requestImageInference() {return fetch(url, {...options, credentials: 'omit', signal: operationSignal});},
162
+ operationSignal
163
+ );
164
+ const result = await settleImageOperation(
165
+ function readImageInference() {
166
+ if (response.ok || response.headers.get('content-type')?.includes('application/json')) {
167
+ return response.json();
168
+ }
169
+ return response.text();
170
+ },
171
+ operationSignal
172
+ );
173
+ if (!response.ok) throw result;
174
+ const retryAfter = response.headers.get('retry-after');
175
+ const seconds = retryAfter?.trim() ? Number(retryAfter) : NaN;
176
+ const date = retryAfter?.trim() ? Date.parse(retryAfter) : NaN;
177
+ const delay = is.finite(seconds) && seconds >= 0 ? seconds * 1000
178
+ : is.finite(date) ? Math.max(0, date - Date.now()) : 1000;
179
+ return {result, delay};
180
+ }
181
+
182
+ try {
183
+ await progress('credentials');
184
+ const key = twinKey === undefined && is.function(getApiKey)
185
+ ? await settleImageOperation(getApiKey, operationSignal) : twinKey;
186
+ if (!is.string(key) || !key) {
187
+ const error = new Error('AI provider is not configured.');
188
+ error.code = 'AI_PROVIDER_NOT_CONFIGURED';
189
+ throw error;
190
+ }
191
+ const headers = {'Content-Type': 'application/json', Authorization: `Bearer ${key}`};
192
+ await settleImageOperation(
193
+ function observeImageSubmission() {
194
+ return onRequest(request, id, {operation: 'images', transport: 'http', destination});
195
+ },
196
+ operationSignal
197
+ );
198
+ await progress('requesting');
199
+ // A lost POST response can represent an accepted paid job. Never replay it.
200
+ let response = await inference(destination, {method: 'POST', headers, body});
201
+ let result = response.result;
202
+ if (asynchronous) {
203
+ const requestId = result?.request_id;
204
+ while (result?.status === 'QUEUED' || result?.status === 'IN_PROGRESS') {
205
+ if (!is.string(requestId) || !requestId) {
206
+ throw imageResultError('Image generation returned a pending job without a request_id.', result);
207
+ }
208
+ await progress(result.status === 'QUEUED' ? 'queued' : 'generating', requestId);
209
+ await waitForImageStatus(response.delay, operationSignal);
210
+ response = await inference(
211
+ `${destination}/${encodeURIComponent(requestId)}/status`,
212
+ {method: 'GET', headers}
213
+ );
214
+ result = response.result;
215
+ }
216
+ if (result?.status === 'FAILED') {
217
+ throw imageResultError('TWiN Cloud image generation failed.', result);
218
+ }
219
+ if (result?.status !== 'COMPLETED' && result?.status !== 'COMPLETE') {
220
+ throw imageResultError('TWiN Cloud returned an unsupported image job status.', result);
221
+ }
222
+ if (!is.array(result.output?.images)) {
223
+ if (!is.string(requestId) || !requestId) {
224
+ throw imageResultError('Image generation returned no result or request_id.', result);
225
+ }
226
+ response = await inference(`${destination}/${encodeURIComponent(requestId)}`, {method: 'GET', headers});
227
+ result = response.result;
228
+ if (result?.status !== 'COMPLETED' && result?.status !== 'COMPLETE') {
229
+ throw imageResultError('TWiN Cloud image generation did not return a completed result.', result);
230
+ }
231
+ }
232
+ }
233
+ const returnedImages = asynchronous ? result?.output?.images : result?.data;
234
+ if (!is.array(returnedImages)) {
235
+ throw imageResultError('TWiN Cloud returned no image collection.', result);
236
+ }
237
+ const images = returnedImages.map(function retainImageDescriptor(image) {return {...image};});
238
+ const format = result.output_format ?? outputFormat;
239
+ const requestId = asynchronous ? result.request_id : undefined;
240
+ await settleImageOperation(
241
+ function observeImageResult() {return onResponse(result, id, false);},
242
+ operationSignal
243
+ );
244
+ await progress('downloading', requestId);
245
+ const completed = await Promise.all(images.map(async function materializeImage(image) {
246
+ let blob;
247
+ const mediaType = image.content_type
248
+ ?? ({png: 'image/png', jpeg: 'image/jpeg', jpg: 'image/jpeg', webp: 'image/webp'}[format])
249
+ ?? 'application/octet-stream';
250
+ if (is.string(image.b64_json)) {
251
+ blob = await settleImageOperation(function decodeImageBase64() {
252
+ return new Blob([Uint8Array.from(atob(image.b64_json), function imageCode(character) {
253
+ return character.charCodeAt(0);
254
+ })], {type: mediaType});
255
+ }, operationSignal);
256
+ } else if (is.string(image.url) && image.url) {
257
+ // Media URLs have their own delivery authority. Never forward the inference key.
258
+ const download = await settleImageOperation(function downloadGeneratedImage() {
259
+ return fetch(image.url, {credentials: 'omit', signal: operationSignal});
260
+ }, operationSignal);
261
+ if (!download.ok) {
262
+ throw await settleImageOperation(function readImageDownloadError() {
263
+ return download.headers.get('content-type')?.includes('application/json')
264
+ ? download.json() : download.text();
265
+ }, operationSignal);
266
+ }
267
+ blob = await settleImageOperation(function readGeneratedImage() {return download.blob();}, operationSignal);
268
+ if (!blob.type) blob = new Blob([blob], {type: mediaType});
269
+ } else {
270
+ throw imageResultError('TWiN Cloud returned an image without data or a URL.', image);
271
+ }
272
+ return {
273
+ blob,
274
+ mediaType: blob.type || mediaType,
275
+ ...(image.width === undefined ? {} : {width: image.width}),
276
+ ...(image.height === undefined ? {} : {height: image.height})
277
+ };
278
+ }));
279
+ await progress('complete', requestId);
280
+ if (operationSignal.aborted) throw normalizeAIRequestAbort(operationSignal.reason);
281
+ return {images: completed};
282
+ } catch (error) {
283
+ if (isAIRequestAbort(error, operationSignal)) throw normalizeAIRequestAbort(error);
284
+ throw error;
285
+ } finally {
286
+ signal?.removeEventListener('abort', cancelImageRequest);
287
+ controller.abort('Image operation settled.');
288
+ }
289
+ }
290
+
291
+ function imageResultError(message, result) {
292
+ const error = new Error(message, {cause: result});
293
+ error.code = 'ARCANE_AI_INVALID_PROVIDER_RESULT';
294
+ return error;
295
+ }
296
+
297
+ /** Abort a wait even when a credential, callback, or host Fetch ignores the signal. */
298
+ function settleImageOperation(operation, signal) {
299
+ return new Promise(function settleCloudImageOperation(resolve, reject) {
300
+ let settled = false;
301
+ function finish(error, value, failed) {
302
+ if (settled) return;
303
+ settled = true;
304
+ signal.removeEventListener('abort', cancelled);
305
+ if (failed) reject(error);
306
+ else resolve(value);
307
+ }
308
+ function cancelled() {finish(normalizeAIRequestAbort(signal.reason), undefined, true);}
309
+ signal.addEventListener('abort', cancelled, {once: true});
310
+ if (signal.aborted) {
311
+ cancelled();
312
+ return;
313
+ }
314
+ try {
315
+ Promise.resolve(operation()).then(
316
+ function accepted(value) {finish(undefined, value, false);},
317
+ function rejected(error) {finish(error, undefined, true);}
318
+ );
319
+ } catch (error) {
320
+ finish(error, undefined, true);
321
+ }
322
+ });
323
+ }
324
+
325
+ async function waitForImageStatus(delay, signal) {
326
+ let remaining = delay;
327
+ do {
328
+ // Split only at the host timer range; retain the provider's entire delay.
329
+ const milliseconds = Math.min(remaining, 2147483647);
330
+ let timer;
331
+ try {
332
+ await settleImageOperation(function waitForImagePoll() {
333
+ return new Promise(function scheduleImagePoll(finish) {timer = setTimeout(finish, milliseconds);});
334
+ }, signal);
335
+ } finally {
336
+ clearTimeout(timer);
337
+ }
338
+ remaining -= milliseconds;
339
+ } while (remaining > 0);
340
+ }
341
+
108
342
  export function structuredOutputFormat(value = false){
109
343
  if(value === false || value === null || value === undefined){
110
344
  return null;
@@ -136,6 +136,130 @@ options; they do not add a key, model, browser state, or retained conversation.
136
136
  Retry warnings and observer failures use the shared console logger and preserve
137
137
  the complete original error.
138
138
 
139
+ ## Generate images with an application-selected model
140
+
141
+ `generateImages` is a separate stateless operation on the same public import.
142
+ The application owns the selected model, labels, prompt, provider parameters,
143
+ credential source, display, and any saved image. It does not instantiate the
144
+ browser AI singleton or change chat, speech, model preferences, or history.
145
+
146
+ ```javascript
147
+ import {generateImages} from 'arcane-os/ai/twin-cloud';
148
+
149
+ const controller = new AbortController();
150
+ const {images} = await generateImages({
151
+ model: 'stable-diffusion-3.5-large',
152
+ prompt: 'A moon-powered toaster launches croissants over purple craters.',
153
+ parameters: {n: 2, size: '1024x1024', output_format: 'png'},
154
+ getApiKey: applicationRuntime.getTwinKey,
155
+ signal: controller.signal,
156
+ onProgress({stage}) {
157
+ console.log(stage);
158
+ }
159
+ });
160
+
161
+ for (const image of images) {
162
+ await applicationRuntime.saveGeneratedImage(image.blob, image.mediaType);
163
+ }
164
+ ```
165
+
166
+ `applicationRuntime.getTwinKey` and `saveGeneratedImage` are caller-owned
167
+ functions in this example, not SDK APIs. Pass the credential function with any
168
+ required receiver already bound. Use `twinKey` instead when the application
169
+ already has its key. `getApiKey()` may return a key or a promise and is called
170
+ only when `twinKey` is `undefined`. An explicitly supplied empty key fails with
171
+ `AI_PROVIDER_NOT_CONFIGURED`; it does not select another credential source.
172
+ The example's model and parameters are application choices, not SDK defaults.
173
+
174
+ ```javascript
175
+ generateImages({
176
+ model,
177
+ prompt,
178
+ parameters,
179
+ twinKey,
180
+ getApiKey,
181
+ signal,
182
+ id,
183
+ onRequest,
184
+ onResponse,
185
+ onProgress
186
+ })
187
+ // Promise<{images: [{blob, mediaType, width?, height?}]}>
188
+ ```
189
+
190
+ `parameters` defaults to `{}`, `signal` to `null`, and `id` to `Date.now()`.
191
+ All diagnostic and progress callbacks are optional.
192
+
193
+ The SDK owns the route for these exact supported identifiers:
194
+
195
+ | Explicit model | Submission and result |
196
+ | --- | --- |
197
+ | `fal-ai/flux/schnell` | POST `/v1/async-invoke` with `{model_id:model,input:{...parameters,prompt}}`; pending jobs use GET `/v1/async-invoke/{request_id}/status`, then GET `/v1/async-invoke/{request_id}` when the completed status lacks `output.images`. |
198
+ | `stable-diffusion-3.5-large` | POST `/v1/images/generations` with `{...parameters,model,prompt}`; images come from the completed response's `data` array. |
199
+
200
+ Both routes use `https://inference.do-ai.run`. Missing or unsupported model
201
+ identifiers reject with `ARCANE_AI_IMAGE_MODEL_UNSUPPORTED`; there is no SDK
202
+ image model default. The prompt must be a string and is sent unchanged,
203
+ including whitespace. `parameters` is an object of additional provider fields;
204
+ the SDK neither supplies nor changes image counts, dimensions, quality, seeds,
205
+ or output limits. Supply the prompt only through `prompt`, and the synchronous
206
+ model only through `model`; duplicate reserved fields in `parameters` throw
207
+ `TypeError` instead of silently replacing one input. Provider-supported
208
+ parameter names and values remain that selected model's contract.
209
+
210
+ Every returned image is materialized in provider order. Base64 image data
211
+ becomes a `Blob`; returned URLs are fetched concurrently without an inference
212
+ Authorization header or browser credentials. The output's `mediaType` uses the
213
+ downloaded Blob's declared type, provider `content_type`, or a declared
214
+ `output_format` of `png`, `jpeg`/`jpg`, or `webp`. With no declared type or known
215
+ format, it is `application/octet-stream`; the SDK does not guess dimensions or
216
+ image metadata. `width` and `height` are included only when the provider supplies
217
+ them. The SDK saves nothing and creates no object URLs; the caller owns any
218
+ storage and the lifetime of display URLs it creates.
219
+
220
+ `onRequest(request,id,metadata)` runs once before submission with the complete
221
+ provider request. Metadata is `{operation:'images',transport:'http',destination}`.
222
+ `onResponse(response,id,false)` runs once with the complete final provider JSON
223
+ after successful generation and before image materialization. Intermediate
224
+ poll replies do not call it. These are explicit diagnostic callbacks: keep raw
225
+ protocol outside ordinary user status and durable chat history. Credentials are
226
+ transport-only and are absent from these payloads. The request and image
227
+ descriptors are captured before their diagnostic callbacks so observer edits do
228
+ not rewrite the operation's content.
229
+
230
+ `onProgress({stage,model,id,requestId?})` reports semantic stages: `credentials`,
231
+ `requesting`, `queued`, `generating`, `downloading`, and `complete`. Pending
232
+ stages can repeat; immediately completed jobs skip them. Async job stages
233
+ include `requestId` when present. Progress contains no image data or protocol
234
+ body. All three callbacks may return a promise; their settlement is awaited
235
+ and their failure rejects the operation. `onResponse` means generation returned
236
+ a final result, not that media downloads have finished. The resolved promise
237
+ delivers all materialized images; a failed download rejects instead of returning
238
+ an incomplete collection.
239
+
240
+ The image operation submits once and performs no automatic retry, including
241
+ after a network loss, HTTP `429`, body-read failure, or callback failure. A lost
242
+ POST response may correspond to an accepted paid job. This image-specific rule
243
+ does not alter chat or speech retry behavior. While a FAL job is `QUEUED` or
244
+ `IN_PROGRESS`, polling uses the provider's `Retry-After` delay when supplied,
245
+ otherwise one second. Completed jobs accept `COMPLETED` and the guide's
246
+ `COMPLETE` spelling. Failed or unusable job results reject with
247
+ `ARCANE_AI_INVALID_PROVIDER_RESULT` and the complete result in `error.cause`.
248
+ HTTP failures throw their complete parsed JSON or text body; transport,
249
+ decoding, and callback failures remain their original errors.
250
+
251
+ Calling `controller.abort()` cancels local credential/callback waits, requests,
252
+ body reads, polling timers, and downloads. It rejects with
253
+ `ARCANE_AI_REQUEST_ABORTED` and ignores later settlement from an operation that
254
+ does not honor the signal. A callback already running remains caller-owned;
255
+ the SDK invokes no subsequent callbacks after cancellation. Local cancellation
256
+ does not claim to cancel a remote paid job. Dispose the controller with the
257
+ owning page or operation; no global startup wait is required.
258
+
259
+ The route and response contracts follow DigitalOcean's official
260
+ [FAL inference guide](https://docs.digitalocean.com/products/inference/how-to/use-fal-models/)
261
+ and [multimodal inference guide](https://docs.digitalocean.com/products/inference/how-to/use-multimodal-inference/).
262
+
139
263
  ## Existing browser AI interface
140
264
 
141
265
  The following browser example uses the same managed imports as the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -86,7 +86,7 @@
86
86
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
87
87
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
88
88
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
89
- "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
89
+ "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
90
90
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
91
91
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
92
92
  "check": "node tools/check-source.mjs && npm test",