arcane-os 0.64.0 → 0.66.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 (50) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-decisions.mjs +3 -2
  4. package/browser-runtime/ai/decision-runtime.mjs +14 -24
  5. package/browser-runtime/core/client.mjs +62 -5
  6. package/docs/guides/native-whisper-openvino.md +334 -0
  7. package/docs/publishing.md +39 -0
  8. package/docs/reference/README.md +11 -1
  9. package/docs/reference/ai/browser-decisions.md +7 -4
  10. package/docs/reference/arcane-ollama.md +1 -1
  11. package/docs/reference/codex-app-server.md +269 -0
  12. package/docs/reference/core-client.md +60 -2
  13. package/docs/reference/core-development.md +157 -0
  14. package/docs/reference/core-native-packaging.md +13 -0
  15. package/docs/reference/core-repositories.md +232 -4
  16. package/docs/reference/git-text-snapshot.md +15 -1
  17. package/docs/reference/inventory/package-api.json +174 -6
  18. package/docs/reference/inventory/runtime-modules.json +8 -6
  19. package/docs/reference/local-ai.md +80 -3
  20. package/docs/reference/local-image-generation.md +5 -0
  21. package/docs/reference/local-image-sdxl.md +1 -1
  22. package/docs/reference/local-whisper.md +7 -1
  23. package/docs/reference/mcp-stdio.md +263 -0
  24. package/docs/reference/native-speech.md +6 -0
  25. package/docs/reference/runtime-components.md +18 -1
  26. package/docs/reference/runtime-modules.md +10 -2
  27. package/docs/reference/sdk-api.md +346 -4
  28. package/package.json +6 -2
  29. package/runtime/arcane/modules/AI.js +156 -13
  30. package/runtime/arcane/modules/Ollama.js +1 -1
  31. package/runtime/arcane/modules/OllamaModelIdentifier.js +13 -0
  32. package/schemas/arcane-app.schema.json +8 -1
  33. package/schemas/arcane-package.schema.json +8 -1
  34. package/src/codex/app-server.mjs +845 -0
  35. package/src/core/development.mjs +457 -0
  36. package/src/core/repositories.mjs +178 -9
  37. package/src/core/services/local-ai.mjs +80 -10
  38. package/src/dev-server.mjs +3 -2
  39. package/src/git-identity.mjs +132 -0
  40. package/src/git-text-snapshot.mjs +5 -3
  41. package/src/index.mjs +1 -0
  42. package/src/local-ai/dev.mjs +80 -426
  43. package/src/local-ai/whisper/config.mjs +31 -3
  44. package/src/local-ai/whisper/index.mjs +62 -5
  45. package/src/local-ai/whisper/install.mjs +72 -14
  46. package/src/local-ai/whisper/native/main.cpp +33 -2
  47. package/src/local-ai/whisper/openvino-build.mjs +138 -0
  48. package/src/mcp/stdio-server.mjs +579 -0
  49. package/src/native/portable-layout.mjs +3 -1
  50. package/src/toolchain.mjs +19 -8
@@ -0,0 +1,579 @@
1
+ import {inspect} from 'node:util';
2
+ import Is from 'strong-type';
3
+
4
+ const is = new Is(false);
5
+
6
+ export const MCP_PROTOCOL_VERSION = '2025-11-25';
7
+
8
+ /** An explicit JSON-RPC failure; ordinary tool failures use CallToolResult. */
9
+ export class McpProtocolError extends Error {
10
+ constructor(code, message, data) {
11
+ super(message);
12
+ if (!is.integer(code) || !is.string(message)) {
13
+ throw new TypeError('MCP protocol errors require an integer code and string message.');
14
+ }
15
+ this.name = 'McpProtocolError';
16
+ this.code = code;
17
+ if (data !== undefined) this.data = data;
18
+ }
19
+ }
20
+
21
+ /** One initialize-era MCP session over caller-owned Node streams. */
22
+ export function createMcpStdioServer({serverInfo, instructions, tools = [], resources = [], onDiagnostic} = {}) {
23
+ if (!record(serverInfo) || !is.string(serverInfo.name) || !is.string(serverInfo.version)) {
24
+ throw new TypeError('MCP serverInfo requires name and version strings.');
25
+ }
26
+ if (instructions !== undefined && !is.string(instructions)) {
27
+ throw new TypeError('MCP instructions must be a string.');
28
+ }
29
+ if (onDiagnostic !== undefined && !is.function(onDiagnostic)) {
30
+ throw new TypeError('MCP onDiagnostic must be a function.');
31
+ }
32
+ const toolRegistry = createRegistry(tools, 'name');
33
+ const resourceRegistry = createRegistry(resources, 'uri');
34
+ for (const {descriptor} of toolRegistry.values()) {
35
+ if (!record(descriptor.inputSchema)) {
36
+ throw new TypeError(`MCP tool ${descriptor.name} requires an inputSchema object.`);
37
+ }
38
+ }
39
+ for (const {descriptor} of resourceRegistry.values()) {
40
+ if (!is.string(descriptor.name)) throw new TypeError('MCP resources require a name string.');
41
+ }
42
+ const pending = new Set();
43
+ const requests = new Map();
44
+ const usedIds = new Set();
45
+ const failures = [];
46
+ const decoder = new TextDecoder(
47
+ 'utf-8',
48
+ {fatal: true}
49
+ );
50
+ let input;
51
+ let output;
52
+ let errorOutput;
53
+ let fragments = [];
54
+ let inputChunks = [];
55
+ let phase = 'new';
56
+ let started = false;
57
+ let closing = false;
58
+ let outputFailed = false;
59
+ let clientInfo;
60
+ let clientCapabilities;
61
+ let writes = Promise.resolve();
62
+ let resolveClosed;
63
+ let rejectClosed;
64
+ const closed = new Promise(
65
+ function retainClosedSettlement(resolve, reject) {
66
+ resolveClosed = resolve;
67
+ rejectClosed = reject;
68
+ }
69
+ );
70
+ // Report failures immediately; callers still receive rejection from closed.
71
+ closed.catch(
72
+ function observeReportedFailure() {}
73
+ );
74
+ const server = {start, cancel, drain, close, closed};
75
+ return server;
76
+
77
+ function start({input: selectedInput = process.stdin, output: selectedOutput = process.stdout, error: selectedError = process.stderr} = {}) {
78
+ if (started || closing) throw new Error('An MCP STDIO server can start only once.');
79
+ if (selectedOutput === selectedError) throw new TypeError('MCP output and diagnostic streams must be separate.');
80
+ input = selectedInput;
81
+ output = selectedOutput;
82
+ errorOutput = selectedError;
83
+ started = true;
84
+ output.on('error', failOutput);
85
+ output.on('close', outputClosed);
86
+ errorOutput.on('error', diagnosticFailed);
87
+ input.on('error', failInput);
88
+ input.on('end', inputEnded);
89
+ input.on('close', inputClosed);
90
+ input.on('data', receiveChunk);
91
+ return server;
92
+ }
93
+
94
+ function cancel(requestId, reason) {
95
+ const request = requests.get(requestId);
96
+ if (!request || request.method === 'initialize' || request.controller.signal.aborted || request.responding) return false;
97
+ request.controller.abort(reason);
98
+ return true;
99
+ }
100
+
101
+ async function drain() {
102
+ while (true) {
103
+ await Promise.all(
104
+ [...pending]
105
+ );
106
+ const currentWrites = writes;
107
+ await currentWrites;
108
+ if (!pending.size && currentWrites === writes) return;
109
+ }
110
+ }
111
+
112
+ function close({cancelPending = false} = {}) {
113
+ if (cancelPending) {
114
+ for (const request of requests.values()) {
115
+ if (!request.responding) {
116
+ request.controller.abort(
117
+ new Error('MCP STDIO session closed.')
118
+ );
119
+ }
120
+ }
121
+ }
122
+ if (closing) return closed;
123
+ closing = true;
124
+ if (started) {
125
+ input.off('data', receiveChunk);
126
+ input.off('end', inputEnded);
127
+ input.off('close', inputClosed);
128
+ input.pause();
129
+ }
130
+ finishClose().catch(rejectClosed);
131
+ return closed;
132
+ }
133
+
134
+ async function finishClose() {
135
+ await drain();
136
+ if (started) {
137
+ input.off('error', failInput);
138
+ output.off('error', failOutput);
139
+ output.off('close', outputClosed);
140
+ errorOutput.off('error', diagnosticFailed);
141
+ }
142
+ fragments = [];
143
+ inputChunks = [];
144
+ requests.clear();
145
+ usedIds.clear();
146
+ if (failures.length) throw new AggregateError(failures, 'MCP STDIO session failed.');
147
+ resolveClosed();
148
+ }
149
+
150
+ function report(cause) {
151
+ try {
152
+ if (onDiagnostic) {
153
+ const observation = onDiagnostic(cause);
154
+ if (observation && is.function(observation.then)) {
155
+ track(
156
+ Promise.resolve(observation).catch(reportDiagnosticFailure)
157
+ );
158
+ }
159
+ } else {
160
+ writeDiagnostic(cause);
161
+ }
162
+ } catch (diagnosticError) {
163
+ reportDiagnosticFailure(diagnosticError);
164
+ }
165
+ }
166
+
167
+ function reportDiagnosticFailure(cause) {
168
+ failures.push(cause);
169
+ writeDiagnostic(cause);
170
+ }
171
+
172
+ function writeDiagnostic(cause) {
173
+ if (!errorOutput) return;
174
+ try {
175
+ const work = writeStream(errorOutput, `${diagnosticText(cause)}\n`);
176
+ track(
177
+ work.catch(diagnosticFailed)
178
+ );
179
+ } catch (diagnosticError) {
180
+ diagnosticFailed(diagnosticError);
181
+ }
182
+ }
183
+
184
+ function diagnosticFailed(cause) {
185
+ if (!failures.includes(cause)) failures.push(cause);
186
+ }
187
+
188
+ function track(work) {
189
+ const observed = work.then(
190
+ function finishWork() {
191
+ pending.delete(observed);
192
+ },
193
+ function unexpectedFailure(cause) {
194
+ pending.delete(observed);
195
+ failInput(cause);
196
+ }
197
+ );
198
+ pending.add(observed);
199
+ }
200
+
201
+ function failInput(cause) {
202
+ failures.push(cause);
203
+ report(cause);
204
+ close(
205
+ {cancelPending: true}
206
+ );
207
+ }
208
+
209
+ function failOutput(cause) {
210
+ if (outputFailed) return;
211
+ outputFailed = true;
212
+ failures.push(cause);
213
+ report(cause);
214
+ close(
215
+ {cancelPending: true}
216
+ );
217
+ }
218
+
219
+ function outputClosed() {
220
+ failOutput(
221
+ new Error('MCP output closed before the session drained.')
222
+ );
223
+ }
224
+
225
+ function receiveChunk(chunk) {
226
+ inputChunks.push(chunk);
227
+ try {
228
+ const text = is.string(chunk) ? chunk : decoder.decode(
229
+ chunk,
230
+ {stream: true}
231
+ );
232
+ receiveText(text);
233
+ // Retain complete source chunks for an unfinished decoder record.
234
+ // A decoded newline makes earlier chunks unnecessary for that record.
235
+ if (text.includes('\n')) inputChunks = [chunk];
236
+ } catch (cause) {
237
+ const failure = new Error(
238
+ 'MCP input could not be decoded or dispatched.',
239
+ {cause}
240
+ );
241
+ failure.inputChunks = inputChunks;
242
+ failure.pendingText = fragments.join('');
243
+ failInput(failure);
244
+ }
245
+ }
246
+
247
+ function receiveText(text) {
248
+ const lines = text.split('\n');
249
+ const trailing = lines.pop();
250
+ for (const line of lines) {
251
+ if (closing) return;
252
+ fragments.push(line);
253
+ const complete = fragments.join('');
254
+ fragments = [];
255
+ receiveLine(complete);
256
+ }
257
+ fragments.push(trailing);
258
+ }
259
+
260
+ function inputEnded() {
261
+ try {
262
+ receiveText(
263
+ decoder.decode()
264
+ );
265
+ // EOF can terminate the final complete JSON record without a newline.
266
+ const finalLine = fragments.join('');
267
+ fragments = [];
268
+ if (finalLine) receiveLine(finalLine);
269
+ } catch (cause) {
270
+ const failure = new Error(
271
+ 'MCP input ended during an unreadable UTF-8 record.',
272
+ {cause}
273
+ );
274
+ failure.inputChunks = inputChunks;
275
+ failure.pendingText = fragments.join('');
276
+ failInput(failure);
277
+ return;
278
+ }
279
+ close();
280
+ }
281
+
282
+ function inputClosed() {
283
+ if (!closing) {
284
+ const failure = new Error('MCP input closed without completing its stream.');
285
+ failure.inputChunks = inputChunks;
286
+ failure.pendingText = fragments.join('');
287
+ failInput(failure);
288
+ }
289
+ }
290
+
291
+ function receiveLine(line) {
292
+ let message;
293
+ try {
294
+ message = JSON.parse(line);
295
+ } catch (cause) {
296
+ const failure = new Error(
297
+ 'MCP input contained malformed JSON.',
298
+ {cause}
299
+ );
300
+ failure.line = line;
301
+ report(failure);
302
+ sendError(
303
+ null,
304
+ new McpProtocolError(-32700, 'Parse error')
305
+ );
306
+ return;
307
+ }
308
+ if (!record(message) || message.jsonrpc !== '2.0') {
309
+ sendError(
310
+ null,
311
+ new McpProtocolError(-32600, 'Invalid Request')
312
+ );
313
+ return;
314
+ }
315
+ const hasId = Object.hasOwn(message, 'id');
316
+ if (!is.string(message.method)) {
317
+ // This server sends no requests; an unsolicited response has no reply.
318
+ if (hasId && (Object.hasOwn(message, 'result') || Object.hasOwn(message, 'error'))) return;
319
+ sendError(
320
+ null,
321
+ new McpProtocolError(-32600, 'Invalid Request')
322
+ );
323
+ return;
324
+ }
325
+ if (!hasId) {
326
+ receiveNotification(message);
327
+ return;
328
+ }
329
+ if (!requestId(message.id)) {
330
+ sendError(
331
+ null,
332
+ new McpProtocolError(-32600, 'Request id must be a string or integer.')
333
+ );
334
+ return;
335
+ }
336
+ if (usedIds.has(message.id)) {
337
+ sendError(
338
+ message.id,
339
+ new McpProtocolError(-32600, 'Request id was already used in this session.')
340
+ );
341
+ return;
342
+ }
343
+ usedIds.add(message.id);
344
+ const request = {method: message.method, controller: new AbortController(), responding: false};
345
+ requests.set(message.id, request);
346
+ track(
347
+ runRequest(message, request)
348
+ );
349
+ }
350
+
351
+ function receiveNotification(message) {
352
+ if (message.params !== undefined && !record(message.params)) return;
353
+ if (message.method === 'notifications/initialized' && phase === 'initialized') {
354
+ phase = 'ready';
355
+ } else if (message.method === 'notifications/cancelled' && record(message.params)) {
356
+ const {requestId: id, reason} = message.params;
357
+ if (requestId(id) && (reason === undefined || is.string(reason))) cancel(id, reason);
358
+ }
359
+ }
360
+
361
+ async function runRequest(message, request) {
362
+ try {
363
+ if (message.params !== undefined && !record(message.params)) {
364
+ throw new McpProtocolError(-32602, 'Request params must be an object.');
365
+ }
366
+ const context = {
367
+ signal: request.controller.signal,
368
+ requestId: message.id,
369
+ method: message.method,
370
+ clientInfo,
371
+ clientCapabilities,
372
+ params: message.params
373
+ };
374
+ const params = message.params ?? {};
375
+ const result = await dispatch(message.method, params, context);
376
+ if (!request.controller.signal.aborted) {
377
+ request.responding = true;
378
+ queueMessage(
379
+ {jsonrpc: '2.0', id: message.id, result}
380
+ );
381
+ }
382
+ } catch (cause) {
383
+ if (request.controller.signal.aborted) {
384
+ report(cause);
385
+ } else {
386
+ request.responding = true;
387
+ const failure = cause instanceof McpProtocolError
388
+ ? cause
389
+ : new McpProtocolError(-32603, 'Internal error');
390
+ if (!(cause instanceof McpProtocolError)) report(cause);
391
+ sendError(message.id, failure);
392
+ }
393
+ } finally {
394
+ requests.delete(message.id);
395
+ }
396
+ }
397
+
398
+ async function dispatch(method, params, context) {
399
+ if (method === 'initialize') {
400
+ if (phase !== 'new') throw new McpProtocolError(-32600, 'Session is already initialized.');
401
+ if (!is.string(params.protocolVersion) || !record(params.capabilities) || !record(params.clientInfo)
402
+ || !is.string(params.clientInfo.name) || !is.string(params.clientInfo.version)) {
403
+ throw new McpProtocolError(-32602, 'initialize requires protocolVersion, capabilities and clientInfo.');
404
+ }
405
+ clientInfo = params.clientInfo;
406
+ clientCapabilities = params.capabilities;
407
+ phase = 'initialized';
408
+ const capabilities = {};
409
+ if (toolRegistry.size) capabilities.tools = {};
410
+ if (resourceRegistry.size) capabilities.resources = {};
411
+ const result = {protocolVersion: MCP_PROTOCOL_VERSION, capabilities, serverInfo};
412
+ if (instructions !== undefined) result.instructions = instructions;
413
+ return result;
414
+ }
415
+ if (method === 'ping') return {};
416
+ if (phase !== 'ready') throw new McpProtocolError(-32000, 'Send initialize and notifications/initialized first.');
417
+ switch (method) {
418
+ case 'tools/list':
419
+ requireFirstPage(params);
420
+ return {tools: descriptors(toolRegistry)};
421
+ case 'tools/call': {
422
+ if (!is.string(params.name) || (params.arguments !== undefined && !record(params.arguments))) {
423
+ throw new McpProtocolError(-32602, 'tools/call requires a name and optional arguments object.');
424
+ }
425
+ if (params.task !== undefined) throw new McpProtocolError(-32602, 'Task-augmented execution is not supported.');
426
+ const tool = toolRegistry.get(params.name);
427
+ if (!tool) throw new McpProtocolError(-32602, `Unknown tool: ${params.name}`);
428
+ let result;
429
+ try {
430
+ const args = params.arguments ?? {};
431
+ result = await tool.handler(args, context);
432
+ } catch (cause) {
433
+ if (cause instanceof McpProtocolError || context.signal.aborted) throw cause;
434
+ report(cause);
435
+ const text = is.string(cause?.message) ? cause.message : diagnosticText(cause);
436
+ return {content: [{type: 'text', text}], isError: true};
437
+ }
438
+ if (!record(result) || !is.array(result.content)) {
439
+ throw new TypeError('MCP tool handlers must return an explicit CallToolResult with a content array.');
440
+ }
441
+ return result;
442
+ }
443
+ case 'resources/list':
444
+ requireFirstPage(params);
445
+ return {resources: descriptors(resourceRegistry)};
446
+ case 'resources/templates/list':
447
+ requireFirstPage(params);
448
+ return {resourceTemplates: []};
449
+ case 'resources/read': {
450
+ if (!is.string(params.uri)) throw new McpProtocolError(-32602, 'resources/read requires a uri string.');
451
+ const resource = resourceRegistry.get(params.uri);
452
+ if (!resource) {
453
+ throw new McpProtocolError(
454
+ -32002,
455
+ 'Resource not found',
456
+ {uri: params.uri}
457
+ );
458
+ }
459
+ const result = await resource.handler(params, context);
460
+ if (!record(result) || !is.array(result.contents)) {
461
+ throw new TypeError('MCP resource handlers must return an explicit ReadResourceResult with a contents array.');
462
+ }
463
+ return result;
464
+ }
465
+ default:
466
+ throw new McpProtocolError(-32601, `Method not found: ${method}`);
467
+ }
468
+ }
469
+
470
+ function sendError(id, cause) {
471
+ const error = {code: cause.code, message: cause.message};
472
+ if (cause.data !== undefined) error.data = cause.data;
473
+ try {
474
+ queueMessage(
475
+ {jsonrpc: '2.0', id, error}
476
+ );
477
+ } catch (encodingError) {
478
+ report(encodingError);
479
+ queueMessage(
480
+ {jsonrpc: '2.0', id, error: {code: -32603, message: 'Internal error'}}
481
+ );
482
+ }
483
+ }
484
+
485
+ function queueMessage(message) {
486
+ if (outputFailed) return;
487
+ const encoded = `${JSON.stringify(message)}\n`;
488
+ // Only the shared output stream is ordered; handlers run independently.
489
+ writes = writes.then(
490
+ function writeMessage() {
491
+ if (outputFailed) return;
492
+ return writeStream(output, encoded);
493
+ }
494
+ ).catch(failOutput);
495
+ }
496
+ }
497
+
498
+ /** A terminal writable event may arrive without the pending write callback. */
499
+ function writeStream(stream, text) {
500
+ return new Promise(
501
+ function awaitStreamWrite(resolve, reject) {
502
+ let settled = false;
503
+ stream.on('error', settle);
504
+ stream.on('close', terminal);
505
+ stream.on('finish', terminal);
506
+ if (stream.errored || stream.destroyed || stream.writableEnded) {
507
+ terminal();
508
+ return;
509
+ }
510
+ try {
511
+ stream.write(text, 'utf8', settle);
512
+ } catch (cause) {
513
+ settle(cause);
514
+ }
515
+
516
+ function terminal() {
517
+ settle(
518
+ stream.errored ?? new Error('MCP writable stream ended before its write callback completed.')
519
+ );
520
+ }
521
+
522
+ function settle(cause) {
523
+ if (settled) return;
524
+ settled = true;
525
+ stream.off('error', settle);
526
+ stream.off('close', terminal);
527
+ stream.off('finish', terminal);
528
+ if (cause) reject(cause);
529
+ else resolve();
530
+ }
531
+ }
532
+ );
533
+ }
534
+
535
+ function record(value) {
536
+ return value !== null && is.object(value) && !is.array(value);
537
+ }
538
+
539
+ function requestId(value) {
540
+ return is.string(value) || is.integer(value);
541
+ }
542
+
543
+ function createRegistry(entries, key) {
544
+ if (!is.array(entries)) throw new TypeError('MCP tools and resources must be arrays.');
545
+ const registry = new Map();
546
+ for (const entry of entries) {
547
+ if (!record(entry) || !is.string(entry[key]) || !is.function(entry.handler)) {
548
+ throw new TypeError(`Each MCP descriptor requires a ${key} string and handler function.`);
549
+ }
550
+ if (registry.has(entry[key])) throw new TypeError(`Duplicate MCP ${key}: ${entry[key]}`);
551
+ const {handler, ...descriptor} = entry;
552
+ registry.set(
553
+ entry[key],
554
+ {descriptor, handler}
555
+ );
556
+ }
557
+ return registry;
558
+ }
559
+
560
+ function descriptors(registry) {
561
+ return Array.from(
562
+ registry.values(),
563
+ function readDescriptor(entry) { return entry.descriptor; }
564
+ );
565
+ }
566
+
567
+ function requireFirstPage(params) {
568
+ if (params.cursor !== undefined) {
569
+ throw new McpProtocolError(-32602, 'This server returns its complete catalog without pagination.');
570
+ }
571
+ }
572
+
573
+ function diagnosticText(cause) {
574
+ if (is.string(cause)) return cause;
575
+ return inspect(
576
+ cause,
577
+ {depth: null, maxArrayLength: null, maxStringLength: null, customInspect: false}
578
+ );
579
+ }
@@ -222,7 +222,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
222
222
  ' });',
223
223
  ' if (runtime.models) resolved.models = runtime.models.map(function nativeModel(model) {',
224
224
  ' const resolvedModel = {...model};',
225
- ' if (model.path !== undefined) resolvedModel.path = runtimePath(model.path);',
225
+ " for (const field of ['path', 'encoderPath', 'encoderDataPath']) {",
226
+ ' if (model[field] !== undefined) resolvedModel[field] = runtimePath(model[field]);',
227
+ ' }',
226
228
  ' if (model.resources !== undefined) {',
227
229
  ' resolvedModel.resources = {};',
228
230
  ' for (const [role, resource] of Object.entries(model.resources)) {',
package/src/toolchain.mjs CHANGED
@@ -25,6 +25,7 @@ import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
25
25
  import {refreshAppPackageProjection} from './app-descriptor.mjs';
26
26
  import {configureDevelopmentLocalAI} from './local-ai/config.mjs';
27
27
  import {createDevelopmentLocalAI} from './local-ai/dev.mjs';
28
+ import {createDevelopmentCore} from './core/development.mjs';
28
29
  import {
29
30
  discoverApps as discoverPackagerApps,
30
31
  inspectApp as inspectPackagedApp,
@@ -626,15 +627,25 @@ export async function developApplication(options = {}) {
626
627
  }
627
628
  );
628
629
  const localAIConfig = prepared.descriptor?.native?.localAI;
629
- const localAI = localAIConfig?.runtimes.length ? createDevelopmentLocalAI({
630
- config: localAIConfig,
630
+ const serviceModules = options.serviceModules ?? prepared.descriptor?.native?.services ?? [];
631
+ const coreOptions = {
631
632
  appRoot: prepared.appRoot,
632
- directory: path.join(prepared.appRoot, '.arcane', 'local-ai', 'runtimes'),
633
633
  application: prepared.descriptor,
634
634
  version: prepared.descriptor.version,
635
635
  signal: options.signal,
636
- onEvent: options.onEvent
636
+ onEvent: options.onEvent,
637
+ services: options.services ?? [],
638
+ serviceModules,
639
+ context: options.context ?? {}
640
+ };
641
+ const localAI = localAIConfig?.runtimes.length ? createDevelopmentLocalAI({
642
+ ...coreOptions,
643
+ config: localAIConfig,
644
+ directory: path.join(prepared.appRoot, '.arcane', 'local-ai', 'runtimes')
637
645
  }) : undefined;
646
+ const core = localAI ?? (serviceModules.length || coreOptions.services.length
647
+ ? createDevelopmentCore(coreOptions)
648
+ : undefined);
638
649
  let server;
639
650
  try {
640
651
  server=await startDevServer({
@@ -653,19 +664,19 @@ export async function developApplication(options = {}) {
653
664
  tls:options.tls,
654
665
  certPath:options.certPath,
655
666
  keyPath:options.keyPath,
656
- localAI,
667
+ core,
657
668
  signal:options.signal,
658
669
  onEvent:options.onEvent
659
670
  });
660
671
  } catch (error) {
661
672
  try {
662
- await localAI?.close(error);
673
+ await core?.close(error);
663
674
  } catch (closeError) {
664
- throw new AggregateError([error, closeError], 'Development startup and local AI shutdown failed.');
675
+ throw new AggregateError([error, closeError], 'Development startup and Core shutdown failed.');
665
676
  }
666
677
  throw error;
667
678
  }
668
- return {...server,appId:prepared.appId,mode:'source',...(localAI ? {localAI} : {})};
679
+ return {...server,appId:prepared.appId,mode:'source',...(core ? {core} : {}),...(localAI ? {localAI} : {})};
669
680
  }
670
681
 
671
682
  export async function packageApplication(options={}){