arcane-os 0.63.0 → 0.65.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 (58) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/core/client.mjs +69 -7
  4. package/docs/guides/native-whisper-openvino.md +334 -0
  5. package/docs/reference/README.md +6 -0
  6. package/docs/reference/arcane-ollama.md +1 -1
  7. package/docs/reference/codex-app-server.md +269 -0
  8. package/docs/reference/core-client.md +71 -4
  9. package/docs/reference/core-development.md +157 -0
  10. package/docs/reference/core-native-packaging.md +48 -6
  11. package/docs/reference/core-repositories.md +258 -0
  12. package/docs/reference/inventory/package-api.json +199 -3
  13. package/docs/reference/inventory/runtime-modules.json +3 -2
  14. package/docs/reference/local-ai.md +77 -4
  15. package/docs/reference/local-image-flux.md +1 -1
  16. package/docs/reference/local-image-generation.md +11 -0
  17. package/docs/reference/local-image-sdxl.md +139 -0
  18. package/docs/reference/local-whisper.md +219 -0
  19. package/docs/reference/mcp-stdio.md +263 -0
  20. package/docs/reference/native-speech.md +51 -2
  21. package/docs/reference/runtime-components.md +18 -1
  22. package/docs/reference/runtime-modules.md +26 -4
  23. package/docs/reference/sdk-api.md +412 -2
  24. package/package.json +9 -2
  25. package/runtime/arcane/components/speech.html +15 -3
  26. package/runtime/arcane/components/voice-transcription.html +19 -3
  27. package/runtime/arcane/modules/AI.js +406 -40
  28. package/runtime/arcane/modules/AIProviderRuntime.js +20 -3
  29. package/runtime/arcane/modules/AIRuntimeState.js +15 -2
  30. package/runtime/arcane/modules/ComponentContracts.js +3 -0
  31. package/runtime/arcane/modules/Ollama.js +1 -1
  32. package/runtime/arcane/modules/OllamaModelIdentifier.js +13 -0
  33. package/schemas/arcane-app.schema.json +60 -2
  34. package/schemas/arcane-package.schema.json +60 -2
  35. package/src/codex/app-server.mjs +845 -0
  36. package/src/core/development.mjs +457 -0
  37. package/src/core/repositories.mjs +375 -0
  38. package/src/core/services/local-ai.mjs +80 -10
  39. package/src/core/services/speech.mjs +1 -0
  40. package/src/dev-server.mjs +3 -2
  41. package/src/index.mjs +1 -0
  42. package/src/local-ai/config.mjs +7 -3
  43. package/src/local-ai/dev.mjs +85 -401
  44. package/src/local-ai/image-library.mjs +9 -0
  45. package/src/local-ai/image-models/sdxl.mjs +18 -0
  46. package/src/local-ai/install.mjs +26 -7
  47. package/src/local-ai/whisper/build.mjs +48 -0
  48. package/src/local-ai/whisper/config.mjs +89 -0
  49. package/src/local-ai/whisper/index.mjs +572 -0
  50. package/src/local-ai/whisper/install.mjs +332 -0
  51. package/src/local-ai/whisper/native/CMakeLists.txt +98 -0
  52. package/src/local-ai/whisper/native/NOTICE +18 -0
  53. package/src/local-ai/whisper/native/README.md +104 -0
  54. package/src/local-ai/whisper/native/main.cpp +520 -0
  55. package/src/local-ai/whisper/openvino-build.mjs +138 -0
  56. package/src/mcp/stdio-server.mjs +579 -0
  57. package/src/native/portable-layout.mjs +39 -7
  58. package/src/toolchain.mjs +19 -8
@@ -0,0 +1,375 @@
1
+ import {mkdir, readdir, writeFile} from 'node:fs/promises';
2
+ import {homedir} from 'node:os';
3
+ import path from 'node:path';
4
+ import Is from 'strong-type';
5
+ import {ArcaneError, throwIfAborted} from '../errors.mjs';
6
+ import {runProcess} from '../process.mjs';
7
+ import {repositoryPull, repositoryPush, repositoryStatus} from '../repository.mjs';
8
+
9
+ const is = new Is(false);
10
+ const directoryOperations = new Map();
11
+
12
+ /** Resolve persistent native data without creating or migrating anything. */
13
+ export function resolveArcaneDataPaths({dataRoot} = {}) {
14
+ let root = dataRoot;
15
+ if (root === undefined) {
16
+ const home = homedir();
17
+ switch (process.platform) {
18
+ case 'win32':
19
+ root = path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'ArcaneData');
20
+ break;
21
+ case 'darwin':
22
+ root = path.join(home, 'Library', 'Application Support', 'ArcaneData');
23
+ break;
24
+ case 'linux': {
25
+ const xdg = process.env.XDG_DATA_HOME;
26
+ const base = xdg && path.isAbsolute(xdg) ? xdg : path.join(home, '.local', 'share');
27
+ root = path.join(base, 'ArcaneData');
28
+ break;
29
+ }
30
+ default:
31
+ throw new ArcaneError(
32
+ 'ARCANE_DATA_ROOT_REQUIRED',
33
+ 'This native host must supply its persistent ArcaneData directory through dataRoot.'
34
+ );
35
+ }
36
+ }
37
+ requireString(root, 'dataRoot');
38
+ const resolved = path.resolve(root);
39
+ return {dataRoot: resolved, repositoriesRoot: path.join(resolved, 'Repos')};
40
+ }
41
+
42
+ /** One connected checkout, composed into an application's own Core service. */
43
+ export function createRepositoryWorkspace(
44
+ {name, directory, dataRoot, remote, branch, onEvent, run = runProcess} = {}
45
+ ) {
46
+ let selected = directory;
47
+ if (selected === undefined) {
48
+ requireString(name, 'name');
49
+ if (name === '.' || name === '..' || /[/\\]/u.test(name)) {
50
+ throw new TypeError('name must be one directory name; use directory for an explicit repository path.');
51
+ }
52
+ selected = path.join(resolveArcaneDataPaths({dataRoot}).repositoriesRoot, name);
53
+ }
54
+ requireString(selected, 'directory');
55
+ if (remote !== undefined) requireString(remote, 'remote');
56
+ if (branch !== undefined) requireString(branch, 'branch');
57
+ if (!is.function(run)) throw new TypeError('run must implement the SDK process adapter.');
58
+ const repositoryDirectory = path.resolve(selected);
59
+ const pending = new Set();
60
+ let opened = false;
61
+ let closing = null;
62
+
63
+ async function prepare(signal, execute = run) {
64
+ throwIfAborted(signal);
65
+ if (opened) return {directory: repositoryDirectory, cloned: false};
66
+ let entries;
67
+ try {
68
+ entries = await readdir(repositoryDirectory);
69
+ } catch (error) {
70
+ if (error.code !== 'ENOENT') throw error;
71
+ entries = [];
72
+ }
73
+ throwIfAborted(signal);
74
+ if (entries.length) {
75
+ // A parent checkout or a bare object cache is not this connection's
76
+ // working root. Inspection never changes an existing destination.
77
+ const result = await execute(
78
+ 'git',
79
+ ['rev-parse', '--is-inside-work-tree', '--show-prefix'],
80
+ {cwd: repositoryDirectory, signal, onEvent}
81
+ );
82
+ throwIfAborted(signal);
83
+ const [inside, prefix] = result.stdout.split(/\r?\n/u);
84
+ if (inside !== 'true' || prefix !== '') {
85
+ throw new ArcaneError(
86
+ 'ARCANE_REPOSITORY_DIRECTORY_INVALID',
87
+ 'The selected nonempty directory is not a Git working repository root.',
88
+ {details: result}
89
+ );
90
+ }
91
+ opened = true;
92
+ return {directory: repositoryDirectory, cloned: false};
93
+ }
94
+ requireString(remote, 'remote');
95
+ await mkdir(path.dirname(repositoryDirectory), {recursive: true});
96
+ throwIfAborted(signal);
97
+ const arguments_ = ['clone', '--progress'];
98
+ if (branch !== undefined) arguments_.push('--branch', branch);
99
+ arguments_.push('--', remote, repositoryDirectory);
100
+ const result = await execute(
101
+ 'git',
102
+ arguments_,
103
+ {cwd: path.dirname(repositoryDirectory), signal, onEvent}
104
+ );
105
+ throwIfAborted(signal);
106
+ opened = true;
107
+ return {directory: repositoryDirectory, cloned: true, stdout: result.stdout, stderr: result.stderr};
108
+ }
109
+
110
+ function accept(operation, signal) {
111
+ try {
112
+ throwIfAborted(signal);
113
+ if (closing) throw new ArcaneError('CORE_CLOSING', 'The repository workspace is closing.');
114
+ } catch (error) {
115
+ return Promise.reject(error);
116
+ }
117
+ const result = orderDirectoryOperation(
118
+ repositoryDirectory,
119
+ async function runRepositoryOperation() {
120
+ throwIfAborted(signal);
121
+ return operation();
122
+ }
123
+ );
124
+ pending.add(result);
125
+ function settled() {
126
+ pending.delete(result);
127
+ }
128
+ result.then(settled, settled);
129
+ return result;
130
+ }
131
+
132
+ function open({signal} = {}) {
133
+ return accept(
134
+ function openRepository() {
135
+ return prepare(signal);
136
+ },
137
+ signal
138
+ );
139
+ }
140
+
141
+ function status({signal} = {}) {
142
+ return accept(
143
+ async function readRepositoryStatus() {
144
+ await prepare(signal);
145
+ return repositoryStatus({workspaceRoot: repositoryDirectory, signal, onEvent, run});
146
+ },
147
+ signal
148
+ );
149
+ }
150
+
151
+ function pull({signal} = {}) {
152
+ return accept(
153
+ async function pullRepository() {
154
+ await prepare(signal);
155
+ return repositoryPull({workspaceRoot: repositoryDirectory, signal, onEvent, run});
156
+ },
157
+ signal
158
+ );
159
+ }
160
+
161
+ function push({signal} = {}) {
162
+ return accept(
163
+ async function pushRepository() {
164
+ await prepare(signal);
165
+ return repositoryPush({workspaceRoot: repositoryDirectory, signal, onEvent, run});
166
+ },
167
+ signal
168
+ );
169
+ }
170
+
171
+ function write({files, message, signal} = {}) {
172
+ let selectedFiles;
173
+ try {
174
+ requireString(message, 'message');
175
+ if (!message.isWellFormed()) throw new TypeError('message cannot be represented completely as UTF-8 text.');
176
+ if (!is.array(files) || files.length === 0) {
177
+ throw new TypeError('write requires at least one explicitly selected file.');
178
+ }
179
+ selectedFiles = Array.from(
180
+ files,
181
+ function snapshotFile(file) {
182
+ const {path: filename, content} = file;
183
+ requireString(filename, 'file.path');
184
+ if (!filename.isWellFormed() || filename.includes('\0')) {
185
+ throw new TypeError('file.path cannot be represented completely as a native filename.');
186
+ }
187
+ if (!is.string(content) || !content.isWellFormed()) {
188
+ throw new TypeError('file.content must be complete UTF-8-representable text.');
189
+ }
190
+ const destination = path.resolve(repositoryDirectory, filename);
191
+ const relative = path.relative(repositoryDirectory, destination);
192
+ const first = relative.split(path.sep)[0];
193
+ if (path.isAbsolute(filename) || !relative || relative === '..'
194
+ || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)
195
+ || first.toLowerCase() === '.git') {
196
+ throw new TypeError('file.path must name a working file relative to this repository, outside Git metadata.');
197
+ }
198
+ return {path: filename, content, destination};
199
+ }
200
+ );
201
+ } catch (error) {
202
+ return Promise.reject(error);
203
+ }
204
+
205
+ return accept(
206
+ async function writeRepositoryFiles() {
207
+ const outcome = {
208
+ directory: repositoryDirectory,
209
+ state: 'local',
210
+ stage: 'prepare',
211
+ paths: selectedFiles.map(function selectedPath(file) { return file.path; }),
212
+ writtenPaths: [],
213
+ written: false,
214
+ staged: false,
215
+ committed: false,
216
+ pushed: false,
217
+ outputs: []
218
+ };
219
+ let pendingWrite = false;
220
+
221
+ async function executeGit(stage, args, input, options = {}) {
222
+ throwIfAborted(signal);
223
+ outcome.stage = stage;
224
+ const output = {stage, stdout: '', stderr: ''};
225
+ const observed = {stdout: false, stderr: false};
226
+ outcome.outputs.push(output);
227
+ let beforeStartFailed = false;
228
+
229
+ function consumeOutput({stream, chunk}) {
230
+ observed[stream] = true;
231
+ output[stream] += chunk;
232
+ }
233
+
234
+ async function observeEvent(event) {
235
+ try {
236
+ await onEvent?.(event);
237
+ } catch (error) {
238
+ if (event.type === 'process.starting') beforeStartFailed = true;
239
+ throw error;
240
+ }
241
+ }
242
+
243
+ function recordCompletion(result) {
244
+ if (!result || !is.integer(result.code)) return;
245
+ output.code = result.code;
246
+ output.signal = result.signal ?? null;
247
+ for (const stream of ['stdout', 'stderr']) {
248
+ if (!observed[stream] && is.string(result[stream])) output[stream] = result[stream];
249
+ }
250
+ if (result.code !== 0) return;
251
+ if (stage === 'stage') outcome.staged = true;
252
+ if (stage === 'commit') {
253
+ outcome.committed = true;
254
+ outcome.state = 'committed';
255
+ }
256
+ if (stage === 'push') {
257
+ outcome.pushed = true;
258
+ outcome.state = 'pushed';
259
+ }
260
+ }
261
+
262
+ try {
263
+ const result = await run(
264
+ 'git',
265
+ args,
266
+ {cwd: options.cwd ?? repositoryDirectory, signal, onEvent: observeEvent,
267
+ onOutput: consumeOutput, captureOutput: false, input}
268
+ );
269
+ recordCompletion(result);
270
+ if (result.code !== 0) {
271
+ throw new ArcaneError(
272
+ 'ARCANE_OPERATION_FAILED',
273
+ `Git ${stage} exited with code ${String(result.code)}.`,
274
+ {details: result}
275
+ );
276
+ }
277
+ return {...result, stdout: output.stdout, stderr: output.stderr};
278
+ } catch (error) {
279
+ if (!beforeStartFailed) recordCompletion(error?.details);
280
+ const notStarted = beforeStartFailed
281
+ || (error?.code === 'ARCANE_PREREQUISITE_MISSING' && !error?.details)
282
+ || (error?.code === 'ARCANE_CANCELLED' && !error?.details);
283
+ if (!notStarted && output.code !== 0 && (stage === 'commit' || stage === 'push')) {
284
+ outcome.state = 'uncertain';
285
+ }
286
+ throw error;
287
+ }
288
+ }
289
+
290
+ async function* commitMessage() {
291
+ yield message;
292
+ }
293
+
294
+ try {
295
+ await prepare(
296
+ signal,
297
+ function prepareGit(command, args, options) {
298
+ return executeGit('prepare', args, undefined, options);
299
+ }
300
+ );
301
+ outcome.stage = 'write';
302
+ for (const file of selectedFiles) {
303
+ throwIfAborted(signal);
304
+ outcome.path = file.path;
305
+ await mkdir(path.dirname(file.destination), {recursive: true});
306
+ throwIfAborted(signal);
307
+ pendingWrite = true;
308
+ await writeFile(file.destination, file.content, {encoding: 'utf8', signal});
309
+ pendingWrite = false;
310
+ outcome.writtenPaths.push(file.path);
311
+ }
312
+ outcome.written = true;
313
+ delete outcome.path;
314
+ await executeGit('stage', ['--literal-pathspecs', 'add', '--', ...outcome.paths]);
315
+ await executeGit(
316
+ 'commit',
317
+ ['--literal-pathspecs', 'commit', '--only', '--cleanup=verbatim', '--file', '-', '--', ...outcome.paths],
318
+ commitMessage()
319
+ );
320
+ await executeGit('push', ['push']);
321
+ outcome.stage = 'complete';
322
+ return outcome;
323
+ } catch (cause) {
324
+ if (pendingWrite) outcome.state = 'uncertain';
325
+ throw new ArcaneError(
326
+ cause?.code ?? 'ARCANE_REPOSITORY_WRITE_FAILED',
327
+ cause instanceof Error ? cause.message : String(cause),
328
+ {cause, details: outcome, exitCode: cause?.exitCode}
329
+ );
330
+ }
331
+ },
332
+ signal
333
+ );
334
+ }
335
+
336
+ function close() {
337
+ if (!closing) {
338
+ closing = Promise.allSettled([...pending]).then(
339
+ function operationsDrained(results) {
340
+ const failures = results.filter(
341
+ function failed(result) {
342
+ return result.status === 'rejected';
343
+ }
344
+ ).map(
345
+ function reason(result) {
346
+ return result.reason;
347
+ }
348
+ );
349
+ if (failures.length) throw new AggregateError(failures, 'Repository operations failed while draining.');
350
+ }
351
+ );
352
+ }
353
+ return closing;
354
+ }
355
+
356
+ return {directory: repositoryDirectory, open, status, pull, push, write, close, drain: close, dispose: close};
357
+ }
358
+
359
+ function requireString(value, name) {
360
+ if (!is.string(value) || value === '') throw new TypeError(`${name} must be a nonempty string.`);
361
+ }
362
+
363
+ function orderDirectoryOperation(directory, operation) {
364
+ const key = process.platform === 'win32' ? directory.toLowerCase() : directory;
365
+ const previous = directoryOperations.get(key) ?? Promise.resolve();
366
+ const result = previous.then(operation, operation);
367
+ function releaseQueue() {
368
+ if (directoryOperations.get(key) === tail) directoryOperations.delete(key);
369
+ }
370
+ // Only the scheduling tail settles successfully after an error. Each caller
371
+ // retains its complete failure; a later operation can retry the same path.
372
+ const tail = result.then(releaseQueue, releaseQueue);
373
+ directoryOperations.set(key, tail);
374
+ return result;
375
+ }
@@ -208,17 +208,54 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
208
208
  publish();
209
209
  }
210
210
 
211
+ function beginOllamaObservation(engine) {
212
+ return {revision: ++engine.ollamaRevision, signal: engine.signal};
213
+ }
214
+
215
+ function ownsOllamaObservation(engine, observation) {
216
+ return observation.signal === engine.signal && !observation.signal.aborted
217
+ && engine.server.current.available && observation.revision >= engine.ollamaAppliedRevision;
218
+ }
219
+
220
+ function recordOllamaFailure(engine, error, observation) {
221
+ if (!ownsOllamaObservation(engine, observation)) return;
222
+ engine.ollamaAppliedRevision = observation.revision;
223
+ recordFailure(engine, error);
224
+ }
225
+
226
+ function recordOllamaModels(engine, catalog, running, observation) {
227
+ if (!ownsOllamaObservation(engine, observation)) return;
228
+ engine.ollamaAppliedRevision = observation.revision;
229
+ engine.state = {...engine.state, ...engine.server.current, error: null};
230
+ const resident = new Map((running.models ?? []).map(function residentModel(model) {
231
+ return [model.model ?? model.name, model];
232
+ }));
233
+ const models = new Map((catalog.models ?? []).map(function catalogModel(model) {
234
+ const id = model.model ?? model.name ?? model.id;
235
+ return [id, {...model, id, loaded: resident.has(id)}];
236
+ }));
237
+ for (const [id, model] of resident) {
238
+ if (!models.has(id)) models.set(id, {...model, id, loaded: true});
239
+ }
240
+ engine.models = [...models.values()];
241
+ }
242
+
211
243
  async function refreshModels(engine, signal) {
212
244
  const revision = engine.revision;
213
245
  if (engine.id === 'ollama') {
214
- const [catalog, running] = await Promise.all([
215
- requestLocalJSON({url: engine.server.url, path: '/api/tags', signal}),
216
- requestLocalJSON({url: engine.server.url, path: '/api/ps', signal})
217
- ]);
218
- const loaded = new Set((running.models ?? []).map(function loadedId(model) { return model.model ?? model.name; }));
219
- engine.models = (catalog.models ?? []).map(function ollamaModel(model) {
220
- return {...model, id: model.model ?? model.name, loaded: loaded.has(model.model ?? model.name)};
221
- });
246
+ const observation = beginOllamaObservation(engine);
247
+ const observationSignal = signal ? AbortSignal.any([signal, observation.signal]) : observation.signal;
248
+ try {
249
+ const [catalog, running] = await Promise.all([
250
+ requestLocalJSON({url: engine.server.url, path: '/api/tags', signal: observationSignal}),
251
+ requestLocalJSON({url: engine.server.url, path: '/api/ps', signal: observationSignal})
252
+ ]);
253
+ observationSignal.throwIfAborted();
254
+ recordOllamaModels(engine, catalog, running, observation);
255
+ } catch (error) {
256
+ if (!observationSignal.aborted) recordOllamaFailure(engine, error, observation);
257
+ throw error;
258
+ }
222
259
  } else {
223
260
  const properties = await requestLocalJSON({url: engine.server.url, path: '/props', signal});
224
261
  engine.router = properties.role === 'router';
@@ -427,6 +464,34 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
427
464
 
428
465
  async function ollamaRequest(operation, parameters = {}, request, method = 'POST') {
429
466
  const engine = await engineReady('ollama', request.signal);
467
+ const engineSignal = engine.signal;
468
+ const observation = operation === 'ps' ? beginOllamaObservation(engine) : null;
469
+ try {
470
+ const result = await ollamaResponse(engine, operation, parameters, request, method);
471
+ if (operation === 'ps') {
472
+ recordOllamaModels(engine, {models: engine.models}, result, observation);
473
+ publish();
474
+ }
475
+ return result;
476
+ } catch (error) {
477
+ if (operation === 'ps' && !request.signal.aborted) recordOllamaFailure(engine, error, observation);
478
+ throw error;
479
+ } finally {
480
+ // Every owned operation that can change residency/catalog leaves
481
+ // a real upstream observation, including cancellation or failure.
482
+ if (['chat', 'generate', 'embed', 'pull', 'create', 'copy', 'delete'].includes(operation)
483
+ && engine.signal === engineSignal && !engineSignal.aborted) {
484
+ try {
485
+ await refreshModels(engine, engineSignal);
486
+ } catch {
487
+ // The refresh publishes only its current owned failure;
488
+ // retain the operation's original result or error.
489
+ }
490
+ }
491
+ }
492
+ }
493
+
494
+ async function ollamaResponse(engine, operation, parameters, request, method) {
430
495
  const signal = AbortSignal.any([request.signal, engine.signal]);
431
496
  const {streamId, ...payload} = parameters;
432
497
  if (payload.stream !== true) {
@@ -472,6 +537,8 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
472
537
  function startEngine(engine) {
473
538
  engine.controller = new AbortController();
474
539
  engine.signal = AbortSignal.any([lifetimeSignal, engine.controller.signal]);
540
+ engine.ollamaRevision = 0;
541
+ engine.ollamaAppliedRevision = 0;
475
542
  const signal = engine.signal;
476
543
  engine.server = createLocalAIServer({
477
544
  id: engine.id, appRoot, runtime: runtimes.find(function runtimeRecord(record) { return record.id === engine.id; }),
@@ -487,7 +554,10 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
487
554
  }
488
555
  return engine;
489
556
  });
490
- engine.ready.catch(function startupFailed(error) { if (!signal.aborted) recordFailure(engine, error); });
557
+ engine.ready.catch(function startupFailed(error) {
558
+ // Ollama refreshes and the server already publish their own failures.
559
+ if (!signal.aborted && engine.id !== 'ollama') recordFailure(engine, error);
560
+ });
491
561
  }
492
562
 
493
563
  const service = {
@@ -557,7 +627,7 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
557
627
  await refreshModels(engine, request.signal);
558
628
  } catch (error) {
559
629
  request.signal.throwIfAborted();
560
- recordFailure(engine, error);
630
+ if (engine.id !== 'ollama') recordFailure(engine, error);
561
631
  }
562
632
  }));
563
633
  return snapshot();
@@ -196,6 +196,7 @@ export function createSpeechService({stt, tts, signal} = {}) {
196
196
  parameters,
197
197
  {
198
198
  signal: operationSignal,
199
+ requestId: request.requestId,
199
200
  onProgress(progress) {
200
201
  if (!operationSignal.aborted) {
201
202
  request.emit?.(
@@ -855,6 +855,7 @@ async function startOwnedDevServer({
855
855
  certPath,
856
856
  keyPath,
857
857
  localAI,
858
+ core = localAI,
858
859
  signal,
859
860
  sdkRuntimeSourceRoot
860
861
  }={},events,releaseSignal){
@@ -1261,7 +1262,7 @@ async function startOwnedDevServer({
1261
1262
  response.end();
1262
1263
  return;
1263
1264
  }
1264
- if (localAI && await localAI.handler(request, response)) return;
1265
+ if (core && await core.handler(request, response)) return;
1265
1266
  if(request.method!=='GET'&&request.method!=='HEAD'){
1266
1267
  deny(response,405,'Method not allowed.');
1267
1268
  return;
@@ -1558,7 +1559,7 @@ async function startOwnedDevServer({
1558
1559
  }
1559
1560
  if(!closeInitiated){
1560
1561
  closeInitiated=true;
1561
- localAIClosing=Promise.resolve().then(function closeLocalAI(){return localAI?.close(error);});
1562
+ localAIClosing=Promise.resolve().then(function closeCore(){return core?.close(error);});
1562
1563
  localAIClosing.catch(function observeLocalAICloseFailure(closeError){operationalError??=closeError;});
1563
1564
  try{
1564
1565
  closeDevelopmentListeners(fileServer, tlsServer).then(
package/src/index.mjs CHANGED
@@ -146,6 +146,7 @@ export {
146
146
  } from './workspace.mjs';
147
147
  export {materializeInstalledSdkRuntime} from './installed-sdk-runtime.mjs';
148
148
  export {startDevServer} from './dev-server.mjs';
149
+ export {createDevelopmentCore} from './core/development.mjs';
149
150
  export {startSourceExampleServer} from './source-server.mjs';
150
151
  export {acceptWebSocket} from './websocket.mjs';
151
152
  export {WS} from '../browser-runtime/websocket-client.mjs';
@@ -4,9 +4,11 @@ import Is from 'strong-type';
4
4
  import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
5
5
  import {SD14_MODEL} from './image-models/sd14.mjs';
6
6
  import {FLUX2_KLEIN_4B_MODEL} from './image-models/flux.mjs';
7
+ import {SDXL_BASE_1_0_MODEL} from './image-models/sdxl.mjs';
8
+ import {normalizeWhisperRuntimeRequirement} from './whisper/config.mjs';
7
9
 
8
10
  const is = new Is(false);
9
- export const LOCAL_AI_RUNTIME_IDS = ['llama.cpp', 'ollama', 'onnx', 'stable-diffusion.cpp'];
11
+ export const LOCAL_AI_RUNTIME_IDS = ['llama.cpp', 'ollama', 'onnx', 'stable-diffusion.cpp', 'whisper.cpp'];
10
12
 
11
13
  /** Image requirements describe assets without acquiring or loading a model. */
12
14
  export function normalizeImageRuntimeRequirement(record) {
@@ -20,9 +22,10 @@ export function normalizeImageRuntimeRequirement(record) {
20
22
  const models = (record.models ?? []).map(
21
23
  function selectedImageModel(value) {
22
24
  const model = value === 'sd14' ? SD14_MODEL
23
- : value === 'flux2-klein-4b' ? FLUX2_KLEIN_4B_MODEL : value;
25
+ : value === 'flux2-klein-4b' ? FLUX2_KLEIN_4B_MODEL
26
+ : value === 'sdxl-base-1.0' ? SDXL_BASE_1_0_MODEL : value;
24
27
  if (!model || !is.object(model) || is.array(model) || !is.string(model.id) || !model.id) {
25
- throw new ArcaneError(ERROR_CODES.usage, 'An image model needs a descriptor with an id, or a built-in model ID (sd14 or flux2-klein-4b).');
28
+ throw new ArcaneError(ERROR_CODES.usage, 'An image model needs a descriptor with an id, or a built-in model ID (sd14, flux2-klein-4b, or sdxl-base-1.0).');
26
29
  }
27
30
  if (!model.resources || !is.object(model.resources) || is.array(model.resources)) {
28
31
  throw new ArcaneError(ERROR_CODES.usage, `Image model ${model.id} needs resources keyed by model role.`);
@@ -59,6 +62,7 @@ export function normalizeLocalAIConfig(value) {
59
62
  throw new ArcaneError(ERROR_CODES.usage, `Unknown local AI runtime: ${String(record?.id ?? item)}.`);
60
63
  }
61
64
  if (record.id === 'stable-diffusion.cpp') return normalizeImageRuntimeRequirement(record);
65
+ if (record.id === 'whisper.cpp') return normalizeWhisperRuntimeRequirement(record);
62
66
  return {...record};
63
67
  });
64
68
  return {...value, runtimes};