arcane-os 0.1.0-dev.5

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 (248) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/COMMERCIAL-LICENSE.md +13 -0
  3. package/LICENSE +661 -0
  4. package/NOTICE +70 -0
  5. package/README.md +448 -0
  6. package/bin/arcane-test.mjs +741 -0
  7. package/bin/arcane.mjs +5 -0
  8. package/docs/architecture.md +234 -0
  9. package/docs/compatibility.md +36 -0
  10. package/docs/event-manager.md +166 -0
  11. package/docs/platform-targets.md +108 -0
  12. package/docs/publishing.md +203 -0
  13. package/docs/reference/README.md +117 -0
  14. package/docs/reference/arcane-ollama.md +288 -0
  15. package/docs/reference/availability-and-normalization.md +123 -0
  16. package/docs/reference/behavioral-testing.md +86 -0
  17. package/docs/reference/cli.md +569 -0
  18. package/docs/reference/core/README.md +62 -0
  19. package/docs/reference/core/arcane-ai-contracts.md +873 -0
  20. package/docs/reference/core/arcane-api.md +601 -0
  21. package/docs/reference/core/arcane-entities.md +65 -0
  22. package/docs/reference/core/arcane-events.md +134 -0
  23. package/docs/reference/core/ollama-module.md +181 -0
  24. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
  25. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
  26. package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
  27. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
  28. package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
  29. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
  30. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
  31. package/docs/reference/event-manager.md +957 -0
  32. package/docs/reference/inventory/package-api.json +2632 -0
  33. package/docs/reference/inventory/runtime-components.json +934 -0
  34. package/docs/reference/inventory/runtime-entities.json +26 -0
  35. package/docs/reference/inventory/runtime-modules.json +1249 -0
  36. package/docs/reference/protocols.md +242 -0
  37. package/docs/reference/runtime-components.md +1098 -0
  38. package/docs/reference/runtime-entities.md +303 -0
  39. package/docs/reference/runtime-modules.md +2010 -0
  40. package/docs/reference/sdk-api.md +4901 -0
  41. package/docs/roadmap.md +79 -0
  42. package/docs/work-amplification.md +124 -0
  43. package/node_modules/event-pubsub/CHANGELOG.md +55 -0
  44. package/node_modules/event-pubsub/MIGRATION.md +70 -0
  45. package/node_modules/event-pubsub/README.md +363 -0
  46. package/node_modules/event-pubsub/SECURITY.md +37 -0
  47. package/node_modules/event-pubsub/index.js +141 -0
  48. package/node_modules/event-pubsub/licence +21 -0
  49. package/node_modules/event-pubsub/package.json +59 -0
  50. package/node_modules/strong-type/README.md +408 -0
  51. package/node_modules/strong-type/assets/strong-type-header.png +0 -0
  52. package/node_modules/strong-type/index.js +1151 -0
  53. package/node_modules/strong-type/licence +21 -0
  54. package/node_modules/strong-type/node.js +125 -0
  55. package/node_modules/strong-type/package.json +61 -0
  56. package/package.json +95 -0
  57. package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
  58. package/runtime/arcane/components/app-bar.html +468 -0
  59. package/runtime/arcane/components/assistant-panel.html +715 -0
  60. package/runtime/arcane/components/calculator.html +8 -0
  61. package/runtime/arcane/components/chart.html +655 -0
  62. package/runtime/arcane/components/chat.html +1225 -0
  63. package/runtime/arcane/components/conversation-view.html +13 -0
  64. package/runtime/arcane/components/dashboard-config.html +341 -0
  65. package/runtime/arcane/components/data-maintenance.html +112 -0
  66. package/runtime/arcane/components/data-view.html +92 -0
  67. package/runtime/arcane/components/directory-picker.html +197 -0
  68. package/runtime/arcane/components/document-inspector.html +252 -0
  69. package/runtime/arcane/components/file-drop.html +264 -0
  70. package/runtime/arcane/components/file-inspector.html +293 -0
  71. package/runtime/arcane/components/file-manager.html +1715 -0
  72. package/runtime/arcane/components/header.html +142 -0
  73. package/runtime/arcane/components/integration-settings.html +14 -0
  74. package/runtime/arcane/components/local-ai-status.html +360 -0
  75. package/runtime/arcane/components/markdown-document.html +1048 -0
  76. package/runtime/arcane/components/markdown-editor.html +360 -0
  77. package/runtime/arcane/components/media-embed.html +8 -0
  78. package/runtime/arcane/components/modal.html +402 -0
  79. package/runtime/arcane/components/output-panel.html +259 -0
  80. package/runtime/arcane/components/preferences-form.html +135 -0
  81. package/runtime/arcane/components/record-timeline.html +105 -0
  82. package/runtime/arcane/components/relationship-board.html +116 -0
  83. package/runtime/arcane/components/screen-capture.html +8 -0
  84. package/runtime/arcane/components/source-code-viewer.html +441 -0
  85. package/runtime/arcane/components/source-explanation.html +124 -0
  86. package/runtime/arcane/components/speech.html +365 -0
  87. package/runtime/arcane/components/summary-strip.html +177 -0
  88. package/runtime/arcane/components/table.html +77 -0
  89. package/runtime/arcane/components/task-progress.html +282 -0
  90. package/runtime/arcane/components/terminal-workspace.html +65 -0
  91. package/runtime/arcane/components/theme-editor.html +41 -0
  92. package/runtime/arcane/components/theme-switcher.html +46 -0
  93. package/runtime/arcane/components/unified-inbox.html +20 -0
  94. package/runtime/arcane/components/voice-transcription.html +476 -0
  95. package/runtime/arcane/components/weather-widget.html +8 -0
  96. package/runtime/arcane/components/web-navigator.html +239 -0
  97. package/runtime/arcane/css/communications.css +1 -0
  98. package/runtime/arcane/css/dashboard-config.css +45 -0
  99. package/runtime/arcane/css/document-site.css +981 -0
  100. package/runtime/arcane/css/layout.css +438 -0
  101. package/runtime/arcane/css/primitives.css +321 -0
  102. package/runtime/arcane/css/theme.css +112 -0
  103. package/runtime/arcane/css/utility-workspace.css +1 -0
  104. package/runtime/arcane/entities/ApiModelRecord.js +20 -0
  105. package/runtime/arcane/entities/Calculation.js +13 -0
  106. package/runtime/arcane/entities/Chat.js +581 -0
  107. package/runtime/arcane/entities/CommunicationMessage.js +29 -0
  108. package/runtime/arcane/entities/CommunicationThread.js +21 -0
  109. package/runtime/arcane/entities/Document.js +10 -0
  110. package/runtime/arcane/entities/File.js +143 -0
  111. package/runtime/arcane/entities/Image.js +135 -0
  112. package/runtime/arcane/entities/IntentEnvelope.js +834 -0
  113. package/runtime/arcane/entities/Preference.js +83 -0
  114. package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
  115. package/runtime/arcane/entities/TerminalSession.js +46 -0
  116. package/runtime/arcane/entities/Theme.js +107 -0
  117. package/runtime/arcane/entities/User.js +1046 -0
  118. package/runtime/arcane/entities/Weather.js +23 -0
  119. package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
  120. package/runtime/arcane/img/arrow-left.png +0 -0
  121. package/runtime/arcane/img/arrow-right.png +0 -0
  122. package/runtime/arcane/img/doc.svg +5 -0
  123. package/runtime/arcane/img/folder.svg +4 -0
  124. package/runtime/arcane/img/image.svg +5 -0
  125. package/runtime/arcane/img/refresh.png +0 -0
  126. package/runtime/arcane/img/send.svg +9 -0
  127. package/runtime/arcane/img/trash.svg +5 -0
  128. package/runtime/arcane/img/upload.svg +5 -0
  129. package/runtime/arcane/modules/AI.js +2048 -0
  130. package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
  131. package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
  132. package/runtime/arcane/modules/AIResponseLength.js +42 -0
  133. package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
  134. package/runtime/arcane/modules/AnsiText.js +53 -0
  135. package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
  136. package/runtime/arcane/modules/AppDataScope.js +245 -0
  137. package/runtime/arcane/modules/AppearancePreferences.js +28 -0
  138. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
  139. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
  140. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
  141. package/runtime/arcane/modules/AsyncBoundary.js +161 -0
  142. package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
  143. package/runtime/arcane/modules/CalculatorEngine.js +20 -0
  144. package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
  145. package/runtime/arcane/modules/ChartLibrary.js +35 -0
  146. package/runtime/arcane/modules/ChatRecords.js +13 -0
  147. package/runtime/arcane/modules/CommunicationAppController.js +43 -0
  148. package/runtime/arcane/modules/CommunicationHub.js +16 -0
  149. package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
  150. package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
  151. package/runtime/arcane/modules/ComponentContracts.js +586 -0
  152. package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
  153. package/runtime/arcane/modules/ConversationActionItems.js +488 -0
  154. package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
  155. package/runtime/arcane/modules/ConversationTimebox.js +527 -0
  156. package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
  157. package/runtime/arcane/modules/DBLS.js +171 -0
  158. package/runtime/arcane/modules/DBOPFS.js +1154 -0
  159. package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
  160. package/runtime/arcane/modules/DataMaintenance.js +91 -0
  161. package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
  162. package/runtime/arcane/modules/DirectoryPicker.js +109 -0
  163. package/runtime/arcane/modules/DocumentNavigation.js +223 -0
  164. package/runtime/arcane/modules/Errors.js +1025 -0
  165. package/runtime/arcane/modules/GifEncoder.js +29 -0
  166. package/runtime/arcane/modules/HTMLImport.js +114 -0
  167. package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
  168. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
  169. package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
  170. package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
  171. package/runtime/arcane/modules/MD.js +111 -0
  172. package/runtime/arcane/modules/Mail.js +352 -0
  173. package/runtime/arcane/modules/MailTransport.mjs +180 -0
  174. package/runtime/arcane/modules/Marked.min.js +71 -0
  175. package/runtime/arcane/modules/MemoryRecords.js +44 -0
  176. package/runtime/arcane/modules/MessageAdvisory.js +38 -0
  177. package/runtime/arcane/modules/ModelDefinition.js +189 -0
  178. package/runtime/arcane/modules/Ollama.js +74 -0
  179. package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
  180. package/runtime/arcane/modules/OllamaSettings.js +24 -0
  181. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
  182. package/runtime/arcane/modules/PreferenceStore.js +109 -0
  183. package/runtime/arcane/modules/QRCode.min.js +1 -0
  184. package/runtime/arcane/modules/Questionnaire.js +61 -0
  185. package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
  186. package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
  187. package/runtime/arcane/modules/RecordReviewStore.js +94 -0
  188. package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
  189. package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
  190. package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
  191. package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
  192. package/runtime/arcane/modules/ScreenCapture.js +20 -0
  193. package/runtime/arcane/modules/SpeechPlayback.js +581 -0
  194. package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
  195. package/runtime/arcane/modules/SystemAppearance.js +20 -0
  196. package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
  197. package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
  198. package/runtime/arcane/modules/TerminalClient.js +52 -0
  199. package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
  200. package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
  201. package/runtime/arcane/modules/ThemeManager.js +131 -0
  202. package/runtime/arcane/modules/TimeGuard.js +149 -0
  203. package/runtime/arcane/modules/ToolCallRouter.js +83 -0
  204. package/runtime/arcane/modules/WaitForComponent.js +102 -0
  205. package/runtime/arcane/modules/YouTubeMedia.js +16 -0
  206. package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
  207. package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
  208. package/runtime/arcane/modules/uPlot.min.css +1 -0
  209. package/runtime/arcane/security/arcane-network-policy.json +6 -0
  210. package/runtime/strong-type/index.js +352 -0
  211. package/runtime/strong-type/licence +21 -0
  212. package/runtime/strong-type/package.json +45 -0
  213. package/schemas/arcane-app-bundle.schema.json +125 -0
  214. package/schemas/arcane-app.schema.json +329 -0
  215. package/schemas/arcane-lock.schema.json +86 -0
  216. package/schemas/arcane-package.schema.json +224 -0
  217. package/schemas/cli-event.schema.json +122 -0
  218. package/schemas/event-stack.schema.json +152 -0
  219. package/schemas/native-build-plan.schema.json +176 -0
  220. package/schemas/target-adapter.schema.json +117 -0
  221. package/src/app-descriptor.mjs +500 -0
  222. package/src/cli/main.mjs +561 -0
  223. package/src/constants.mjs +31 -0
  224. package/src/dev-server.mjs +718 -0
  225. package/src/doctor.mjs +315 -0
  226. package/src/dom-event-instrumentation.mjs +594 -0
  227. package/src/errors.mjs +75 -0
  228. package/src/event-manager.mjs +1342 -0
  229. package/src/event-queue.mjs +138 -0
  230. package/src/events.mjs +219 -0
  231. package/src/index.mjs +177 -0
  232. package/src/integrated-provider-loader.mjs +432 -0
  233. package/src/native-plan.mjs +698 -0
  234. package/src/native-provider-loader.mjs +1126 -0
  235. package/src/packager/core.mjs +2691 -0
  236. package/src/process.mjs +353 -0
  237. package/src/release-bundle.mjs +2523 -0
  238. package/src/repository.mjs +90 -0
  239. package/src/runtime.mjs +452 -0
  240. package/src/scaffold.mjs +380 -0
  241. package/src/targets/index.mjs +436 -0
  242. package/src/templates/assets/app-icon.png +0 -0
  243. package/src/templates/workspace-template.mjs +388 -0
  244. package/src/testing-loader.mjs +9 -0
  245. package/src/testing.mjs +427 -0
  246. package/src/toolchain.mjs +1335 -0
  247. package/src/update-check.mjs +307 -0
  248. package/src/workspace.mjs +449 -0
@@ -0,0 +1,870 @@
1
+ import {AI_PREFERENCE_SLOT_KEYS} from './AIPreferenceTuple.js';
2
+ import {
3
+ getCoreLocalModelCatalog,
4
+ isUserManagedLoopbackLocalAIStatus,
5
+ USER_MANAGED_LOOPBACK_PROVIDER_MODE
6
+ } from './CoreLocalModelCatalog.js';
7
+ import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
8
+
9
+ export const LOCAL_AI_BROWSER_ENDPOINTS=Object.freeze({
10
+ speech:'http://127.0.0.1:8011/health'
11
+ });
12
+
13
+ const DEFAULT_TIMEOUT_MS=3000;
14
+ const MAX_TIMEOUT_MS=10000;
15
+ const MAX_HEALTH_RESPONSE_CHARACTERS=64*1024;
16
+ const LOCAL_SPEECH_PROVIDERS=new Set([
17
+ 'LOCAL',
18
+ 'LOCAL_SPEACH',
19
+ 'LOCAL_SPEECH'
20
+ ]);
21
+
22
+ function token(value,{uppercase=false}={}){
23
+ const normalized=typeof value==='string'
24
+ ?value.trim()
25
+ :'';
26
+
27
+ return uppercase
28
+ ?normalized.toUpperCase()
29
+ :normalized;
30
+ }
31
+
32
+ function errorCode(error,fallback){
33
+ const code=token(error?.code,{uppercase:true});
34
+ return code&&code.length<=128
35
+ ?code
36
+ :fallback;
37
+ }
38
+
39
+ function freeze(value){
40
+ if(!value||typeof value!=='object'||Object.isFrozen(value)){
41
+ return value;
42
+ }
43
+
44
+ for(const child of Object.values(value)){
45
+ freeze(child);
46
+ }
47
+
48
+ return Object.freeze(value);
49
+ }
50
+
51
+ function positiveSafeInteger(value){
52
+ return Number.isSafeInteger(value)&&value>0
53
+ ?value
54
+ :null;
55
+ }
56
+
57
+ function preferenceSlot(preferences,index){
58
+ const value=preferences[index];
59
+
60
+ if(typeof value!=='string'||!value.trim()){
61
+ throw new TypeError(
62
+ 'The AI preference for '
63
+ +AI_PREFERENCE_SLOT_KEYS[index]
64
+ +' must be a non-empty string.'
65
+ );
66
+ }
67
+
68
+ return value.trim();
69
+ }
70
+
71
+ /**
72
+ * Maps Arcane's six-slot AI preference tuple onto the local services that the
73
+ * selected provider/model pairs require. This function does not alter the
74
+ * tuple or choose a replacement provider.
75
+ */
76
+ export function deriveLocalAIRequirements(preferences){
77
+ if(!Array.isArray(preferences)||preferences.length!==AI_PREFERENCE_SLOT_KEYS.length){
78
+ throw new TypeError(
79
+ 'AI preferences must contain exactly '
80
+ +AI_PREFERENCE_SLOT_KEYS.length
81
+ +' slots.'
82
+ );
83
+ }
84
+
85
+ const llmProvider=preferenceSlot(preferences,0);
86
+ const sttProvider=preferenceSlot(preferences,1);
87
+ const ttsProvider=preferenceSlot(preferences,2);
88
+ const llmModel=preferenceSlot(preferences,3);
89
+ const ttsModel=preferenceSlot(preferences,4);
90
+ const sttModel=preferenceSlot(preferences,5);
91
+
92
+ return freeze({
93
+ llm:{
94
+ slot:'llm',
95
+ provider:llmProvider,
96
+ model:llmModel,
97
+ required:token(llmProvider,{uppercase:true})==='OLLAMA',
98
+ service:'ollama'
99
+ },
100
+ stt:{
101
+ slot:'stt',
102
+ provider:sttProvider,
103
+ model:sttModel,
104
+ required:LOCAL_SPEECH_PROVIDERS.has(
105
+ token(sttProvider,{uppercase:true})
106
+ ),
107
+ service:'speech'
108
+ },
109
+ tts:{
110
+ slot:'tts',
111
+ provider:ttsProvider,
112
+ model:ttsModel,
113
+ required:LOCAL_SPEECH_PROVIDERS.has(
114
+ token(ttsProvider,{uppercase:true})
115
+ ),
116
+ service:'speech'
117
+ }
118
+ });
119
+ }
120
+
121
+ function explicitHealthBoolean(status,names){
122
+ for(const name of names){
123
+ if(Object.prototype.hasOwnProperty.call(status,name)){
124
+ return status[name]===true;
125
+ }
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ function healthBoolean(status,names){
132
+ return explicitHealthBoolean(status,names)??status.ready===true;
133
+ }
134
+
135
+ function healthText(status,names){
136
+ for(const name of names){
137
+ const value=token(status[name]);
138
+
139
+ if(value){
140
+ return value.slice(0,128);
141
+ }
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Evaluates STT and TTS independently. Raw browser health responses use
149
+ * snake_case fields; Arcane Core responses may use camelCase or their bounded
150
+ * aggregate ready proof.
151
+ */
152
+ export function evaluateLocalSpeechHealth(status){
153
+ if(!status||typeof status!=='object'||Array.isArray(status)){
154
+ return freeze({
155
+ reachable:false,
156
+ ready:false,
157
+ status:'unavailable',
158
+ ttsEngine:null,
159
+ sttEngine:null,
160
+ stt:{
161
+ ready:false,
162
+ ffmpeg:false,
163
+ whisperCli:false,
164
+ whisperServer:false,
165
+ whisperModel:false
166
+ },
167
+ tts:{
168
+ ready:false,
169
+ kokoro:false,
170
+ voices:false
171
+ }
172
+ });
173
+ }
174
+
175
+ const serviceStatus=healthText(status,['status'])||'unknown';
176
+ const statusReady=serviceStatus.toLowerCase()==='ok'
177
+ ||(serviceStatus==='unknown'&&status.ready===true);
178
+ const ttsEngine=healthText(status,['ttsEngine','tts_engine']);
179
+ const sttEngine=healthText(status,['sttEngine','stt_engine']);
180
+ const ffmpeg=healthBoolean(status,['ffmpeg','ffmpegExists','ffmpeg_exists']);
181
+ const whisperCli=healthBoolean(
182
+ status,
183
+ ['whisperCli','whisperCliExists','whisper_cli_exists']
184
+ );
185
+ const whisperServer=healthBoolean(
186
+ status,
187
+ ['whisperServer','whisperServerExists','whisper_server_exists']
188
+ );
189
+ const whisperModel=healthBoolean(
190
+ status,
191
+ ['whisperModel','whisperExists','whisper_exists']
192
+ );
193
+ const kokoro=healthBoolean(
194
+ status,
195
+ ['kokoro','kokoroExists','kokoro_exists']
196
+ );
197
+ const voices=healthBoolean(
198
+ status,
199
+ ['voices','voicesExists','voices_exists']
200
+ );
201
+ const transcriptionAvailable=explicitHealthBoolean(
202
+ status,
203
+ ['transcriptionAvailable','transcription_available']
204
+ );
205
+ const synthesisAvailable=explicitHealthBoolean(
206
+ status,
207
+ ['synthesisAvailable','synthesis_available']
208
+ );
209
+ const sttReady=statusReady
210
+ &&sttEngine?.toLowerCase()==='whisper.cpp'
211
+ &&(transcriptionAvailable??(
212
+ ffmpeg
213
+ &&whisperCli
214
+ &&whisperServer
215
+ &&whisperModel
216
+ ));
217
+ const ttsReady=statusReady
218
+ &&ttsEngine?.toLowerCase()==='kokoro-onnx'
219
+ &&(synthesisAvailable??(kokoro&&voices));
220
+
221
+ return freeze({
222
+ reachable:true,
223
+ ready:sttReady&&ttsReady,
224
+ status:serviceStatus,
225
+ ttsEngine,
226
+ sttEngine,
227
+ stt:{
228
+ ready:Boolean(sttReady),
229
+ ffmpeg,
230
+ whisperCli,
231
+ whisperServer,
232
+ whisperModel
233
+ },
234
+ tts:{
235
+ ready:Boolean(ttsReady),
236
+ kokoro,
237
+ voices
238
+ }
239
+ });
240
+ }
241
+
242
+ function normalizedTimeout(value){
243
+ if(value===undefined){
244
+ return DEFAULT_TIMEOUT_MS;
245
+ }
246
+
247
+ if(!Number.isSafeInteger(value)||value<1||value>MAX_TIMEOUT_MS){
248
+ throw new RangeError(
249
+ 'Local AI readiness timeout must be between 1 and '
250
+ +MAX_TIMEOUT_MS
251
+ +' milliseconds.'
252
+ );
253
+ }
254
+
255
+ return value;
256
+ }
257
+
258
+ async function boundedFetchJSON(fetchImpl,url,timeoutMs){
259
+ if(typeof fetchImpl!=='function'){
260
+ const error=new Error('Browser fetch is unavailable.');
261
+ error.code='BROWSER_FETCH_UNAVAILABLE';
262
+ throw error;
263
+ }
264
+
265
+ const controller=typeof AbortController==='function'
266
+ ?new AbortController()
267
+ :null;
268
+ let timer;
269
+ const timeout=new Promise((_resolve,reject)=>{
270
+ timer=setTimeout(()=>{
271
+ controller?.abort();
272
+ const error=new Error('The local readiness request timed out.');
273
+ error.code='LOCAL_AI_READINESS_TIMEOUT';
274
+ reject(error);
275
+ },timeoutMs);
276
+ });
277
+ const request=(async()=>{
278
+ const response=await fetchImpl(
279
+ url,
280
+ {
281
+ method:'GET',
282
+ credentials:'omit',
283
+ cache:'no-store',
284
+ ...(controller?{signal:controller.signal}:{})
285
+ }
286
+ );
287
+
288
+ if(!response||response.ok!==true){
289
+ const error=new Error('The local readiness endpoint was not ready.');
290
+ error.code='LOCAL_AI_HEALTH_HTTP_ERROR';
291
+ throw error;
292
+ }
293
+
294
+ if(typeof response.text!=='function'){
295
+ const error=new Error('The local readiness endpoint returned an invalid response.');
296
+ error.code='LOCAL_AI_HEALTH_INVALID_RESPONSE';
297
+ throw error;
298
+ }
299
+
300
+ const text=await response.text();
301
+
302
+ if(
303
+ typeof text!=='string'
304
+ ||!text
305
+ ||text.length>MAX_HEALTH_RESPONSE_CHARACTERS
306
+ ){
307
+ const error=new Error('The local readiness response was outside Arcane limits.');
308
+ error.code='LOCAL_AI_HEALTH_INVALID_RESPONSE';
309
+ throw error;
310
+ }
311
+
312
+ let parsed;
313
+
314
+ try{
315
+ parsed=JSON.parse(text);
316
+ }catch{
317
+ const error=new Error('The local readiness endpoint returned invalid JSON.');
318
+ error.code='LOCAL_AI_HEALTH_INVALID_RESPONSE';
319
+ throw error;
320
+ }
321
+
322
+ if(!parsed||typeof parsed!=='object'||Array.isArray(parsed)){
323
+ const error=new Error('The local readiness endpoint returned an invalid record.');
324
+ error.code='LOCAL_AI_HEALTH_INVALID_RESPONSE';
325
+ throw error;
326
+ }
327
+
328
+ return parsed;
329
+ })();
330
+
331
+ try{
332
+ return await Promise.race([request,timeout]);
333
+ }finally{
334
+ clearTimeout(timer);
335
+ }
336
+ }
337
+
338
+ async function probeNativeOllama(arcane,selectedModel){
339
+ if(typeof arcane?.localAI?.status!=='function'){
340
+ return freeze({
341
+ ready:false,
342
+ model:null,
343
+ errorCode:'ARCANE_LOCAL_AI_STATUS_UNAVAILABLE'
344
+ });
345
+ }
346
+
347
+ const model=normalizeOllamaModelIdentifier(selectedModel);
348
+
349
+ if(!model){
350
+ return freeze({
351
+ ready:false,
352
+ model:null,
353
+ errorCode:'OLLAMA_MODEL_INVALID'
354
+ });
355
+ }
356
+
357
+ try{
358
+ const status=await arcane.localAI.status();
359
+ const activeParallelRequests=positiveSafeInteger(
360
+ status?.ollama?.activeParallelRequests
361
+ );
362
+
363
+ if(status?.ollama?.available===false){
364
+ return freeze({
365
+ ready:false,
366
+ model,
367
+ ...(activeParallelRequests===null
368
+ ?{}
369
+ :{activeParallelRequests}),
370
+ errorCode:errorCode(
371
+ {code:status.ollama.errorCode},
372
+ 'OLLAMA_UNAVAILABLE'
373
+ )
374
+ });
375
+ }
376
+
377
+ const catalog=getCoreLocalModelCatalog(status);
378
+ const available=catalog.some(function matchesSelectedModel(entry){
379
+ return entry.modelId===model;
380
+ });
381
+
382
+ return freeze({
383
+ ready:available,
384
+ model,
385
+ ...(activeParallelRequests===null
386
+ ?{}
387
+ :{activeParallelRequests}),
388
+ errorCode:available?null:'OLLAMA_MODEL_UNAVAILABLE'
389
+ });
390
+ }catch(error){
391
+ return freeze({
392
+ ready:false,
393
+ model,
394
+ errorCode:errorCode(error,'ARCANE_LOCAL_AI_STATUS_INVALID')
395
+ });
396
+ }
397
+ }
398
+
399
+ async function probeUserManagedLoopbackOllama(arcane,selectedModel){
400
+ if(typeof arcane?.localAI?.status!=='function'){
401
+ return freeze({
402
+ ready:false,
403
+ model:null,
404
+ providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
405
+ managed:false,
406
+ errorCode:'ARCANE_LOCAL_AI_STATUS_UNAVAILABLE'
407
+ });
408
+ }
409
+
410
+ const model=normalizeOllamaModelIdentifier(selectedModel);
411
+
412
+ if(!model){
413
+ return freeze({
414
+ ready:false,
415
+ model:null,
416
+ providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
417
+ managed:false,
418
+ errorCode:'OLLAMA_MODEL_INVALID'
419
+ });
420
+ }
421
+
422
+ try{
423
+ const status=await arcane.localAI.status();
424
+
425
+ if(!isUserManagedLoopbackLocalAIStatus(status)){
426
+ return freeze({
427
+ ready:false,
428
+ model,
429
+ providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
430
+ managed:false,
431
+ errorCode:'ANDROID_USER_MANAGED_OLLAMA_UNAVAILABLE'
432
+ });
433
+ }
434
+
435
+ if(status?.ollama?.available!==true){
436
+ return freeze({
437
+ ready:false,
438
+ model,
439
+ providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
440
+ managed:false,
441
+ errorCode:errorCode(
442
+ {code:status?.ollama?.errorCode},
443
+ 'OLLAMA_UNAVAILABLE'
444
+ )
445
+ });
446
+ }
447
+
448
+ const catalog=getCoreLocalModelCatalog(status);
449
+ const available=catalog.some(function matchesSelectedModel(entry){
450
+ return entry.modelId===model;
451
+ });
452
+
453
+ return freeze({
454
+ ready:available,
455
+ model,
456
+ providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
457
+ managed:false,
458
+ errorCode:available?null:'OLLAMA_MODEL_UNAVAILABLE'
459
+ });
460
+ }catch(error){
461
+ return freeze({
462
+ ready:false,
463
+ model,
464
+ providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
465
+ managed:false,
466
+ errorCode:errorCode(error,'ANDROID_USER_MANAGED_OLLAMA_UNAVAILABLE')
467
+ });
468
+ }
469
+ }
470
+
471
+ function unavailableBrowserOllama(){
472
+ return freeze({
473
+ ready:false,
474
+ model:null,
475
+ errorCode:'ARCANE_CORE_REQUIRED'
476
+ });
477
+ }
478
+
479
+ async function probeNativeSpeech(arcane){
480
+ if(typeof arcane?.speech?.status!=='function'){
481
+ return freeze({
482
+ ...evaluateLocalSpeechHealth(null),
483
+ errorCode:'ARCANE_SPEECH_UNAVAILABLE'
484
+ });
485
+ }
486
+
487
+ try{
488
+ return freeze({
489
+ ...evaluateLocalSpeechHealth(await arcane.speech.status()),
490
+ errorCode:null
491
+ });
492
+ }catch(error){
493
+ return freeze({
494
+ ...evaluateLocalSpeechHealth(null),
495
+ errorCode:errorCode(error,'LOCAL_SPEECH_UNAVAILABLE')
496
+ });
497
+ }
498
+ }
499
+
500
+ async function probeBrowserSpeech(fetchImpl,timeoutMs){
501
+ try{
502
+ return freeze({
503
+ ...evaluateLocalSpeechHealth(
504
+ await boundedFetchJSON(
505
+ fetchImpl,
506
+ LOCAL_AI_BROWSER_ENDPOINTS.speech,
507
+ timeoutMs
508
+ )
509
+ ),
510
+ errorCode:null
511
+ });
512
+ }catch(error){
513
+ return freeze({
514
+ ...evaluateLocalSpeechHealth(null),
515
+ errorCode:errorCode(error,'LOCAL_SPEECH_UNAVAILABLE')
516
+ });
517
+ }
518
+ }
519
+
520
+ async function resolveMode(runtime,arcane){
521
+ if(
522
+ runtime==='native'
523
+ ||runtime==='browser'
524
+ ||runtime===USER_MANAGED_LOOPBACK_PROVIDER_MODE
525
+ ){
526
+ return runtime;
527
+ }
528
+
529
+ if(runtime!==undefined&&(!runtime||typeof runtime!=='object')){
530
+ throw new TypeError(
531
+ 'Local AI runtime must be "native", "browser", '
532
+ +'"user-managed-loopback", or a runtime snapshot.'
533
+ );
534
+ }
535
+
536
+ let snapshot=runtime;
537
+
538
+ if(snapshot===undefined&&typeof arcane?.runtime?.current==='function'){
539
+ try{
540
+ snapshot=await arcane.runtime.current();
541
+ }catch{
542
+ snapshot=null;
543
+ }
544
+ }
545
+
546
+ if(snapshot?.managedLocalAI===true){
547
+ return 'native';
548
+ }
549
+ if(
550
+ snapshot?.native===true
551
+ &&snapshot?.transport==='android-webview'
552
+ ){
553
+ return USER_MANAGED_LOOPBACK_PROVIDER_MODE;
554
+ }
555
+ return 'browser';
556
+ }
557
+
558
+ async function probeRequiredServices({
559
+ requirements,
560
+ mode,
561
+ arcane,
562
+ fetchImpl,
563
+ timeoutMs
564
+ }){
565
+ const requiresSpeech=requirements.stt.required||requirements.tts.required;
566
+ const [ollama,speech]=await Promise.all([
567
+ requirements.llm.required
568
+ ?mode==='native'
569
+ ?probeNativeOllama(arcane,requirements.llm.model)
570
+ :mode===USER_MANAGED_LOOPBACK_PROVIDER_MODE
571
+ ?probeUserManagedLoopbackOllama(
572
+ arcane,
573
+ requirements.llm.model
574
+ )
575
+ :unavailableBrowserOllama()
576
+ :null,
577
+ requiresSpeech
578
+ ?mode==='native'
579
+ ?probeNativeSpeech(arcane)
580
+ :mode===USER_MANAGED_LOOPBACK_PROVIDER_MODE
581
+ ?probeNativeSpeech(arcane)
582
+ :probeBrowserSpeech(fetchImpl,timeoutMs)
583
+ :null
584
+ ]);
585
+
586
+ return freeze({ollama,speech});
587
+ }
588
+
589
+ function slotResults(requirements,services){
590
+ const activeParallelRequests=positiveSafeInteger(
591
+ services.ollama?.activeParallelRequests
592
+ );
593
+
594
+ return freeze({
595
+ llm:{
596
+ ...requirements.llm,
597
+ ...(activeParallelRequests===null
598
+ ?{}
599
+ :{activeParallelRequests}),
600
+ ready:requirements.llm.required
601
+ ?services.ollama?.ready===true
602
+ :null,
603
+ status:!requirements.llm.required
604
+ ?'not-selected'
605
+ :services.ollama?.ready===true?'ready':'unavailable',
606
+ errorCode:requirements.llm.required&&services.ollama?.ready!==true
607
+ ?services.ollama?.errorCode||'OLLAMA_UNAVAILABLE'
608
+ :null
609
+ },
610
+ stt:{
611
+ ...requirements.stt,
612
+ ready:requirements.stt.required
613
+ ?services.speech?.stt?.ready===true
614
+ :null,
615
+ status:!requirements.stt.required
616
+ ?'not-selected'
617
+ :services.speech?.stt?.ready===true?'ready':'unavailable',
618
+ errorCode:requirements.stt.required&&services.speech?.stt?.ready!==true
619
+ ?services.speech?.errorCode||'LOCAL_STT_NOT_READY'
620
+ :null
621
+ },
622
+ tts:{
623
+ ...requirements.tts,
624
+ ready:requirements.tts.required
625
+ ?services.speech?.tts?.ready===true
626
+ :null,
627
+ status:!requirements.tts.required
628
+ ?'not-selected'
629
+ :services.speech?.tts?.ready===true?'ready':'unavailable',
630
+ errorCode:requirements.tts.required&&services.speech?.tts?.ready!==true
631
+ ?services.speech?.errorCode||'LOCAL_TTS_NOT_READY'
632
+ :null
633
+ }
634
+ });
635
+ }
636
+
637
+ function unavailableSlots(slots){
638
+ return ['llm','stt','tts'].filter(
639
+ slot=>slots[slot].required&&slots[slot].ready!==true
640
+ );
641
+ }
642
+
643
+ function recoveryServices(slots){
644
+ const services=[];
645
+
646
+ if(
647
+ slots.llm.required
648
+ &&slots.llm.ready!==true
649
+ &&!['OLLAMA_MODEL_INVALID','OLLAMA_MODEL_UNAVAILABLE'].includes(
650
+ slots.llm.errorCode
651
+ )
652
+ ){
653
+ services.push('ollama');
654
+ }
655
+
656
+ if(
657
+ (slots.stt.required&&slots.stt.ready!==true)
658
+ ||(slots.tts.required&&slots.tts.ready!==true)
659
+ ){
660
+ services.push('speech');
661
+ }
662
+
663
+ return services;
664
+ }
665
+
666
+ function recoveryFailureService(error,failedServices){
667
+ const service=token(
668
+ error?.diagnosticDetails?.service?.id,
669
+ {uppercase:false}
670
+ ).toLowerCase();
671
+
672
+ return failedServices.includes(service)
673
+ ?service
674
+ :null;
675
+ }
676
+
677
+ function preserveRecoveryFailure(
678
+ services,
679
+ failedServices,
680
+ recoveryErrorCode,
681
+ failedService
682
+ ){
683
+ if(!recoveryErrorCode){
684
+ return services;
685
+ }
686
+
687
+ const targets=failedService
688
+ ?[failedService]
689
+ :failedServices.length===1
690
+ ?failedServices
691
+ :[];
692
+
693
+ return freeze({
694
+ ollama:targets.includes('ollama')
695
+ &&services.ollama?.ready!==true
696
+ ?freeze({...services.ollama,errorCode:recoveryErrorCode})
697
+ :services.ollama,
698
+ speech:targets.includes('speech')
699
+ &&(
700
+ services.speech?.stt?.ready!==true
701
+ ||services.speech?.tts?.ready!==true
702
+ )
703
+ ?freeze({...services.speech,errorCode:recoveryErrorCode})
704
+ :services.speech
705
+ });
706
+ }
707
+
708
+ function browserManualInstruction(services){
709
+ if(services.includes('ollama')&&services.includes('speech')){
710
+ return 'Local model access requires Arcane Core. Manually start your local speech service if you still need it.';
711
+ }
712
+
713
+ if(services.includes('ollama')){
714
+ return 'Local model access requires Arcane Core.';
715
+ }
716
+
717
+ return 'Manually start your local speech service.';
718
+ }
719
+
720
+ function userManagedLoopbackInstruction(services){
721
+ if(services.includes('ollama')&&services.includes('speech')){
722
+ return 'Start your Ollama service in Android Linux Terminal, make the selected model available, and start the Arcane speech service on its configured loopback endpoint.';
723
+ }
724
+
725
+ if(services.includes('ollama')){
726
+ return 'Start your Ollama service in Android Linux Terminal and make the selected model available.';
727
+ }
728
+
729
+ return 'Start the Arcane speech service on its configured loopback endpoint.';
730
+ }
731
+
732
+ function guidance(mode,slots){
733
+ const affectedSlots=unavailableSlots(slots);
734
+
735
+ if(!affectedSlots.length){
736
+ return null;
737
+ }
738
+
739
+ const services=recoveryServices(slots);
740
+ const profileSettings=affectedSlots.map(slot=>slot+'Provider');
741
+ const message=mode==='browser'
742
+ ?browserManualInstruction(services)
743
+ +' Alternatively, switch the affected Profile setting to OpenAI and enter an OpenAI API/license key. Arcane will not switch providers automatically.'
744
+ :mode===USER_MANAGED_LOOPBACK_PROVIDER_MODE
745
+ ?userManagedLoopbackInstruction(services)
746
+ +' Arcane does not install, start, repair, pull, or otherwise manage this service or its models. Alternatively, switch the affected Profile setting to OpenAI. Arcane will not switch providers automatically.'
747
+ :'Arcane could not recover the selected local AI service. Retry or review Local AI in Arcane Settings. Arcane will not switch providers automatically.';
748
+
749
+ return freeze({
750
+ mode,
751
+ affectedSlots,
752
+ services,
753
+ profileSettings,
754
+ message,
755
+ automaticProviderSwitch:false
756
+ });
757
+ }
758
+
759
+ function nativeRecovery(arcane,recover){
760
+ if(recover!==undefined){
761
+ if(typeof recover!=='function'){
762
+ throw new TypeError('Local AI recovery must be a function.');
763
+ }
764
+
765
+ return recover;
766
+ }
767
+
768
+ return typeof arcane?.localAI?.recover==='function'
769
+ ?arcane.localAI.recover.bind(arcane.localAI)
770
+ :null;
771
+ }
772
+
773
+ function report({
774
+ mode,
775
+ requirements,
776
+ services,
777
+ recovery
778
+ }){
779
+ const slots=slotResults(requirements,services);
780
+ const unavailable=unavailableSlots(slots);
781
+
782
+ return freeze({
783
+ ready:unavailable.length===0,
784
+ mode,
785
+ requirements,
786
+ slots,
787
+ services,
788
+ unavailableSlots:unavailable,
789
+ recovery,
790
+ guidance:guidance(mode,slots)
791
+ });
792
+ }
793
+
794
+ /**
795
+ * Checks only the local providers selected in the supplied preference tuple.
796
+ *
797
+ * Managed native Arcane apps use capability-gated bridge methods and may
798
+ * request one bounded recovery attempt before one re-probe. Android uses only
799
+ * admitted native status/chat and speech-status methods for user-managed
800
+ * loopback services and never invokes lifecycle recovery. Ordinary browsers
801
+ * never probe Ollama; browser speech health remains read-only and never invokes
802
+ * recovery.
803
+ */
804
+ export async function checkLocalAIReadiness({
805
+ preferences,
806
+ runtime,
807
+ arcane=globalThis.Arcane,
808
+ fetchImpl=globalThis.fetch,
809
+ recover,
810
+ timeoutMs
811
+ }={}){
812
+ const requirements=deriveLocalAIRequirements(preferences);
813
+ const mode=await resolveMode(runtime,arcane);
814
+ const timeout=normalizedTimeout(timeoutMs);
815
+ let services=await probeRequiredServices({
816
+ requirements,
817
+ mode,
818
+ arcane,
819
+ fetchImpl,
820
+ timeoutMs:timeout
821
+ });
822
+ let slots=slotResults(requirements,services);
823
+ const failedServices=recoveryServices(slots);
824
+ const recoverNative=mode==='native'
825
+ ?nativeRecovery(arcane,recover)
826
+ :null;
827
+ const recovery={
828
+ attempted:false,
829
+ services:[],
830
+ errorCode:null
831
+ };
832
+ let failedRecoveryService=null;
833
+
834
+ if(failedServices.length&&recoverNative){
835
+ recovery.attempted=true;
836
+ recovery.services=[...failedServices];
837
+
838
+ try{
839
+ await recoverNative({services:[...failedServices]});
840
+ }catch(error){
841
+ recovery.errorCode=errorCode(error,'LOCAL_AI_RECOVERY_FAILED');
842
+ failedRecoveryService=recoveryFailureService(
843
+ error,
844
+ failedServices
845
+ );
846
+ }
847
+
848
+ services=await probeRequiredServices({
849
+ requirements,
850
+ mode,
851
+ arcane,
852
+ fetchImpl,
853
+ timeoutMs:timeout
854
+ });
855
+ services=preserveRecoveryFailure(
856
+ services,
857
+ failedServices,
858
+ recovery.errorCode,
859
+ failedRecoveryService
860
+ );
861
+ slots=slotResults(requirements,services);
862
+ }
863
+
864
+ return report({
865
+ mode,
866
+ requirements,
867
+ services,
868
+ recovery:freeze(recovery)
869
+ });
870
+ }