arcane-os 0.54.1 → 0.56.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 (45) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +35 -28
  3. package/browser-runtime/core/client.mjs +552 -0
  4. package/browser-runtime/core/contracts.mjs +85 -0
  5. package/docs/architecture.md +31 -23
  6. package/docs/compatibility.md +10 -1
  7. package/docs/consumer-updates.md +82 -0
  8. package/docs/platform-targets.md +20 -11
  9. package/docs/publishing.md +41 -18
  10. package/docs/reference/README.md +19 -7
  11. package/docs/reference/availability-and-normalization.md +15 -5
  12. package/docs/reference/behavioral-testing.md +7 -6
  13. package/docs/reference/cli.md +12 -4
  14. package/docs/reference/core-client.md +163 -0
  15. package/docs/reference/core-native-packaging.md +149 -0
  16. package/docs/reference/core-runtime.md +185 -0
  17. package/docs/reference/inventory/package-api.json +271 -5
  18. package/docs/reference/protocols.md +38 -14
  19. package/docs/reference/pwa.md +17 -0
  20. package/docs/reference/sdk-api.md +772 -15
  21. package/package.json +10 -3
  22. package/schemas/arcane-app.schema.json +10 -0
  23. package/schemas/arcane-package.schema.json +10 -0
  24. package/schemas/native-build-plan.schema.json +8 -0
  25. package/src/app-descriptor.mjs +18 -1
  26. package/src/app-layout.mjs +6 -2
  27. package/src/cli/main.mjs +8 -6
  28. package/src/core/host.mjs +23 -0
  29. package/src/core/hosts/arcane-core-loader.cjs +2 -0
  30. package/src/core/hosts/linux/arcane_core_process.c +568 -0
  31. package/src/core/hosts/linux/arcane_core_process.h +127 -0
  32. package/src/core/hosts/sea-launcher.cjs +17 -0
  33. package/src/core/hosts/windows/ArcaneCoreProcess.cs +416 -0
  34. package/src/core/runtime.mjs +345 -0
  35. package/src/core/stdio.mjs +209 -0
  36. package/src/core-classic-source.mjs +43 -0
  37. package/src/import-map.mjs +62 -0
  38. package/src/native/portable-layout.mjs +184 -0
  39. package/src/native/portable-provider.mjs +211 -0
  40. package/src/native-plan.mjs +10 -4
  41. package/src/native-provider-loader.mjs +15 -4
  42. package/src/packager/core.mjs +131 -32
  43. package/src/process.mjs +8 -4
  44. package/src/targets/index.mjs +3 -1
  45. package/src/toolchain.mjs +23 -6
@@ -0,0 +1,552 @@
1
+ import {arcaneEvents} from '../event-manager.mjs';
2
+ import {CORE_PROTOCOL,CORE_READY_EVENTS,CoreError,serializeCoreError} from './contracts.mjs';
3
+
4
+ const CORE_CLIENT_KEY=Symbol.for('arcane-os.core.client');
5
+ const CORE_CLIENT_INSTALLATION=Symbol.for('arcane-os.core.client.installation');
6
+ const CORE_EVENT='core.rpc.event';
7
+ const LONG_OPERATION_TIMEOUT=50*60*1000;
8
+
9
+ /** One RPC client. Native execution and service policy belong to the host. */
10
+ export function createCoreClient({
11
+ global=globalThis,
12
+ transport:providedTransport=null,
13
+ eventOwner=arcaneEvents,
14
+ eventOwnerReady,
15
+ autoConnect=true,
16
+ replayRuntimeState=false,
17
+ onError=error=>global.console?.error('Arcane Core client failed.',error)
18
+ }={}){
19
+ const pending=new Map();
20
+ const acknowledgements=new Map();
21
+ const completedEvents=new Map();
22
+ const eventActions=[];
23
+ const cleanups=[];
24
+ const eventIdentity={};
25
+ let source=null;
26
+ let drainingEvents=false;
27
+ let transport=null;
28
+ let closed=false;
29
+ let eventOwnerFailure=null;
30
+ let transportFailure=null;
31
+ let transportFailureReported=false;
32
+
33
+ function report(error){
34
+ if(error===transportFailure){
35
+ if(transportFailureReported)return;
36
+ transportFailureReported=true;
37
+ }
38
+ try{onError(error instanceof CoreError?error:new CoreError(error));}
39
+ catch(reportError){global.console?.error('Arcane Core error listener failed.',reportError,error);}
40
+ }
41
+ function uuid(){
42
+ return global.crypto?.randomUUID?.()
43
+ ??`req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
44
+ }
45
+ function attachEventOwner(owner){
46
+ if(closed)return;
47
+ source=owner.createSource(eventIdentity,{source:'core-client',eventTypes:[CORE_EVENT]});
48
+ // This is an ordered handoff queue, not an event dispatcher. The SDK
49
+ // event owner alone manages subscriptions and delivery.
50
+ drainingEvents=true;
51
+ try{
52
+ while(eventActions.length&&!closed)eventActions.shift()();
53
+ }finally{
54
+ drainingEvents=false;
55
+ }
56
+ }
57
+ function withEvents(action){
58
+ if(closed)return;
59
+ if(source&&!drainingEvents)action();
60
+ else eventActions.push(action);
61
+ }
62
+ if(eventOwner)attachEventOwner(eventOwner);
63
+ const eventsReady=eventOwner?Promise.resolve():Promise.resolve(eventOwnerReady).then(attachEventOwner);
64
+ eventsReady.catch(function failEventOwner(error){
65
+ eventOwnerFailure=error instanceof CoreError?error:new CoreError(error);
66
+ for(const id of pending.keys())settle(id,eventOwnerFailure);
67
+ report(eventOwnerFailure);
68
+ });
69
+
70
+ function emit(event,data){
71
+ withEvents(()=>source.dispatch(CORE_EVENT,{event,data}));
72
+ }
73
+ function complete(event,data){
74
+ if(completedEvents.has(event))return false;
75
+ completedEvents.set(event,data);
76
+ emit(event,data);
77
+ return true;
78
+ }
79
+ const events={
80
+ on(event,listener){
81
+ if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
82
+ let active=!closed;
83
+ let dispose=null;
84
+ withEvents(()=>{
85
+ if(!active)return;
86
+ dispose=source.on(CORE_EVENT,function receiveCoreEvent(occurrence){
87
+ if(!active)return;
88
+ const message=occurrence.detail;
89
+ if(event==='*')listener(message);
90
+ else if(event===message.event)listener(message.data);
91
+ });
92
+ });
93
+ return function unsubscribe(){
94
+ if(!active)return false;
95
+ active=false;
96
+ dispose?.();
97
+ return true;
98
+ };
99
+ },
100
+ once(event,listener){
101
+ if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
102
+ const unsubscribe=events.on(event,function receiveOnce(value){
103
+ unsubscribe();
104
+ listener(value);
105
+ });
106
+ return unsubscribe;
107
+ },
108
+ when(event,listener){
109
+ if(!CORE_READY_EVENTS.includes(event))throw new TypeError('Arcane completion event is not designated as durable.');
110
+ if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
111
+ if(!completedEvents.has(event))return events.once(event,listener);
112
+ const value=completedEvents.get(event);
113
+ let active=!closed;
114
+ Promise.resolve().then(function replayReadyEvent(){
115
+ if(!active||closed)return;
116
+ active=false;
117
+ try{listener(value);}catch(error){report(error);}
118
+ });
119
+ return function unsubscribe(){active=false;};
120
+ },
121
+ completed(event){return CORE_READY_EVENTS.includes(event)&&completedEvents.has(event);}
122
+ };
123
+
124
+ function settle(id,error,result){
125
+ const request=pending.get(id);
126
+ if(!request)return false;
127
+ pending.delete(id);
128
+ clearTimeout(request.timer);
129
+ request.signal?.removeEventListener('abort',request.abort);
130
+ if(error)request.reject(error);
131
+ else request.resolve(result);
132
+ return true;
133
+ }
134
+ function receive(input){
135
+ if(closed)return false;
136
+ let message=input;
137
+ if(typeof message==='string'){
138
+ try{message=JSON.parse(message);}
139
+ catch(error){report(new CoreError({code:'ARCANE_INVALID_NATIVE_JSON',message:error.message,input,cause:error}));return false;}
140
+ }
141
+ if(!message||message.protocol!==CORE_PROTOCOL)return false;
142
+ if(message.type==='event'){
143
+ const data=Object.hasOwn(message,'data')?message.data:{};
144
+ if(CORE_READY_EVENTS.includes(message.event))complete(message.event,data);
145
+ else emit(message.event,data);
146
+ return true;
147
+ }
148
+ if(message.type!=='response'||!pending.has(message.id))return false;
149
+ // Final responses follow earlier event deliveries, including while a
150
+ // classic host is connecting the shared SDK event owner. A stream's
151
+ // completion must not remove its subscription before its chunks arrive.
152
+ withEvents(()=>settle(message.id,message.ok?null:new CoreError(message.error),message.result));
153
+ return true;
154
+ }
155
+ function installCallback(name,value){
156
+ const previous=global[name];
157
+ global[name]=value;
158
+ cleanups.push(function restoreCallback(){
159
+ if(global[name]!==value)return;
160
+ if(previous===undefined)delete global[name];
161
+ else global[name]=previous;
162
+ });
163
+ }
164
+ function parseAcknowledgement(input,{required=false}={}){
165
+ let acknowledgement=input;
166
+ if(typeof acknowledgement==='string'){
167
+ try{acknowledgement=JSON.parse(acknowledgement);}catch{acknowledgement=null;}
168
+ }
169
+ if(acknowledgement?.accepted===false){
170
+ throw new CoreError(acknowledgement.error??{code:'BRIDGE_REJECTED',message:'The native host rejected the request.'});
171
+ }
172
+ if(required&&acknowledgement?.accepted!==true){
173
+ throw new CoreError({code:'NATIVE_BRIDGE_REPLY_INVALID',message:'The native host returned an invalid acknowledgement.'});
174
+ }
175
+ }
176
+ function acknowledge(token,value){
177
+ const record=acknowledgements.get(token);
178
+ if(!record)return false;
179
+ acknowledgements.delete(token);
180
+ clearTimeout(record.timer);
181
+ try{parseAcknowledgement(value,{required:true});record.resolve();}
182
+ catch(error){record.reject(error);}
183
+ return true;
184
+ }
185
+ function chooseTransport(){
186
+ const webview=global.chrome?.webview;
187
+ if(webview?.hostObjects){
188
+ const bridge=webview.hostObjects.arcaneBridge;
189
+ function nativeMessage(event){receive(event.data);}
190
+ webview.addEventListener('message',nativeMessage);
191
+ cleanups.push(()=>webview.removeEventListener?.('message',nativeMessage));
192
+ return {name:'webview2',async send(frame){
193
+ try{parseAcknowledgement(await bridge.Send(JSON.stringify(frame)));}
194
+ catch(error){
195
+ if(error instanceof CoreError)throw error;
196
+ const details=serializeCoreError(error);
197
+ if(error?.code===undefined)details.code='ARCANE_BRIDGE_CALL_FAILED';
198
+ throw new CoreError({
199
+ method:frame.method,transport:'webview2',...details
200
+ });
201
+ }
202
+ }};
203
+ }
204
+ const webkit=global.webkit?.messageHandlers?.arcane;
205
+ if(typeof global.__arcaneWebKitPostMessage==='function'||webkit){
206
+ installCallback('__arcaneReceive',receive);
207
+ installCallback('__arcaneWebKitAcknowledge',acknowledge);
208
+ return {name:'webkitgtk',async send(frame){
209
+ const serialized=JSON.stringify(frame);
210
+ if(typeof global.__arcaneWebKitPostMessage!=='function'){
211
+ parseAcknowledgement(await webkit.postMessage(serialized),{required:true});
212
+ return;
213
+ }
214
+ const token=uuid();
215
+ return new Promise(function sendWebKitFrame(resolve,reject){
216
+ const timer=setTimeout(function missingAcknowledgement(){
217
+ acknowledgements.delete(token);
218
+ reject(new CoreError({code:'NATIVE_BRIDGE_ACK_TIMEOUT',message:'The native host did not acknowledge the request.'}));
219
+ },30000);
220
+ acknowledgements.set(token,{resolve,reject,timer});
221
+ try{
222
+ if(global.__arcaneWebKitPostMessage(token,serialized)!==true){
223
+ throw new CoreError({code:'NATIVE_BRIDGE_UNTRUSTED_MAIN_FRAME',message:'The native host did not accept the request.'});
224
+ }
225
+ }catch(error){
226
+ acknowledgements.delete(token);
227
+ clearTimeout(timer);
228
+ reject(error instanceof CoreError?error:new CoreError(error));
229
+ }
230
+ });
231
+ }};
232
+ }
233
+ const android=global.arcaneAndroid;
234
+ if(typeof android?.postMessage==='function'){
235
+ const previous=android.onmessage;
236
+ function androidMessage(message){
237
+ receive(message&&typeof message==='object'&&'data' in message?message.data:message);
238
+ }
239
+ android.onmessage=androidMessage;
240
+ cleanups.push(()=>{if(android.onmessage===androidMessage)android.onmessage=previous;});
241
+ return {name:'android-webview',async send(frame){
242
+ try{await android.postMessage(JSON.stringify(frame));}
243
+ catch(error){
244
+ const details=serializeCoreError(error);
245
+ if(error?.code===undefined)details.code='ARCANE_ANDROID_BRIDGE_CALL_FAILED';
246
+ throw new CoreError({
247
+ method:frame.method,transport:'android-webview',...details
248
+ });
249
+ }
250
+ }};
251
+ }
252
+ if(global.__ARCANE_DEV_HTTP__){
253
+ if(typeof global.EventSource==='function'){
254
+ const stream=new global.EventSource('/events');
255
+ stream.onmessage=event=>receive(event.data);
256
+ stream.onerror=event=>report(new CoreError({code:'DEV_BRIDGE_EVENT_ERROR',message:'The development event connection reported an error.',event}));
257
+ cleanups.push(()=>stream.close());
258
+ }
259
+ return {name:'development-http',async send(frame){
260
+ const response=await global.fetch('/rpc',{
261
+ method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(frame)
262
+ });
263
+ const payload=await response.json();
264
+ if(!response.ok)throw new CoreError(payload?.error??{code:'DEV_BRIDGE_FAILED',message:'The development bridge failed.'});
265
+ if(payload?.type==='response')receive(payload);
266
+ }};
267
+ }
268
+ throw new CoreError({code:'ARCANE_TRANSPORT_UNAVAILABLE',message:'This interface is not connected to an Arcane native host.'});
269
+ }
270
+ function connect(){
271
+ if(closed)throw transportFailure??new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
272
+ if(transport)return transport;
273
+ const cleanupStart=cleanups.length;
274
+ try{
275
+ const selected=providedTransport??chooseTransport();
276
+ if(typeof selected?.send!=='function'){
277
+ throw new CoreError({code:'ARCANE_TRANSPORT_INVALID',message:'The Core transport must provide send(frame).'});
278
+ }
279
+ transport=selected;
280
+ if(typeof transport.subscribe==='function'){
281
+ const unsubscribe=transport.subscribe(receive);
282
+ if(typeof unsubscribe==='function'){
283
+ if(closed){
284
+ try{unsubscribe();}catch(error){report(error);}
285
+ }else cleanups.push(unsubscribe);
286
+ }
287
+ }
288
+ }catch(error){
289
+ transport=null;
290
+ for(const cleanup of cleanups.splice(cleanupStart)){
291
+ try{cleanup();}catch(cleanupError){report(cleanupError);}
292
+ }
293
+ throw error;
294
+ }
295
+ if(closed)throw transportFailure??new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
296
+ complete('transport.ready',{protocol:CORE_PROTOCOL,transport:transport.name});
297
+ if(replayRuntimeState&&!closed)sendControl({protocol:CORE_PROTOCOL,type:'control',control:'runtime.replay'});
298
+ // A native callback may close the client synchronously during replay.
299
+ if(closed)throw transportFailure??new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
300
+ return transport;
301
+ }
302
+ function runtimeSnapshot(){
303
+ const name=transport?.name??providedTransport?.name
304
+ ??(global.chrome?.webview?.hostObjects?'webview2'
305
+ :typeof global.__arcaneWebKitPostMessage==='function'||global.webkit?.messageHandlers?.arcane?'webkitgtk'
306
+ :typeof global.arcaneAndroid?.postMessage==='function'?'android-webview'
307
+ :global.__ARCANE_DEV_HTTP__?'development-http':'standalone');
308
+ return {connected:transport!==null&&!closed,transport:name,
309
+ native:['webview2','webkitgtk','android-webview'].includes(name),
310
+ managedLocalAI:['webview2','webkitgtk'].includes(name)};
311
+ }
312
+ function sendControl(frame){
313
+ if(!transport)return;
314
+ try{Promise.resolve(transport.send(frame)).catch(report);}catch(error){report(error);}
315
+ }
316
+ function aborted(method){
317
+ return new CoreError({name:'AbortError',code:'ARCANE_REQUEST_ABORTED',message:'The Arcane request was cancelled.',method});
318
+ }
319
+ function cancel(id,error){
320
+ if(!settle(id,error))return false;
321
+ sendControl({protocol:CORE_PROTOCOL,type:'control',control:'request.cancel',requestId:id});
322
+ return true;
323
+ }
324
+ function cancelAll(){
325
+ if(!pending.size)return false;
326
+ for(const [id,request] of pending)settle(id,aborted(request.method));
327
+ sendControl({protocol:CORE_PROTOCOL,type:'control',control:'requests.cancelAll'});
328
+ return true;
329
+ }
330
+ function invoke(method,parameters={},options={}){
331
+ if(transportFailure)return Promise.reject(transportFailure);
332
+ if(eventOwnerFailure)return Promise.reject(eventOwnerFailure);
333
+ let selected;
334
+ try{selected=connect();}catch(error){return Promise.reject(error);}
335
+ const {signal,timeoutMs=10*60*1000}=options??{};
336
+ if(signal&&(typeof signal.aborted!=='boolean'
337
+ ||typeof signal.addEventListener!=='function'
338
+ ||typeof signal.removeEventListener!=='function')){
339
+ return Promise.reject(new TypeError('Arcane request signal must be an AbortSignal.'));
340
+ }
341
+ if(signal?.aborted)return Promise.reject(aborted(method));
342
+ const id=uuid();
343
+ const frame={protocol:CORE_PROTOCOL,type:'request',id,method,parameters,sentAt:new Date().toISOString()};
344
+ const promise=new Promise(function ownRequest(resolve,reject){
345
+ const abort=()=>cancel(id,aborted(method));
346
+ const timer=timeoutMs>0?setTimeout(()=>cancel(id,new CoreError({
347
+ code:'ARCANE_REQUEST_TIMEOUT',message:'Arcane did not finish the operation before the request timed out.',method
348
+ })),timeoutMs):null;
349
+ pending.set(id,{resolve,reject,timer,signal,abort,method});
350
+ signal?.addEventListener('abort',abort,{once:true});
351
+ if(signal?.aborted)abort();
352
+ });
353
+ if(!pending.has(id))return promise;
354
+ function sendFailed(error){
355
+ const failure=error instanceof CoreError?error:new CoreError(error);
356
+ if(!settle(id,failure))report(failure);
357
+ }
358
+ try{
359
+ Promise.resolve(selected.send(frame)).catch(sendFailed);
360
+ }catch(error){sendFailed(error);}
361
+ return promise;
362
+ }
363
+ function disposeTransport(error){
364
+ for(const record of acknowledgements.values()){
365
+ clearTimeout(record.timer);
366
+ record.reject(error);
367
+ }
368
+ acknowledgements.clear();
369
+ for(const cleanup of cleanups.splice(0)){
370
+ try{cleanup();}catch(error){report(error);}
371
+ }
372
+ try{source?.dispose();}catch(disposeError){report(disposeError);}
373
+ source=null;
374
+ transport=null;
375
+ // Registration closures and already-delivered payloads belong to this
376
+ // client lifetime and are released when the owner explicitly closes it.
377
+ eventActions.length=0;
378
+ completedEvents.clear();
379
+ }
380
+ function failTransport(error){
381
+ if(closed)return false;
382
+ transportFailure=error instanceof CoreError?error:new CoreError(error);
383
+ closed=true;
384
+ for(const id of pending.keys())settle(id,transportFailure);
385
+ disposeTransport(transportFailure);
386
+ report(transportFailure);
387
+ return true;
388
+ }
389
+ function close(){
390
+ if(closed)return false;
391
+ closed=true;
392
+ cancelAll();
393
+ disposeTransport(new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'}));
394
+ return true;
395
+ }
396
+ if(typeof global.addEventListener==='function'){
397
+ global.addEventListener('pagehide',cancelAll,{capture:true});
398
+ cleanups.push(()=>global.removeEventListener('pagehide',cancelAll,{capture:true}));
399
+ }
400
+ if(autoConnect){
401
+ try{connect();}catch(error){if(error.code!=='ARCANE_TRANSPORT_UNAVAILABLE')report(error);}
402
+ }
403
+ return {protocol:CORE_PROTOCOL,Error:CoreError,invoke,receive,connect,close,failTransport,cancelAll,events,eventsReady,runtime:{current:runtimeSnapshot},uuid};
404
+ }
405
+
406
+ /** Existing namespace call shapes; method implementations remain host-owned. */
407
+ export function createCoreFacade(client){
408
+ const {invoke,events,uuid}=client;
409
+ const long={timeoutMs:LONG_OPERATION_TIMEOUT};
410
+ function ollamaInvoke(operation,request={},options={}){
411
+ const onChunk=typeof options==='function'?options:options?.onChunk;
412
+ const timeoutMs=options?.timeoutMs??(['pull','push','create'].includes(operation)?LONG_OPERATION_TIMEOUT:10*60*1000);
413
+ if(typeof onChunk!=='function')return invoke(`ollama.${operation}`,request,{timeoutMs,signal:options?.signal});
414
+ const streamId=uuid();
415
+ const unsubscribe=events.on('ollama.chunk',function streamChunk(event){
416
+ if(event?.streamId===streamId)onChunk(event.chunk,{operation,streamId});
417
+ });
418
+ return invoke(`ollama.${operation}`,{...request,stream:true,streamId},{timeoutMs,signal:options?.signal}).finally(unsubscribe);
419
+ }
420
+ function environmentNameIsSensitive(name){
421
+ const canonical=String(name??'').toUpperCase();
422
+ return canonical.split(/[_.-]+/u).some(token=>[
423
+ 'AUTH','BEARER','CREDENTIAL','CREDENTIALS','KEY','KEYS','PASS','PASSWD','PASSWORD','PASSWORDS','PWD','SECRET','SECRETS','TOKEN','TOKENS'
424
+ ].includes(token))||['ACCESSKEY','ACCESSTOKEN','APIKEY','AUTHKEY','AUTHTOKEN','CLIENTSECRET','PRIVATEKEY'].some(suffix=>canonical.endsWith(suffix));
425
+ }
426
+ return {
427
+ protocol:CORE_PROTOCOL,Error:CoreError,runtime:client.runtime,events,
428
+ ai:{
429
+ models:()=>invoke('ai.models'),chat:request=>invoke('ai.chat',request??{},{timeoutMs:130000}),
430
+ profile:()=>invoke('ai.profile.current'),providerSettings:()=>invoke('ai.provider.settings.get'),
431
+ saveProviderSettings:settings=>invoke('ai.provider.settings.set',settings??{},{timeoutMs:130000}),
432
+ providerModels:()=>invoke('ai.provider.models',{},{timeoutMs:130000})
433
+ },
434
+ environment:{list:()=>invoke('environment.list'),get:name=>invoke('environment.get',{name}),
435
+ set:(name,value,options)=>invoke('environment.set',{name,value,protected:options&&Object.hasOwn(options,'protected')?options.protected:environmentNameIsSensitive(name)}),
436
+ remove:name=>invoke('environment.delete',{name})},
437
+ mail:{send:request=>invoke('mail.send',request??{},{timeoutMs:450000})},
438
+ speech:{status:()=>invoke('speech.status',{},{timeoutMs:10000}),
439
+ synthesize:request=>invoke('speech.synthesize',request??{},{timeoutMs:180000}),
440
+ transcribe:request=>invoke('speech.transcribe',request??{},{timeoutMs:180000})},
441
+ localAI:{status:()=>invoke('localai.status',{},{timeoutMs:15000}),
442
+ ensurePlatform:()=>invoke('localai.platform.ensure',{},long),recover:request=>invoke('localai.services.recover',request??{},long),
443
+ setParallelRequests:request=>invoke('localai.parallel.requests.set',request??{},long),
444
+ inspectIsolatedModel:request=>invoke('localai.isolated.inspect',request??{},{timeoutMs:45000}),
445
+ runIsolatedQuestion(request={},options={}){
446
+ const operationId=uuid();
447
+ const unsubscribe=typeof options?.onPhase==='function'?events.on('localai.isolated.phase',event=>{
448
+ if(event?.operationId===operationId)options.onPhase(event.phase,event);
449
+ }):()=>{};
450
+ return invoke('localai.isolated.question',{...request,operationId},long).finally(unsubscribe);
451
+ }},
452
+ ollama:{version:()=>invoke('ollama.version'),models:()=>invoke('ollama.models'),list:()=>invoke('ollama.models'),
453
+ running:()=>invoke('ollama.running'),show:(model,options={})=>invoke('ollama.show',{...options,model}),
454
+ generate:(request,options)=>ollamaInvoke('generate',request,options),chat:(request,options)=>ollamaInvoke('chat',request,options),
455
+ embed:request=>invoke('ollama.embed',request??{}),
456
+ pull:(model,options={},streamOptions)=>ollamaInvoke('pull',{...options,model},streamOptions),
457
+ push:(model,options={},streamOptions)=>ollamaInvoke('push',{...options,model},streamOptions),
458
+ create:(request,options)=>ollamaInvoke('create',request,options),
459
+ copy:(source,destination)=>invoke('ollama.copy',{source,destination},{timeoutMs:120000}),
460
+ delete:model=>invoke('ollama.delete',{model},{timeoutMs:120000}),selection:()=>invoke('ollama.selection.get'),
461
+ select:preference=>invoke('ollama.selection.set',{preference:preference??'auto'},long),settings:()=>invoke('ollama.settings.get'),
462
+ saveSettings:settings=>invoke('ollama.settings.set',settings??{},long),createBrain:definition=>invoke('ollama.brain.create',definition??{},long),
463
+ serviceSettings:()=>invoke('ollama.service.settings.get'),saveServiceSettings:settings=>invoke('ollama.service.settings.set',settings??{},long)},
464
+ app:{current:()=>invoke('app.current')},applications:{list:()=>invoke('apps.list'),launch:id=>invoke('apps.launch',{id})},
465
+ external:{open:uri=>invoke('external.open',{uri})},
466
+ terminal:{start:(options={})=>invoke('terminal.start',{shell:options?.shell??'auto',cwd:options?.cwd??'',columns:options?.columns??120,rows:options?.rows??32}),
467
+ list:()=>invoke('terminal.list'),write:(sessionId,data)=>invoke('terminal.write',{sessionId,data}),
468
+ resize:(sessionId,columns,rows)=>invoke('terminal.resize',{sessionId,columns,rows}),
469
+ signal:(sessionId,signal='interrupt')=>invoke('terminal.signal',{sessionId,signal}),close:sessionId=>invoke('terminal.close',{sessionId})},
470
+ capabilities:{list:()=>invoke('capabilities.list')},platform:{status:()=>invoke('platform.status')},permissions:{status:()=>invoke('permissions.status')},
471
+ version:{current:()=>invoke('version.current'),installation:()=>invoke('installation.status')},machine:{status:()=>invoke('machine.status')},user:{current:()=>invoke('user.current')},
472
+ requirements:{list:()=>invoke('requirements.list'),ensure:(requirementIds,options={})=>invoke('requirements.ensure',{
473
+ requirementIds,userProcessInterruption:options?.userProcessInterruption??'deny'
474
+ },long)},
475
+ installation:{status:()=>invoke('installation.status'),ensure:()=>invoke('installation.ensure',{},long),openUninstaller:()=>invoke('installation.uninstaller.open')},
476
+ users:{list:()=>invoke('users.list'),validate:usernames=>invoke('users.validate',{usernames:Array.isArray(usernames)?usernames:[usernames]}),
477
+ add:usernames=>invoke('users.add',{usernames:Array.isArray(usernames)?usernames:[usernames]},long),
478
+ activate:username=>invoke('users.activate',{username},long),resetPassword:username=>invoke('users.resetPassword',{username},long),
479
+ applyPassword:(username,temporaryPassword)=>invoke('users.applyPassword',{username,temporaryPassword},long),
480
+ verifyShell:username=>invoke('users.verifyShell',{username},long),restoreShell:username=>invoke('users.restoreShell',{username},long)},
481
+ system:{lock:()=>invoke('system.lock'),ping:()=>invoke('system.ping',{},{timeoutMs:10000}),metrics:()=>invoke('system.metrics'),
482
+ failurePolicy:()=>invoke('system.failurePolicy.get'),saveFailurePolicy:settings=>invoke('system.failurePolicy.set',settings??{})},
483
+ network:{status:()=>invoke('network.status')},filesystem:{selectDirectory:(options={})=>invoke('filesystem.directory.select',options,long)},
484
+ storage:{list:()=>invoke('storage.list'),get:key=>invoke('storage.get',{key}),set:(key,value)=>invoke('storage.set',{key,value}),delete:key=>invoke('storage.delete',{key})},
485
+ preferences:{list:()=>invoke('preferences.list'),get:key=>invoke('preferences.get',{key}),set:(key,value)=>invoke('preferences.set',{key,value}),
486
+ setMany:entries=>invoke('preferences.setMany',{entries}),delete:key=>invoke('preferences.delete',{key})},
487
+ appearance:{current:()=>invoke('appearance.current'),apply:appearance=>invoke('appearance.apply',appearance??{})},session:{logout:()=>invoke('session.logout')},
488
+ provisioning:{plan:usernames=>invoke('provisioning.plan',{usernames:Array.isArray(usernames)?usernames:[usernames].filter(Boolean)})},
489
+ diagnostics:{recentErrors:()=>invoke('diagnostics.recent'),get:diagnosticId=>invoke('diagnostics.get',{diagnosticId})},
490
+ development:{inspect:root=>invoke('development.inspect',{root}),context:(root,query)=>invoke('development.context',{root,query},{timeoutMs:130000}),
491
+ setup:(root,taskId)=>invoke('development.setup',{root,taskId},long),installNode:()=>invoke('development.node.install',{},long)}
492
+ };
493
+ }
494
+
495
+ /** Reads an installed SDK client without creating a transport or changing globals. */
496
+ export function getInstalledCoreClient(global=globalThis){
497
+ const client=global?.[CORE_CLIENT_KEY];
498
+ const installation=client?.[CORE_CLIENT_INSTALLATION];
499
+ return installation?.global===global&&installation.active?client:null;
500
+ }
501
+
502
+ /** Installs the same synchronous facade used by classic native-host scripts. */
503
+ export function installCoreClient(global=globalThis,options={}){
504
+ if(global[CORE_CLIENT_KEY])return global[CORE_CLIENT_KEY];
505
+ const client=createCoreClient({...options,global,autoConnect:false});
506
+ const facade=createCoreFacade(client);
507
+ const installation={global,active:true};
508
+ client[CORE_CLIENT_INSTALLATION]=installation;
509
+ const previousFacade=global.Arcane;
510
+ global.Arcane=facade;
511
+ global[CORE_CLIENT_KEY]=client;
512
+ const previous=global.__arcaneReceive;
513
+ global.__arcaneReceive=client.receive;
514
+ const previousFailure=global.__arcaneTransportFailed;
515
+ function nativeTransportFailed(error){return client.failTransport(error);}
516
+ global.__arcaneTransportFailed=nativeTransportFailed;
517
+ const close=client.close;
518
+ const failTransport=client.failTransport;
519
+ let installedTransportFailure=null;
520
+ function releaseInstalledClient(){
521
+ installation.active=false;
522
+ if(global[CORE_CLIENT_KEY]===client)delete global[CORE_CLIENT_KEY];
523
+ if(global.Arcane===facade){
524
+ if(previousFacade===undefined)delete global.Arcane;
525
+ else global.Arcane=previousFacade;
526
+ }
527
+ if(global.__arcaneReceive===client.receive){
528
+ if(previous===undefined)delete global.__arcaneReceive;
529
+ else global.__arcaneReceive=previous;
530
+ }
531
+ if(global.__arcaneTransportFailed===nativeTransportFailed){
532
+ if(previousFailure===undefined)delete global.__arcaneTransportFailed;
533
+ else global.__arcaneTransportFailed=previousFailure;
534
+ }
535
+ }
536
+ client.close=function closeInstalledClient(){
537
+ installation.active=false;
538
+ try{return close();}finally{releaseInstalledClient();}
539
+ };
540
+ client.failTransport=function failInstalledTransport(error){
541
+ if(installation.active)installedTransportFailure=error instanceof CoreError?error:new CoreError(error);
542
+ releaseInstalledClient();
543
+ return failTransport(installedTransportFailure??error);
544
+ };
545
+ try{client.connect();}catch(error){
546
+ if(error!==installedTransportFailure&&error.code!=='ARCANE_TRANSPORT_UNAVAILABLE'){
547
+ if(options.onError)options.onError(error);
548
+ else global.console?.error('Arcane Core transport initialization failed.',error);
549
+ }
550
+ }
551
+ return client;
552
+ }
@@ -0,0 +1,85 @@
1
+ export const CORE_PROTOCOL='arcane/1';
2
+ export const CORE_READY_EVENTS=['transport.ready','core.ready'];
3
+
4
+ /** Wire descriptions, not method admission or application policy. */
5
+ export const CORE_FRAME_CONTRACTS={
6
+ request:{type:'request',fields:['protocol','type','id','method','parameters','sentAt']},
7
+ response:{type:'response',fields:['protocol','type','id','ok'],success:['result'],failure:['error'],optional:['time']},
8
+ event:{type:'event',fields:['protocol','type','event','data'],optional:['time']},
9
+ cancel:{type:'control',control:'request.cancel',fields:['protocol','type','control','requestId']},
10
+ cancelAll:{type:'control',control:'requests.cancelAll',fields:['protocol','type','control']},
11
+ replay:{type:'control',control:'runtime.replay',fields:['protocol','type','control']}
12
+ };
13
+
14
+ /** These names describe existing methods; installed services determine availability. */
15
+ export const CORE_METHOD_CONTRACTS={
16
+ 'system.ping':{input:'empty-object-v1',output:'system-ping-result-v1',meaning:'Dispatcher response, not application or model readiness.'},
17
+ 'version.current':{input:'empty-object-v1',output:'version-string-v1',meaning:'Active host release version.'},
18
+ 'app.current':{input:'empty-object-v1',output:'application-descriptor-v1'},
19
+ 'capabilities.list':{input:'empty-object',output:'{app,grants,methods}'},
20
+ 'platform.status':{input:'empty-object-v1',output:'platform-status-v1'},
21
+ 'network.status':{input:'empty-object-v1',output:'network-status-v1',meaning:'A non-loopback interface is present; this is not proof of Internet reachability.'},
22
+ 'apps.list':{input:'empty-object-v1',output:'application-catalog-v1'},
23
+ 'apps.launch':{input:'application-launch-v1',output:'application-launch-result-v1',meaning:'Host acceptance, not application readiness.'},
24
+ 'external.open':{input:'external-open-v1',output:'external-open-result-v1',meaning:'Host acceptance, not confirmation of a user action.'},
25
+ 'filesystem.directory.select':{input:'directory-selection-options',output:'directory-selection-result'},
26
+ 'storage.list':{input:'empty-object',output:'{keys}'},
27
+ 'storage.get':{input:'{key}',output:'{key,found,value}'},
28
+ 'storage.set':{input:'{key,value}',output:'{key,value}'},
29
+ 'storage.delete':{input:'{key}',output:'{key,deleted}'},
30
+ 'preferences.list':{input:'empty-object',output:'{keys}'},
31
+ 'preferences.get':{input:'{key}',output:'{key,found,value}'},
32
+ 'preferences.set':{input:'{key,value}',output:'{key,value}'},
33
+ 'preferences.setMany':{input:'{entries}',output:'preference-batch-result'},
34
+ 'preferences.delete':{input:'{key}',output:'{key,deleted}'},
35
+ 'terminal.start':{input:'terminal-start-v1',output:'terminal-session-v1'},
36
+ 'terminal.list':{input:'empty-object-v1',output:'terminal-list-v1'},
37
+ 'terminal.write':{input:'terminal-write-v1',output:'terminal-write-result-v1'},
38
+ 'terminal.resize':{input:'terminal-resize-v1',output:'terminal-resize-result-v1'},
39
+ 'terminal.signal':{input:'terminal-signal-v1',output:'terminal-signal-result-v1'},
40
+ 'terminal.close':{input:'terminal-session-id-v1',output:'terminal-close-result-v1'},
41
+ 'environment.list':{input:'empty-object-v1',output:'environment-list-result-v1'},
42
+ 'environment.get':{input:'environment-name-v1',output:'environment-get-result-v1'},
43
+ 'environment.set':{input:'environment-set-v1',output:'environment-set-result-v1'},
44
+ 'environment.delete':{input:'environment-name-v1',output:'environment-delete-result-v1'},
45
+ 'mail.send':{input:'mail-send-v1',output:'mail-send-result-v1'},
46
+ 'user.current':{input:'empty-object-v1',output:'user-identity-v1'}
47
+ };
48
+
49
+ export class CoreError extends Error{
50
+ constructor(value){
51
+ const source=value!==null&&typeof value==='object'
52
+ ?value
53
+ :{message:value===undefined?'Arcane operation failed.':String(value)};
54
+ super(source.message??source.userMessage??'Arcane operation failed.');
55
+ for(const key of Object.getOwnPropertyNames(source))this[key]=source[key];
56
+ this.name=source.name??'ArcaneError';
57
+ this.code=source.code??'ARCANE_ERROR';
58
+ this.resolution=source.resolution??null;
59
+ this.diagnosticId=source.diagnosticId??null;
60
+ this.technicalMessage=source.technicalMessage??source.message??null;
61
+ const hresult=String(source.message??'').match(/\b0x[0-9a-f]{8}\b/iu);
62
+ this.hresult=source.hresult??hresult?.[0]?.toUpperCase()??null;
63
+ this.causeName=source.causeName??(source.name&&source.name!=='ArcaneError'?source.name:null);
64
+ if(source.stack!==undefined)this.stack=source.stack;
65
+ if(source.cause!==undefined)this.cause=source.cause;
66
+ }
67
+ }
68
+
69
+ export function serializeCoreError(error){
70
+ if(error===null||typeof error!=='object'){
71
+ return {name:'Error',code:'ARCANE_ERROR',message:String(error)};
72
+ }
73
+ const record=Object.fromEntries(Object.getOwnPropertyNames(error).map(key=>[key,error[key]]));
74
+ for(const key of ['name','message','stack','cause']){
75
+ if(error[key]!==undefined)record[key]=key==='cause'&&error[key] instanceof Error
76
+ ?serializeCoreError(error[key]):error[key];
77
+ }
78
+ if(Array.isArray(error.errors)){
79
+ record.errors=error.errors.map(value=>value instanceof Error?serializeCoreError(value):value);
80
+ }
81
+ if(record.name===undefined)record.name='Error';
82
+ if(record.code===undefined)record.code='ARCANE_ERROR';
83
+ if(record.message===undefined)record.message='Arcane operation failed.';
84
+ return record;
85
+ }