arcane-os 0.54.1 → 0.55.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,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.55.0
4
+
5
+ - Add public Core client, protocol contracts and classic-script source generation
6
+ through `arcane-os/core/client`, `core/contracts` and `core/classic-source`.
7
+ Native facades install synchronously; the shared event owner retains ordered
8
+ early events and responses. Correlation, complete payloads and diagnostics,
9
+ cancellation and transport-versus-service readiness remain explicit.
10
+ - Add app-neutral native Core runtime and stdio transport through
11
+ `arcane-os/core/runtime` and `core/stdio`. Hosts register services, start them
12
+ independently and distinguish cancellable renderer requests from accepted
13
+ service-lifetime work. Graceful shutdown waits for accepted responses, service
14
+ drain/disposal and queued transport output without terminating the host.
15
+ - Add SDK-owned portable native payload assembly and explicit application-service
16
+ composition through `arcane-os/native/portable-provider` and `core/host`.
17
+ Portable provider loading no longer requires an Arcane OS checkout; explicit
18
+ checkout providers remain supported. Preserve complete selected releases,
19
+ dependency records and the application's actual SDK version separately from
20
+ the builder, including partial direct browser package projections. Portable
21
+ output declares the host's serving, client injection and stdio responsibilities;
22
+ it is not an executable platform host.
23
+ - Preserve Core client FIFO order during deferred event-owner handoff, report
24
+ asynchronous send failures after request settlement, retain original native
25
+ diagnostic fields and require callable transport sending before readiness.
26
+ - Correct disabled-PWA packaging so selected output documents lose active SDK
27
+ manifest and bootstrap references before native-module processing. Preserve
28
+ source PWA settings, unrelated authored links, inactive HTML and unselected
29
+ documents; enabled PWA generation keeps its existing behavior.
30
+ - Executable platform hosts and application host cutover remain separate work.
31
+ Applications retain service composition, model selection and saved settings.
32
+ No new dependency, automatic model loading or data migration is added.
33
+
3
34
  ## 0.54.1
4
35
 
5
36
  - Select the self-contained Transformers.js 4.3.0 browser CDN bundle for
package/README.md CHANGED
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
20
20
  share the theme, packaging, event, cancellation,
21
21
  and browser run contracts while retaining their selected application layout.
22
22
 
23
- This checkout defines the `0.54.1` SDK contract. Applications pin one exact npm
23
+ This checkout defines the `0.55.0` SDK contract. Applications pin one exact npm
24
24
  version and lockfile; registry state is deliberately not baked into application
25
25
  artifacts.
26
26
 
@@ -594,10 +594,11 @@ Arcane root, shared, and native tests require the explicit shared focused-test
594
594
  form above. External app repositories retain their root `test/` plus selected
595
595
  app-test behavior.
596
596
 
597
- Integrated native app builds are also implemented. `--workspace` and
598
- `--arcane-root` must resolve to the same Arcane checkout, one `--app` and one
599
- declared native target are selected, and `--output-root` must resolve outside
600
- that checkout. The SDK then uses the same package, plan, provider, and
597
+ Integrated native app builds select one `--app` and one declared native target,
598
+ and `--output-root` must resolve outside that checkout. An explicit
599
+ `--arcane-root` must resolve to the same Arcane checkout as `--workspace`;
600
+ the default portable provider instead comes from the installed SDK package.
601
+ The SDK then uses the same package, plan, provider, and
601
602
  same-process run lifecycle as an external app repository. Verification remains
602
603
  an explicit operation for a selected release output.
603
604
 
@@ -617,8 +618,8 @@ arcane package [--app <id>] [--dry-run]
617
618
  arcane verify [--app <id>]
618
619
  arcane bundle [--app <id>] [--artifact <file>.arcane-app.tar.gz] [--overwrite]
619
620
  arcane verify-bundle <file.arcane-app.tar.gz>
620
- arcane native-doctor --target <native-target> --arcane-root <directory>
621
- arcane native-prepare --target <native-target> --arcane-root <directory>
621
+ arcane native-doctor --target <native-target> [--arcane-root <directory>]
622
+ arcane native-prepare --target <native-target> [--arcane-root <directory>]
622
623
  arcane build --target <target> [--arcane-root <directory>] [--output-root <directory>] [--format <format>] [--signing <mode>]
623
624
  arcane run [--target <target>] [--app <id>] [--arcane-root <directory>] [--output-root <directory>] [--format <format>] [--signing <mode>]
624
625
  arcane targets
@@ -689,24 +690,25 @@ package installation, or assertions.
689
690
 
690
691
  ## Current target support
691
692
 
692
- The SDK exposes one browser target and five explicitly paired
693
- native development targets: a non-runnable portable directory, a
694
- Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
693
+ The SDK exposes one browser target and an SDK-owned, non-runnable portable
694
+ payload directory. Four executable target entries require an explicit Arcane OS
695
+ provider: a Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
695
696
  unsigned-local-test DEBs, and an Android development-signed APK. The
696
697
  `android-arm64` APK is architecture-neutral because it contains no native ABI;
697
698
  the target name identifies its supported physical/native ARM64 run profile.
698
699
 
699
- Every native build requires an explicit Arcane OS checkout compatible with this
700
- SDK version and a canonical app descriptor that declares the
701
- exact selected target. The SDK never searches for or silently selects a mutable
702
- toolchain root. Scaffold a separate
700
+ Portable uses the installed SDK provider without an Arcane OS checkout.
701
+ An explicit `--arcane-root` retains the checkout provider selection; executable
702
+ targets still require that root and their platform toolchains. Every native
703
+ build needs a canonical app descriptor declaring the exact selected target.
704
+ The SDK never searches for a mutable toolchain root. Scaffold a separate
703
705
  repository with the matching `--target`, or add and validate that target in the
704
706
  canonical descriptor before invoking its native command. From an external
705
707
  application repository, run one selected target:
706
708
 
707
709
  ```bash
708
- npm exec -- arcane native-doctor --target portable --arcane-root "../Arcane OS"
709
- npm exec -- arcane build --target portable --arcane-root "../Arcane OS"
710
+ npm exec -- arcane native-doctor --target portable
711
+ npm exec -- arcane build --target portable
710
712
 
711
713
  # Windows x64: build or build, verify, and launch in one process
712
714
  npm exec -- arcane build --target windows-x64 --arcane-root "../Arcane OS"
@@ -737,15 +739,18 @@ browser package.
737
739
  `native-prepare` is available as a standalone toolchain diagnostic. Do not run
738
740
  it automatically: the build command prepares the selected toolchain itself.
739
741
 
740
- The build reads the selected app release and descriptor, requires the
741
- Core to meet the highest minimum declared by the SDK runtime, selected app, and
742
- bundled dependencies, keep their declared Arcane protocol, and provide every
743
- required feature, capability, and method. A Core reporting a higher version is
744
- accepted only when it passes those same current-SDK checks; this is not a promise
745
- that future SDK or Core releases will remain compatible. The build copies the
746
- complete selected release. Portable emits an app-scoped
747
- Arcane Core payload under `build/portable/`; that directory is not executable
748
- and `arcane run --target portable` is intentionally unavailable. Windows emits
742
+ The build passes the complete selected app release, descriptor, dependencies and
743
+ minimum Core requirement to the provider. The SDK portable provider copies those
744
+ payloads and generates a Core entry and synchronous classic client. Explicit
745
+ application services use `createPortableNativeProvider({services:[{module,options}]})`
746
+ from `arcane-os/native/portable-provider`; the default imports no OS product or
747
+ application services. `arcane-os/core/host` owns reusable native service
748
+ composition and shutdown. See [Core native packaging](docs/reference/core-native-packaging.md).
749
+ Payload assembly is separate from composed-host compatibility and execution.
750
+ The portable payload under `build/portable/` is not executable and
751
+ `arcane run --target portable` is intentionally unavailable. No executable host,
752
+ installer, model assets or platform build toolchain is added by this SDK
753
+ provider. Through explicit Arcane OS providers, Windows emits
749
754
  an app-scoped executable bundle under `build/windows-x64/`. Linux emits an
750
755
  app-scoped amd64 DEB under `build/linux-x64/` or ARM64 DEB under
751
756
  `build/linux-arm64/`. Android emits one development-signed, architecture-neutral
@@ -778,7 +783,7 @@ signing, store-publishing, release, or promotion evidence.
778
783
  | Integrated Arcane checkout workflow | Yes |
779
784
  | ArcaneOllama managed-service check | Yes on supported Windows hosts |
780
785
  | SDK native plan/provider boundary | Yes |
781
- | Portable app-scoped Core directory | Yes, with explicit `--arcane-root`; not directly runnable |
786
+ | Portable app-scoped payload and reusable Core entry | SDK provider by default; explicit checkout override retained; not directly runnable |
782
787
  | Windows x64 EXE bundle | Yes, unsigned local development with explicit `--arcane-root` |
783
788
  | Linux x64 DEB | Yes, unsigned local development with explicit `--arcane-root` |
784
789
  | Linux ARM64 DEB | Yes, unsigned local development on a compatible native ARM64 toolchain with explicit `--arcane-root`; native build, WebKit readiness, and cancellation evidence is recorded |
@@ -0,0 +1,491 @@
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_EVENT='core.rpc.event';
6
+ const LONG_OPERATION_TIMEOUT=50*60*1000;
7
+
8
+ /** One RPC client. Native execution and service policy belong to the host. */
9
+ export function createCoreClient({
10
+ global=globalThis,
11
+ transport:providedTransport=null,
12
+ eventOwner=arcaneEvents,
13
+ eventOwnerReady,
14
+ autoConnect=true,
15
+ onError=error=>global.console?.error('Arcane Core client failed.',error)
16
+ }={}){
17
+ const pending=new Map();
18
+ const acknowledgements=new Map();
19
+ const completedEvents=new Map();
20
+ const eventActions=[];
21
+ const cleanups=[];
22
+ const eventIdentity={};
23
+ let source=null;
24
+ let drainingEvents=false;
25
+ let transport=null;
26
+ let closed=false;
27
+ let eventOwnerFailure=null;
28
+
29
+ function report(error){
30
+ try{onError(error instanceof CoreError?error:new CoreError(error));}
31
+ catch(reportError){global.console?.error('Arcane Core error listener failed.',reportError,error);}
32
+ }
33
+ function uuid(){
34
+ return global.crypto?.randomUUID?.()
35
+ ??`req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
36
+ }
37
+ function attachEventOwner(owner){
38
+ if(closed)return;
39
+ source=owner.createSource(eventIdentity,{source:'core-client',eventTypes:[CORE_EVENT]});
40
+ // This is an ordered handoff queue, not an event dispatcher. The SDK
41
+ // event owner alone manages subscriptions and delivery.
42
+ drainingEvents=true;
43
+ try{
44
+ while(eventActions.length&&!closed)eventActions.shift()();
45
+ }finally{
46
+ drainingEvents=false;
47
+ }
48
+ }
49
+ function withEvents(action){
50
+ if(closed)return;
51
+ if(source&&!drainingEvents)action();
52
+ else eventActions.push(action);
53
+ }
54
+ if(eventOwner)attachEventOwner(eventOwner);
55
+ const eventsReady=eventOwner?Promise.resolve():Promise.resolve(eventOwnerReady).then(attachEventOwner);
56
+ eventsReady.catch(function failEventOwner(error){
57
+ eventOwnerFailure=error instanceof CoreError?error:new CoreError(error);
58
+ for(const id of pending.keys())settle(id,eventOwnerFailure);
59
+ report(eventOwnerFailure);
60
+ });
61
+
62
+ function emit(event,data){
63
+ withEvents(()=>source.dispatch(CORE_EVENT,{event,data}));
64
+ }
65
+ function complete(event,data){
66
+ if(completedEvents.has(event))return false;
67
+ completedEvents.set(event,data);
68
+ emit(event,data);
69
+ return true;
70
+ }
71
+ const events={
72
+ on(event,listener){
73
+ if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
74
+ let active=!closed;
75
+ let dispose=null;
76
+ withEvents(()=>{
77
+ if(!active)return;
78
+ dispose=source.on(CORE_EVENT,function receiveCoreEvent(occurrence){
79
+ if(!active)return;
80
+ const message=occurrence.detail;
81
+ if(event==='*')listener(message);
82
+ else if(event===message.event)listener(message.data);
83
+ });
84
+ });
85
+ return function unsubscribe(){
86
+ if(!active)return false;
87
+ active=false;
88
+ dispose?.();
89
+ return true;
90
+ };
91
+ },
92
+ once(event,listener){
93
+ if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
94
+ const unsubscribe=events.on(event,function receiveOnce(value){
95
+ unsubscribe();
96
+ listener(value);
97
+ });
98
+ return unsubscribe;
99
+ },
100
+ when(event,listener){
101
+ if(!CORE_READY_EVENTS.includes(event))throw new TypeError('Arcane completion event is not designated as durable.');
102
+ if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
103
+ if(!completedEvents.has(event))return events.once(event,listener);
104
+ const value=completedEvents.get(event);
105
+ let active=!closed;
106
+ Promise.resolve().then(function replayReadyEvent(){
107
+ if(!active||closed)return;
108
+ active=false;
109
+ try{listener(value);}catch(error){report(error);}
110
+ });
111
+ return function unsubscribe(){active=false;};
112
+ },
113
+ completed(event){return CORE_READY_EVENTS.includes(event)&&completedEvents.has(event);}
114
+ };
115
+
116
+ function settle(id,error,result){
117
+ const request=pending.get(id);
118
+ if(!request)return false;
119
+ pending.delete(id);
120
+ clearTimeout(request.timer);
121
+ request.signal?.removeEventListener('abort',request.abort);
122
+ if(error)request.reject(error);
123
+ else request.resolve(result);
124
+ return true;
125
+ }
126
+ function receive(input){
127
+ if(closed)return false;
128
+ let message=input;
129
+ if(typeof message==='string'){
130
+ try{message=JSON.parse(message);}
131
+ catch(error){report(new CoreError({code:'ARCANE_INVALID_NATIVE_JSON',message:error.message,input,cause:error}));return false;}
132
+ }
133
+ if(!message||message.protocol!==CORE_PROTOCOL)return false;
134
+ if(message.type==='event'){
135
+ const data=Object.hasOwn(message,'data')?message.data:{};
136
+ if(CORE_READY_EVENTS.includes(message.event))complete(message.event,data);
137
+ else emit(message.event,data);
138
+ return true;
139
+ }
140
+ if(message.type!=='response'||!pending.has(message.id))return false;
141
+ // Final responses follow earlier event deliveries, including while a
142
+ // classic host is connecting the shared SDK event owner. A stream's
143
+ // completion must not remove its subscription before its chunks arrive.
144
+ withEvents(()=>settle(message.id,message.ok?null:new CoreError(message.error),message.result));
145
+ return true;
146
+ }
147
+ function installCallback(name,value){
148
+ const previous=global[name];
149
+ global[name]=value;
150
+ cleanups.push(function restoreCallback(){
151
+ if(global[name]!==value)return;
152
+ if(previous===undefined)delete global[name];
153
+ else global[name]=previous;
154
+ });
155
+ }
156
+ function parseAcknowledgement(input,{required=false}={}){
157
+ let acknowledgement=input;
158
+ if(typeof acknowledgement==='string'){
159
+ try{acknowledgement=JSON.parse(acknowledgement);}catch{acknowledgement=null;}
160
+ }
161
+ if(acknowledgement?.accepted===false){
162
+ throw new CoreError(acknowledgement.error??{code:'BRIDGE_REJECTED',message:'The native host rejected the request.'});
163
+ }
164
+ if(required&&acknowledgement?.accepted!==true){
165
+ throw new CoreError({code:'NATIVE_BRIDGE_REPLY_INVALID',message:'The native host returned an invalid acknowledgement.'});
166
+ }
167
+ }
168
+ function acknowledge(token,value){
169
+ const record=acknowledgements.get(token);
170
+ if(!record)return false;
171
+ acknowledgements.delete(token);
172
+ clearTimeout(record.timer);
173
+ try{parseAcknowledgement(value,{required:true});record.resolve();}
174
+ catch(error){record.reject(error);}
175
+ return true;
176
+ }
177
+ function chooseTransport(){
178
+ const webview=global.chrome?.webview;
179
+ if(webview?.hostObjects){
180
+ const bridge=webview.hostObjects.arcaneBridge;
181
+ function nativeMessage(event){receive(event.data);}
182
+ webview.addEventListener('message',nativeMessage);
183
+ cleanups.push(()=>webview.removeEventListener?.('message',nativeMessage));
184
+ return {name:'webview2',async send(frame){
185
+ try{parseAcknowledgement(await bridge.Send(JSON.stringify(frame)));}
186
+ catch(error){
187
+ if(error instanceof CoreError)throw error;
188
+ const details=serializeCoreError(error);
189
+ if(error?.code===undefined)details.code='ARCANE_BRIDGE_CALL_FAILED';
190
+ throw new CoreError({
191
+ method:frame.method,transport:'webview2',...details
192
+ });
193
+ }
194
+ }};
195
+ }
196
+ const webkit=global.webkit?.messageHandlers?.arcane;
197
+ if(typeof global.__arcaneWebKitPostMessage==='function'||webkit){
198
+ installCallback('__arcaneReceive',receive);
199
+ installCallback('__arcaneWebKitAcknowledge',acknowledge);
200
+ return {name:'webkitgtk',async send(frame){
201
+ const serialized=JSON.stringify(frame);
202
+ if(typeof global.__arcaneWebKitPostMessage!=='function'){
203
+ parseAcknowledgement(await webkit.postMessage(serialized),{required:true});
204
+ return;
205
+ }
206
+ const token=uuid();
207
+ return new Promise(function sendWebKitFrame(resolve,reject){
208
+ const timer=setTimeout(function missingAcknowledgement(){
209
+ acknowledgements.delete(token);
210
+ reject(new CoreError({code:'NATIVE_BRIDGE_ACK_TIMEOUT',message:'The native host did not acknowledge the request.'}));
211
+ },30000);
212
+ acknowledgements.set(token,{resolve,reject,timer});
213
+ try{
214
+ if(global.__arcaneWebKitPostMessage(token,serialized)!==true){
215
+ throw new CoreError({code:'NATIVE_BRIDGE_UNTRUSTED_MAIN_FRAME',message:'The native host did not accept the request.'});
216
+ }
217
+ }catch(error){
218
+ acknowledgements.delete(token);
219
+ clearTimeout(timer);
220
+ reject(error instanceof CoreError?error:new CoreError(error));
221
+ }
222
+ });
223
+ }};
224
+ }
225
+ const android=global.arcaneAndroid;
226
+ if(typeof android?.postMessage==='function'){
227
+ const previous=android.onmessage;
228
+ function androidMessage(message){
229
+ receive(message&&typeof message==='object'&&'data' in message?message.data:message);
230
+ }
231
+ android.onmessage=androidMessage;
232
+ cleanups.push(()=>{if(android.onmessage===androidMessage)android.onmessage=previous;});
233
+ return {name:'android-webview',async send(frame){
234
+ try{await android.postMessage(JSON.stringify(frame));}
235
+ catch(error){
236
+ const details=serializeCoreError(error);
237
+ if(error?.code===undefined)details.code='ARCANE_ANDROID_BRIDGE_CALL_FAILED';
238
+ throw new CoreError({
239
+ method:frame.method,transport:'android-webview',...details
240
+ });
241
+ }
242
+ }};
243
+ }
244
+ if(global.__ARCANE_DEV_HTTP__){
245
+ if(typeof global.EventSource==='function'){
246
+ const stream=new global.EventSource('/events');
247
+ stream.onmessage=event=>receive(event.data);
248
+ stream.onerror=event=>report(new CoreError({code:'DEV_BRIDGE_EVENT_ERROR',message:'The development event connection reported an error.',event}));
249
+ cleanups.push(()=>stream.close());
250
+ }
251
+ return {name:'development-http',async send(frame){
252
+ const response=await global.fetch('/rpc',{
253
+ method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(frame)
254
+ });
255
+ const payload=await response.json();
256
+ if(!response.ok)throw new CoreError(payload?.error??{code:'DEV_BRIDGE_FAILED',message:'The development bridge failed.'});
257
+ if(payload?.type==='response')receive(payload);
258
+ }};
259
+ }
260
+ throw new CoreError({code:'ARCANE_TRANSPORT_UNAVAILABLE',message:'This interface is not connected to an Arcane native host.'});
261
+ }
262
+ function connect(){
263
+ if(closed)throw new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
264
+ if(transport)return transport;
265
+ const cleanupStart=cleanups.length;
266
+ try{
267
+ const selected=providedTransport??chooseTransport();
268
+ if(typeof selected?.send!=='function'){
269
+ throw new CoreError({code:'ARCANE_TRANSPORT_INVALID',message:'The Core transport must provide send(frame).'});
270
+ }
271
+ transport=selected;
272
+ if(typeof transport.subscribe==='function'){
273
+ const unsubscribe=transport.subscribe(receive);
274
+ if(typeof unsubscribe==='function')cleanups.push(unsubscribe);
275
+ }
276
+ }catch(error){
277
+ transport=null;
278
+ for(const cleanup of cleanups.splice(cleanupStart)){
279
+ try{cleanup();}catch(cleanupError){report(cleanupError);}
280
+ }
281
+ throw error;
282
+ }
283
+ complete('transport.ready',{protocol:CORE_PROTOCOL,transport:transport.name});
284
+ return transport;
285
+ }
286
+ function runtimeSnapshot(){
287
+ const name=transport?.name??providedTransport?.name
288
+ ??(global.chrome?.webview?.hostObjects?'webview2'
289
+ :typeof global.__arcaneWebKitPostMessage==='function'||global.webkit?.messageHandlers?.arcane?'webkitgtk'
290
+ :typeof global.arcaneAndroid?.postMessage==='function'?'android-webview'
291
+ :global.__ARCANE_DEV_HTTP__?'development-http':'standalone');
292
+ return {connected:transport!==null&&!closed,transport:name,
293
+ native:['webview2','webkitgtk','android-webview'].includes(name),
294
+ managedLocalAI:['webview2','webkitgtk'].includes(name)};
295
+ }
296
+ function sendControl(frame){
297
+ if(!transport)return;
298
+ try{Promise.resolve(transport.send(frame)).catch(report);}catch(error){report(error);}
299
+ }
300
+ function aborted(method){
301
+ return new CoreError({name:'AbortError',code:'ARCANE_REQUEST_ABORTED',message:'The Arcane request was cancelled.',method});
302
+ }
303
+ function cancel(id,error){
304
+ if(!settle(id,error))return false;
305
+ sendControl({protocol:CORE_PROTOCOL,type:'control',control:'request.cancel',requestId:id});
306
+ return true;
307
+ }
308
+ function cancelAll(){
309
+ if(!pending.size)return false;
310
+ for(const [id,request] of pending)settle(id,aborted(request.method));
311
+ sendControl({protocol:CORE_PROTOCOL,type:'control',control:'requests.cancelAll'});
312
+ return true;
313
+ }
314
+ function invoke(method,parameters={},options={}){
315
+ if(eventOwnerFailure)return Promise.reject(eventOwnerFailure);
316
+ let selected;
317
+ try{selected=connect();}catch(error){return Promise.reject(error);}
318
+ const {signal,timeoutMs=10*60*1000}=options??{};
319
+ if(signal&&(typeof signal.aborted!=='boolean'
320
+ ||typeof signal.addEventListener!=='function'
321
+ ||typeof signal.removeEventListener!=='function')){
322
+ return Promise.reject(new TypeError('Arcane request signal must be an AbortSignal.'));
323
+ }
324
+ if(signal?.aborted)return Promise.reject(aborted(method));
325
+ const id=uuid();
326
+ const frame={protocol:CORE_PROTOCOL,type:'request',id,method,parameters,sentAt:new Date().toISOString()};
327
+ const promise=new Promise(function ownRequest(resolve,reject){
328
+ const abort=()=>cancel(id,aborted(method));
329
+ const timer=timeoutMs>0?setTimeout(()=>cancel(id,new CoreError({
330
+ code:'ARCANE_REQUEST_TIMEOUT',message:'Arcane did not finish the operation before the request timed out.',method
331
+ })),timeoutMs):null;
332
+ pending.set(id,{resolve,reject,timer,signal,abort,method});
333
+ signal?.addEventListener('abort',abort,{once:true});
334
+ if(signal?.aborted)abort();
335
+ });
336
+ if(!pending.has(id))return promise;
337
+ function sendFailed(error){
338
+ const failure=error instanceof CoreError?error:new CoreError(error);
339
+ if(!settle(id,failure))report(failure);
340
+ }
341
+ try{
342
+ Promise.resolve(selected.send(frame)).catch(sendFailed);
343
+ }catch(error){sendFailed(error);}
344
+ return promise;
345
+ }
346
+ function close(){
347
+ if(closed)return false;
348
+ closed=true;
349
+ cancelAll();
350
+ for(const record of acknowledgements.values()){
351
+ clearTimeout(record.timer);
352
+ record.reject(new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'}));
353
+ }
354
+ acknowledgements.clear();
355
+ for(const cleanup of cleanups.splice(0)){
356
+ try{cleanup();}catch(error){report(error);}
357
+ }
358
+ source?.dispose();
359
+ // Registration closures and already-delivered payloads belong to this
360
+ // client lifetime and are released when the owner explicitly closes it.
361
+ eventActions.length=0;
362
+ completedEvents.clear();
363
+ return true;
364
+ }
365
+ if(typeof global.addEventListener==='function'){
366
+ global.addEventListener('pagehide',cancelAll,{capture:true});
367
+ cleanups.push(()=>global.removeEventListener('pagehide',cancelAll,{capture:true}));
368
+ }
369
+ if(autoConnect){
370
+ try{connect();}catch(error){if(error.code!=='ARCANE_TRANSPORT_UNAVAILABLE')report(error);}
371
+ }
372
+ return {protocol:CORE_PROTOCOL,Error:CoreError,invoke,receive,connect,close,cancelAll,events,eventsReady,runtime:{current:runtimeSnapshot},uuid};
373
+ }
374
+
375
+ /** Existing namespace call shapes; method implementations remain host-owned. */
376
+ export function createCoreFacade(client){
377
+ const {invoke,events,uuid}=client;
378
+ const long={timeoutMs:LONG_OPERATION_TIMEOUT};
379
+ function ollamaInvoke(operation,request={},options={}){
380
+ const onChunk=typeof options==='function'?options:options?.onChunk;
381
+ const timeoutMs=options?.timeoutMs??(['pull','push','create'].includes(operation)?LONG_OPERATION_TIMEOUT:10*60*1000);
382
+ if(typeof onChunk!=='function')return invoke(`ollama.${operation}`,request,{timeoutMs,signal:options?.signal});
383
+ const streamId=uuid();
384
+ const unsubscribe=events.on('ollama.chunk',function streamChunk(event){
385
+ if(event?.streamId===streamId)onChunk(event.chunk,{operation,streamId});
386
+ });
387
+ return invoke(`ollama.${operation}`,{...request,stream:true,streamId},{timeoutMs,signal:options?.signal}).finally(unsubscribe);
388
+ }
389
+ function environmentNameIsSensitive(name){
390
+ const canonical=String(name??'').toUpperCase();
391
+ return canonical.split(/[_.-]+/u).some(token=>[
392
+ 'AUTH','BEARER','CREDENTIAL','CREDENTIALS','KEY','KEYS','PASS','PASSWD','PASSWORD','PASSWORDS','PWD','SECRET','SECRETS','TOKEN','TOKENS'
393
+ ].includes(token))||['ACCESSKEY','ACCESSTOKEN','APIKEY','AUTHKEY','AUTHTOKEN','CLIENTSECRET','PRIVATEKEY'].some(suffix=>canonical.endsWith(suffix));
394
+ }
395
+ return {
396
+ protocol:CORE_PROTOCOL,Error:CoreError,runtime:client.runtime,events,
397
+ ai:{
398
+ models:()=>invoke('ai.models'),chat:request=>invoke('ai.chat',request??{},{timeoutMs:130000}),
399
+ profile:()=>invoke('ai.profile.current'),providerSettings:()=>invoke('ai.provider.settings.get'),
400
+ saveProviderSettings:settings=>invoke('ai.provider.settings.set',settings??{},{timeoutMs:130000}),
401
+ providerModels:()=>invoke('ai.provider.models',{},{timeoutMs:130000})
402
+ },
403
+ environment:{list:()=>invoke('environment.list'),get:name=>invoke('environment.get',{name}),
404
+ set:(name,value,options)=>invoke('environment.set',{name,value,protected:options&&Object.hasOwn(options,'protected')?options.protected:environmentNameIsSensitive(name)}),
405
+ remove:name=>invoke('environment.delete',{name})},
406
+ mail:{send:request=>invoke('mail.send',request??{},{timeoutMs:450000})},
407
+ speech:{status:()=>invoke('speech.status',{},{timeoutMs:10000}),
408
+ synthesize:request=>invoke('speech.synthesize',request??{},{timeoutMs:180000}),
409
+ transcribe:request=>invoke('speech.transcribe',request??{},{timeoutMs:180000})},
410
+ localAI:{status:()=>invoke('localai.status',{},{timeoutMs:15000}),
411
+ ensurePlatform:()=>invoke('localai.platform.ensure',{},long),recover:request=>invoke('localai.services.recover',request??{},long),
412
+ setParallelRequests:request=>invoke('localai.parallel.requests.set',request??{},long),
413
+ inspectIsolatedModel:request=>invoke('localai.isolated.inspect',request??{},{timeoutMs:45000}),
414
+ runIsolatedQuestion(request={},options={}){
415
+ const operationId=uuid();
416
+ const unsubscribe=typeof options?.onPhase==='function'?events.on('localai.isolated.phase',event=>{
417
+ if(event?.operationId===operationId)options.onPhase(event.phase,event);
418
+ }):()=>{};
419
+ return invoke('localai.isolated.question',{...request,operationId},long).finally(unsubscribe);
420
+ }},
421
+ ollama:{version:()=>invoke('ollama.version'),models:()=>invoke('ollama.models'),list:()=>invoke('ollama.models'),
422
+ running:()=>invoke('ollama.running'),show:(model,options={})=>invoke('ollama.show',{...options,model}),
423
+ generate:(request,options)=>ollamaInvoke('generate',request,options),chat:(request,options)=>ollamaInvoke('chat',request,options),
424
+ embed:request=>invoke('ollama.embed',request??{}),
425
+ pull:(model,options={},streamOptions)=>ollamaInvoke('pull',{...options,model},streamOptions),
426
+ push:(model,options={},streamOptions)=>ollamaInvoke('push',{...options,model},streamOptions),
427
+ create:(request,options)=>ollamaInvoke('create',request,options),
428
+ copy:(source,destination)=>invoke('ollama.copy',{source,destination},{timeoutMs:120000}),
429
+ delete:model=>invoke('ollama.delete',{model},{timeoutMs:120000}),selection:()=>invoke('ollama.selection.get'),
430
+ select:preference=>invoke('ollama.selection.set',{preference:preference??'auto'},long),settings:()=>invoke('ollama.settings.get'),
431
+ saveSettings:settings=>invoke('ollama.settings.set',settings??{},long),createBrain:definition=>invoke('ollama.brain.create',definition??{},long),
432
+ serviceSettings:()=>invoke('ollama.service.settings.get'),saveServiceSettings:settings=>invoke('ollama.service.settings.set',settings??{},long)},
433
+ app:{current:()=>invoke('app.current')},applications:{list:()=>invoke('apps.list'),launch:id=>invoke('apps.launch',{id})},
434
+ external:{open:uri=>invoke('external.open',{uri})},
435
+ terminal:{start:(options={})=>invoke('terminal.start',{shell:options?.shell??'auto',cwd:options?.cwd??'',columns:options?.columns??120,rows:options?.rows??32}),
436
+ list:()=>invoke('terminal.list'),write:(sessionId,data)=>invoke('terminal.write',{sessionId,data}),
437
+ resize:(sessionId,columns,rows)=>invoke('terminal.resize',{sessionId,columns,rows}),
438
+ signal:(sessionId,signal='interrupt')=>invoke('terminal.signal',{sessionId,signal}),close:sessionId=>invoke('terminal.close',{sessionId})},
439
+ capabilities:{list:()=>invoke('capabilities.list')},platform:{status:()=>invoke('platform.status')},permissions:{status:()=>invoke('permissions.status')},
440
+ version:{current:()=>invoke('version.current'),installation:()=>invoke('installation.status')},machine:{status:()=>invoke('machine.status')},user:{current:()=>invoke('user.current')},
441
+ requirements:{list:()=>invoke('requirements.list'),ensure:(requirementIds,options={})=>invoke('requirements.ensure',{
442
+ requirementIds,userProcessInterruption:options?.userProcessInterruption??'deny'
443
+ },long)},
444
+ installation:{status:()=>invoke('installation.status'),ensure:()=>invoke('installation.ensure',{},long),openUninstaller:()=>invoke('installation.uninstaller.open')},
445
+ users:{list:()=>invoke('users.list'),validate:usernames=>invoke('users.validate',{usernames:Array.isArray(usernames)?usernames:[usernames]}),
446
+ add:usernames=>invoke('users.add',{usernames:Array.isArray(usernames)?usernames:[usernames]},long),
447
+ activate:username=>invoke('users.activate',{username},long),resetPassword:username=>invoke('users.resetPassword',{username},long),
448
+ applyPassword:(username,temporaryPassword)=>invoke('users.applyPassword',{username,temporaryPassword},long),
449
+ verifyShell:username=>invoke('users.verifyShell',{username},long),restoreShell:username=>invoke('users.restoreShell',{username},long)},
450
+ system:{lock:()=>invoke('system.lock'),ping:()=>invoke('system.ping',{},{timeoutMs:10000}),metrics:()=>invoke('system.metrics'),
451
+ failurePolicy:()=>invoke('system.failurePolicy.get'),saveFailurePolicy:settings=>invoke('system.failurePolicy.set',settings??{})},
452
+ network:{status:()=>invoke('network.status')},filesystem:{selectDirectory:(options={})=>invoke('filesystem.directory.select',options,long)},
453
+ 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})},
454
+ preferences:{list:()=>invoke('preferences.list'),get:key=>invoke('preferences.get',{key}),set:(key,value)=>invoke('preferences.set',{key,value}),
455
+ setMany:entries=>invoke('preferences.setMany',{entries}),delete:key=>invoke('preferences.delete',{key})},
456
+ appearance:{current:()=>invoke('appearance.current'),apply:appearance=>invoke('appearance.apply',appearance??{})},session:{logout:()=>invoke('session.logout')},
457
+ provisioning:{plan:usernames=>invoke('provisioning.plan',{usernames:Array.isArray(usernames)?usernames:[usernames].filter(Boolean)})},
458
+ diagnostics:{recentErrors:()=>invoke('diagnostics.recent'),get:diagnosticId=>invoke('diagnostics.get',{diagnosticId})},
459
+ development:{inspect:root=>invoke('development.inspect',{root}),context:(root,query)=>invoke('development.context',{root,query},{timeoutMs:130000}),
460
+ setup:(root,taskId)=>invoke('development.setup',{root,taskId},long),installNode:()=>invoke('development.node.install',{},long)}
461
+ };
462
+ }
463
+
464
+ /** Installs the same synchronous facade used by classic native-host scripts. */
465
+ export function installCoreClient(global=globalThis,options={}){
466
+ if(global[CORE_CLIENT_KEY])return global[CORE_CLIENT_KEY];
467
+ const client=createCoreClient({...options,global,autoConnect:false});
468
+ const facade=createCoreFacade(client);
469
+ global.Arcane=facade;
470
+ global[CORE_CLIENT_KEY]=client;
471
+ const previous=global.__arcaneReceive;
472
+ global.__arcaneReceive=client.receive;
473
+ const close=client.close;
474
+ client.close=function closeInstalledClient(){
475
+ const changed=close();
476
+ if(global[CORE_CLIENT_KEY]===client)delete global[CORE_CLIENT_KEY];
477
+ if(global.Arcane===facade)delete global.Arcane;
478
+ if(global.__arcaneReceive===client.receive){
479
+ if(previous===undefined)delete global.__arcaneReceive;
480
+ else global.__arcaneReceive=previous;
481
+ }
482
+ return changed;
483
+ };
484
+ try{client.connect();}catch(error){
485
+ if(error.code!=='ARCANE_TRANSPORT_UNAVAILABLE'){
486
+ if(options.onError)options.onError(error);
487
+ else global.console?.error('Arcane Core transport initialization failed.',error);
488
+ }
489
+ }
490
+ return client;
491
+ }