arcane-os 0.29.1 → 0.31.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,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.31.0
4
+
5
+ - Standalone applications use their repository root and installed npm package
6
+ paths. New standalone projects default to `appsRoot: "."`; initialization
7
+ preserves an existing configured layout, and explicit multi-app workspaces
8
+ retain their selected application directories.
9
+ - Remove the retired `legacyAppPaths` setting and all SDK-generated nested app
10
+ redirects and duplicate PWA worker/inventory files. Consumers must remove the
11
+ obsolete setting when upgrading. Enabled PWA files remain at the app root;
12
+ public SDK files remain under `node_modules/arcane-os` or the installed alias.
13
+ - Preserve selected authored files, complete URLs and queries, installation
14
+ identity, saved data and caches. Generation does not delete preexisting app
15
+ files; application owners preserve and relocate their content independently.
16
+ - Generated and offline app files are committed for hosting workflows to
17
+ consume. Existing public Node TWiN and browser APIs remain available.
18
+
19
+ ## 0.30.0
20
+
21
+ - Add the public `arcane-os/ai/twin-cloud` subpath with stateless `fetchRequest`
22
+ for Node and browser callers. Supply the TWiN key, model, messages, optional
23
+ structured-output schema and cancellation signal explicitly. The result is
24
+ the complete parsed provider completion, with no browser startup, saved
25
+ conversation, hidden model default or output cap.
26
+ - Share the existing TWiN HTTP, structured JSON, overload retry and cancellation
27
+ implementation with the browser AI owner while preserving its public methods
28
+ and lifecycle. Only overload responses with HTTP 429 use the existing
29
+ three-second retry; other failures retain their complete provider response.
30
+
3
31
  ## 0.29.1
4
32
 
5
33
  - Preserve complete long and non-ASCII filenames in application release bundles
package/README.md CHANGED
@@ -16,31 +16,34 @@
16
16
  `arcane-os` is the application SDK and command-line toolchain for Arcane OS. It
17
17
  supports two explicit workspace profiles: an external app repository uses the
18
18
  version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
- `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
- event, cancellation, and browser run contracts.
19
+ `arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
20
+ and browser run contracts while retaining their selected application layout.
21
21
 
22
- This checkout defines the `0.29.1` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.31.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
26
- External browser apps can use the installed npm package directly, without a
26
+ Standalone browser apps use their repository root and installed npm package directly, without a
27
27
  generated workspace `arcane/` directory or `arcane.lock.json`. Select the
28
28
  [four installed-package routes](docs/reference/protocols.md#installed-package-browser-routes)
29
29
  in `arcane-packager.json`: development, managed import maps, and PWA resources
30
30
  read the installed SDK at real `/node_modules/arcane-os/...` browser URLs when
31
31
  each destination equals its source. An alias uses its actual installed folder.
32
- Select `appsRoot: "."` for a standalone root app, or scaffold one with
33
- `arcane new my-app --apps-root .`. After npm installation, `arcane import-map`
34
- refreshes managed maps and the root app's static PWA/navigation files for ordinary
35
- static hosting. Portable app packages still contain their selected runtime.
32
+ `arcane new my-app` defaults to `appsRoot: "."`. After npm installation,
33
+ `arcane import-map` refreshes managed maps and the enabled root PWA files for
34
+ ordinary static hosting. It generates no nested app redirects or duplicate PWA
35
+ files. Remove the retired `legacyAppPaths` setting when upgrading. Commit generated
36
+ and offline app files for hosting workflows to consume. Portable app packages
37
+ still contain their selected runtime.
36
38
  Root/direct maps use `arcane-os/modules/<filename>` and
37
39
  `arcane-os/entities/<filename>` (including extensions) plus the focused lowercase
38
40
  exports, rather than `arcane/*` aliases. These paths resolve to the actual npm
39
41
  files, preserving relative component URLs. New root apps put the SDK in runtime
40
42
  `dependencies`; init preserves existing runtime/optional declarations and promotes
41
43
  a root app's SDK development declaration without changing its other packages.
42
- Existing `apps/<id>`, virtual `/arcane` routes, and materialized workspaces remain
43
- supported. Node services continue to
44
+ Explicit multi-app `appsRoot: "apps"`, virtual runtime routes, and existing
45
+ materialized workspaces remain supported; initialization preserves their selected
46
+ layout. Node services continue to
44
47
  use the installed CLI or public imports such as `arcane-os/mail`.
45
48
 
46
49
  The [mail gateway](docs/reference/mail.md) serves HTTPS with HTTP/2 on port 4433
@@ -82,7 +85,7 @@ Create one browser application, install its pinned SDK, and start its source
82
85
  server:
83
86
 
84
87
  ```bash
85
- npx arcane-os@0.26.0 new hello-speech --path ./hello-speech --target browser
88
+ npx arcane-os@0.31.0 new hello-speech --path ./hello-speech --target browser
86
89
  cd hello-speech
87
90
  npm install
88
91
  ```
@@ -74,11 +74,11 @@ export function getBrowserDeviceSettings(navigatorObject = globalThis.navigator)
74
74
 
75
75
  // Only an explicit browser adapter type or fallback flag establishes its class.
76
76
  // Vendor names and powerPreference (including Chromium's echoed request) do not.
77
- export function describeBrowserGpu(info, legacyFallbackAdapter) {
77
+ export function describeBrowserGpu(info, fallbackAdapter) {
78
78
  const adapterType = is.string(info?.type) ? info.type : null;
79
79
  const isFallbackAdapter = is.boolean(info?.isFallbackAdapter)
80
80
  ? info.isFallbackAdapter
81
- : is.boolean(legacyFallbackAdapter) ? legacyFallbackAdapter : null;
81
+ : is.boolean(fallbackAdapter) ? fallbackAdapter : null;
82
82
  let performanceStatus = 'unknown';
83
83
  if (isFallbackAdapter === true || adapterType === 'CPU') performanceStatus = 'fallback';
84
84
  else if (adapterType === 'discrete GPU') performanceStatus = 'discrete';
@@ -0,0 +1,230 @@
1
+ import Is from 'strong-type';
2
+ import {arcaneLogging} from '../logging.mjs';
3
+
4
+ const is = new Is(false);
5
+ const twinChatURL = 'https://inference.do-ai.run/v1/chat/completions';
6
+
7
+ /** A stateless TWiN request; browser AI shares the HTTP and format owners below. */
8
+ export async function fetchRequest({
9
+ twinKey,
10
+ model,
11
+ messages = [],
12
+ structuredOutput = false,
13
+ tools = [],
14
+ toolChoice = 'auto',
15
+ parallelToolCalls,
16
+ reasoningEffort,
17
+ signal = null,
18
+ id = Date.now(),
19
+ onRequest = function observeTWiNRequest(){},
20
+ onResponse = function observeTWiNResponse(){}
21
+ } = {}){
22
+ if(signal?.aborted){
23
+ throw normalizeAIRequestAbort(signal.reason);
24
+ }
25
+ if(!is.string(twinKey) || !twinKey){
26
+ const error = new Error('AI provider is not configured.');
27
+ error.code = 'AI_PROVIDER_NOT_CONFIGURED';
28
+ throw error;
29
+ }
30
+ if(!is.string(model) || !model){
31
+ throw new TypeError('TWiN fetchRequest requires an explicit model.');
32
+ }
33
+
34
+ const request = {model, messages, stream:false};
35
+ const format = structuredOutputFormat(structuredOutput);
36
+ if(format){
37
+ request.response_format = openAIResponseFormat(format);
38
+ }
39
+ if(tools.length){
40
+ request.tools = tools;
41
+ request.tool_choice = toolChoice;
42
+ if(parallelToolCalls !== undefined){
43
+ request.parallel_tool_calls = parallelToolCalls;
44
+ }
45
+ }
46
+ if(reasoningEffort){
47
+ request.reasoning_effort = reasoningEffort;
48
+ }
49
+
50
+ try{
51
+ await onRequest(
52
+ request,
53
+ id,
54
+ {operation:'fetch', transport:'http', destination:twinChatURL}
55
+ );
56
+ if(signal?.aborted){
57
+ throw normalizeAIRequestAbort(signal.reason);
58
+ }
59
+ const response = await fetchJSONResponse(
60
+ twinChatURL,
61
+ {
62
+ method:'POST',
63
+ credentials:'omit',
64
+ headers:{
65
+ 'Content-Type':'application/json',
66
+ Authorization:`Bearer ${twinKey}`
67
+ },
68
+ body:JSON.stringify(request),
69
+ ...(signal ? {signal} : {})
70
+ }
71
+ );
72
+ if(signal?.aborted){
73
+ throw normalizeAIRequestAbort(signal.reason);
74
+ }
75
+ await onResponse(response, id, false);
76
+ if(signal?.aborted){
77
+ throw normalizeAIRequestAbort(signal.reason);
78
+ }
79
+ return response;
80
+ }catch(error){
81
+ if(isAIRequestAbort(error, signal)){
82
+ throw normalizeAIRequestAbort(error);
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ export function isAIRequestAbort(error, signal){
89
+ return signal?.aborted
90
+ || error?.name === 'AbortError'
91
+ || error?.code === 'ARCANE_REQUEST_ABORTED'
92
+ || error?.code === 'ARCANE_AI_REQUEST_ABORTED'
93
+ || error?.code === 'AI_REQUEST_ABORTED';
94
+ }
95
+
96
+ export function normalizeAIRequestAbort(error){
97
+ if(error?.code === 'ARCANE_AI_REQUEST_ABORTED'){
98
+ return error;
99
+ }
100
+ const normalized = new Error('The AI request was cancelled.', {cause:error});
101
+ normalized.name = 'AbortError';
102
+ normalized.code = 'ARCANE_AI_REQUEST_ABORTED';
103
+ return normalized;
104
+ }
105
+
106
+ export function structuredOutputFormat(value = false){
107
+ if(value === false || value === null || value === undefined){
108
+ return null;
109
+ }
110
+ if(value === true || value === 'json'){
111
+ return 'json';
112
+ }
113
+ if(
114
+ is.object(value)
115
+ && !is.array(value)
116
+ && (
117
+ Object.getPrototypeOf(value) === Object.prototype
118
+ || Object.getPrototypeOf(value) === null
119
+ )
120
+ ){
121
+ return value;
122
+ }
123
+ const error = new TypeError(
124
+ 'AI structured output must be enabled with true, json, or a JSON Schema object.'
125
+ );
126
+ error.code = 'AI_STRUCTURED_OUTPUT_INVALID';
127
+ throw error;
128
+ }
129
+
130
+ export function openAIResponseFormat(format){
131
+ if(format === 'json'){
132
+ return {type:'json_object'};
133
+ }
134
+ if(format){
135
+ return {
136
+ type:'json_schema',
137
+ json_schema:{name:'structured_response', strict:true, schema:format}
138
+ };
139
+ }
140
+ return null;
141
+ }
142
+
143
+ /** Shared with browser streaming; consume no successful body at this boundary. */
144
+ export async function fetchHTTPResponse(url, options){
145
+ const {signal} = options;
146
+ const retryDelayMs = 3000;
147
+ try{
148
+ while(true){
149
+ if(signal?.aborted){
150
+ throw normalizeAIRequestAbort(signal.reason);
151
+ }
152
+ const response = await fetch(url, options);
153
+ if(signal?.aborted){
154
+ throw normalizeAIRequestAbort(signal.reason);
155
+ }
156
+ if(response.ok){
157
+ return response;
158
+ }
159
+ const contentType = response.headers.get('content-type') || '';
160
+ const error = contentType.includes('application/json')
161
+ ? await response.json()
162
+ : await response.text();
163
+ if(signal?.aborted){
164
+ throw normalizeAIRequestAbort(signal.reason);
165
+ }
166
+ const message = is.string(error)
167
+ ? error
168
+ : error?.error?.message ?? error?.message;
169
+ if(
170
+ response.status !== 429
171
+ || !is.string(message)
172
+ || !message.toLowerCase().includes('overload')
173
+ ){
174
+ throw error;
175
+ }
176
+ arcaneLogging.warn(
177
+ `${message}\nRetrying in ${retryDelayMs / 1000} seconds`,
178
+ error
179
+ );
180
+ await new Promise(function waitForOverloadRetry(resolve, reject){
181
+ function finishRetryDelay(){
182
+ signal?.removeEventListener('abort', cancelRetryDelay);
183
+ resolve();
184
+ }
185
+ function cancelRetryDelay(){
186
+ clearTimeout(timer);
187
+ signal.removeEventListener('abort', cancelRetryDelay);
188
+ reject(normalizeAIRequestAbort(signal.reason));
189
+ }
190
+ const timer = setTimeout(finishRetryDelay, retryDelayMs);
191
+ signal?.addEventListener('abort', cancelRetryDelay, {once:true});
192
+ if(signal?.aborted){
193
+ cancelRetryDelay();
194
+ }
195
+ });
196
+ }
197
+ }catch(error){
198
+ if(isAIRequestAbort(error, signal)){
199
+ throw normalizeAIRequestAbort(error);
200
+ }
201
+ throw error;
202
+ }
203
+ }
204
+
205
+ /** Return the entire parsed completion without selecting or rewriting choices. */
206
+ export async function fetchJSONResponse(url, options){
207
+ const {signal} = options;
208
+ try{
209
+ const response = await fetchHTTPResponse(url, options);
210
+ if(signal?.aborted){
211
+ throw normalizeAIRequestAbort(signal.reason);
212
+ }
213
+ const contentType = response.headers.get('content-type') || '';
214
+ if(!contentType.includes('application/json')){
215
+ throw new TypeError(
216
+ `AI request returned ${contentType || 'an unknown content type'} instead of JSON.`
217
+ );
218
+ }
219
+ const completion = await response.json();
220
+ if(signal?.aborted){
221
+ throw normalizeAIRequestAbort(signal.reason);
222
+ }
223
+ return completion;
224
+ }catch(error){
225
+ if(isAIRequestAbort(error, signal)){
226
+ throw normalizeAIRequestAbort(error);
227
+ }
228
+ throw error;
229
+ }
230
+ }
@@ -1,8 +1,125 @@
1
1
  # TWiN Cloud: one request
2
2
 
3
- TWiN Cloud is the high-level `AI.js` default remote LLM service, named `TWIN`.
4
- Speech stays on device and does not use the TWiN access key. This guide uses the
5
- same managed browser imports as the [browser speech quick start](browser-speech.md).
3
+ Use `fetchRequest` from `arcane-os/ai/twin-cloud` for a complete TWiN Cloud
4
+ request in Node or a browser with an explicit key and model. It imports no
5
+ browser profile, DOM, user singleton, or storage, and starts no work on import.
6
+ The existing browser `AI.js` interface remains available for applications that
7
+ already use its provider selection, lifecycle, and speech. TWiN Cloud is that
8
+ interface's default remote LLM service, named `TWIN`; speech stays on device
9
+ and does not use the TWiN access key.
10
+
11
+ ## Node: explicit key, model, and structured result
12
+
13
+ Install the published `arcane-os` package in the Node project. Keep the key in
14
+ the application's existing server configuration, outside source control and
15
+ diagnostics. In this example, `server-config.json` is that caller-owned local
16
+ configuration file with a `twinKey` property; add its exact path to `.gitignore`
17
+ before creating it. The SDK does not discover or write this file.
18
+
19
+ ```javascript
20
+ import serverConfig from './server-config.json' with {type: 'json'};
21
+ import {fetchRequest} from 'arcane-os/ai/twin-cloud';
22
+
23
+ const response = await fetchRequest({
24
+ twinKey: serverConfig.twinKey,
25
+ model: 'openai-gpt-oss-20b',
26
+ messages: [{
27
+ role: 'user',
28
+ content: 'Explain why the moon-powered toaster keeps burning breakfast. Return HTML and plain text.'
29
+ }],
30
+ structuredOutput: {
31
+ type: 'object',
32
+ properties: {
33
+ html: {type: 'string'},
34
+ text: {type: 'string'}
35
+ },
36
+ required: ['html', 'text'],
37
+ additionalProperties: false
38
+ }
39
+ });
40
+
41
+ console.log(response);
42
+ ```
43
+
44
+ `model` is required and remains exactly the supplied identifier. This function
45
+ does not select the browser profile's default model. The example's prompt and
46
+ `html`/`text` schema are caller-owned data, not SDK business logic. Changing
47
+ those fields changes the requested result without changing the SDK.
48
+
49
+ The resolved value is the complete parsed provider JSON, including every
50
+ choice and provider field. The SDK does not extract only the first message,
51
+ parse its content into a second object, or replace the response with an
52
+ application-specific record. The supplied schema becomes
53
+ `response_format: {type:'json_schema', json_schema:{name:'structured_response',
54
+ strict:true, schema:...}}`. `structuredOutput:true` or `'json'` instead selects
55
+ `response_format:{type:'json_object'}`; omission leaves structured output off.
56
+
57
+ ## Shared request behavior
58
+
59
+ The focused API accepts complete `messages`, optional `tools`, `toolChoice`,
60
+ `parallelToolCalls`, and `reasoningEffort` in addition to the explicit
61
+ `twinKey` and `model`. Tool options use the existing chat-completion wire fields
62
+ `tools`, `tool_choice`, and `parallel_tool_calls` when `tools` is nonempty.
63
+ A supplied nonempty `reasoningEffort` uses the provider's `reasoning_effort`
64
+ field; omission preserves its default. No output limit is added by this API.
65
+ The function neither executes tools nor adds provider-response envelope
66
+ validation.
67
+
68
+ Optional `id`, `onRequest(request,id,metadata)`, and
69
+ `onResponse(response,id,false)` follow the complete-response `AI.fetchRequest`
70
+ callback shape. The request callback runs before dispatch; the response
71
+ callback receives the complete parsed result before it is returned. Omitted
72
+ `id` uses `Date.now()`; request metadata is
73
+ `{operation:'fetch',transport:'http',destination:'https://inference.do-ai.run/v1/chat/completions'}`.
74
+ The key is
75
+ transport authentication, not part of either callback's message payload. Keep
76
+ credentials out of application logging as well.
77
+
78
+ Only HTTP `429` with a message containing `overload` (case-insensitive) repeats automatically,
79
+ after `3000` milliseconds. Another overload repeats the same complete request;
80
+ other HTTP failures do not become an automatic retry loop. Pass a fresh
81
+ `AbortController`'s `signal` and call `abort()` to cancel. Cancellation during
82
+ the request, response-body read, retry wait, or callback settlement prevents
83
+ successful result delivery and rejects with `ARCANE_AI_REQUEST_ABORTED`.
84
+ Other HTTP failures throw the complete parsed JSON error body or text body.
85
+ A missing key uses `AI_PROVIDER_NOT_CONFIGURED`, a missing explicit model
86
+ throws `TypeError`, and an unsupported `structuredOutput` input uses
87
+ `AI_STRUCTURED_OUTPUT_INVALID`.
88
+
89
+ The SDK retains no request or response history between calls and uses no
90
+ DBOPFS, chat entity, or memory extraction. Each call's `messages` are its
91
+ complete caller-supplied context. The caller decides whether and how to keep
92
+ the result; this stateless transport adds no saved conversation or migration.
93
+
94
+ Browser applications can use this same focused import through their generated
95
+ managed import map. Browser Fetch and CORS behavior still apply. Importing it
96
+ does not instantiate `AI`, read saved preferences, configure speech, or change
97
+ the existing `arcane-os/ai` browser entry.
98
+
99
+ ### Shared low-level integration helpers
100
+
101
+ The same module also exports the helpers used by browser `AI.js`. Ordinary
102
+ callers use `fetchRequest`; these exports let SDK transport integration share
103
+ the existing implementation rather than maintain another retry or body reader.
104
+
105
+ | Export | Contract |
106
+ | --- | --- |
107
+ | `fetchHTTPResponse(url,options)` | Uses the caller's Fetch options, overload retry and cancellation; returns a successful `Response` with its body unconsumed. |
108
+ | `fetchJSONResponse(url,options)` | Uses that HTTP owner, requires `application/json`, and returns the complete parsed body without selecting choices. |
109
+ | `structuredOutputFormat(value=false)` | Maps false/null/undefined to null, true/`'json'` to `'json'`, and preserves a supplied plain JSON Schema object. Other inputs use `AI_STRUCTURED_OUTPUT_INVALID`. |
110
+ | `openAIResponseFormat(format)` | Maps the normalized value to `json_object`, strict `json_schema` named `structured_response`, or null. |
111
+ | `isAIRequestAbort(error,signal)` | Recognizes an aborted signal, `AbortError`, or the existing Arcane AI/request cancellation codes. |
112
+ | `normalizeAIRequestAbort(error)` | Preserves an existing `ARCANE_AI_REQUEST_ABORTED` error or creates that `AbortError` with the original value as its cause. |
113
+
114
+ These helpers start no work on import. HTTP helpers require explicit URL and
115
+ options; they do not add a key, model, browser state, or retained conversation.
116
+ Overload warnings use the shared console logger and preserve the complete
117
+ provider error.
118
+
119
+ ## Existing browser AI interface
120
+
121
+ The following browser example uses the same managed imports as the
122
+ [browser speech quick start](browser-speech.md).
6
123
 
7
124
  ## Install and import
8
125
 
@@ -78,6 +78,7 @@ version; WebKitGTK availability must not be generalized to macOS.
78
78
  | Read host identity, capabilities, storage, preferences, appearance, or platform state | `globalThis.Arcane` | **Cross-host** where the method is implemented and admitted | Promise behavior and `Arcane.Error` are normalized. Result fields are normalized unless the method explicitly documents a platform-dependent snapshot. |
79
79
  | Use local AI without coupling app code to Ollama HTTP | `Arcane.localAI`, `Arcane.ai`, or `/arcane/modules/Ollama.js` | Primarily **Native**; Android exposes a narrower admitted inference projection | Admission, errors, and managed-operation events are normalized. Direct Ollama response envelopes remain **Provider-native**. |
80
80
  | Use TWiN Cloud from the renderer profile | `/arcane/modules/AI.js` | **Cloud** from an allowed browser/native renderer | High-level chat behavior is normalized by the module. The TWiN access key authenticates remote LLM chat; raw provider diagnostics remain provider-specific. No automatic cloud fallback is inferred from local failure. |
81
+ | Send one TWiN Cloud request with an explicit key and model | `fetchRequest` from `arcane-os/ai/twin-cloud` | **Node** and **Browser**, using standard Fetch and a remote HTTPS provider | Keeps complete messages and returns the full parsed provider JSON. Shared structured-output mapping, overload-only HTTP 429 retry after 3000 ms, and cancellation match browser TWiN transport. No browser profile, AI/user singleton, DBOPFS, or retained request history is created. |
81
82
  | Use speech through one application helper | `/arcane/modules/AI.js` and `Arcane.speech` | **Browser** or **Native** | The helper keeps audio on device: Whisper owns STT and Kokoro owns TTS. It automatically cleans only the outbound speech-input copy and normalizes application-facing audio/text behavior while browser and native request/response plumbing differs below that boundary. |
82
83
  | Inspect or manage raw Ollama models | `Arcane.ollama` or `/arcane/modules/Ollama.js` | **Native** desktop Core for management; narrower Android inference only | Wrapper method names, errors, streaming correlation, and admission are Arcane-controlled. Direct Ollama success envelopes are intentionally provider-native. |
83
84
  | Use native terminal, installation, user, provisioning, or machine controls | matching `Arcane.*` namespace | **Native** and app/capability restricted | Calls and errors use the common bridge contract. Platform results can be host-specific and are marked in the method guide. |
@@ -240,6 +241,14 @@ snapshot.
240
241
 
241
242
  ### Provider-native within an Arcane boundary
242
243
 
244
+ [`arcane-os/ai/twin-cloud`](ai/twin-cloud.md) accepts an explicit `twinKey` and
245
+ `model`, with the same named `fetchRequest` import in Node and managed browsers.
246
+ It preserves complete provider response JSON while mapping the supplied
247
+ structured-output, tool, and reasoning options to the TWiN wire contract.
248
+ The SDK adds no output cap or persisted context. Cancellation applies during
249
+ request, body read, overload retry wait, and callback settlement. The existing
250
+ profile-backed browser `AI.js` entry and its lifecycle remain separate.
251
+
243
252
  Direct `Arcane.ollama.chat()`, `generate()`, `show()`, `embed()`, and lifecycle
244
253
  methods return complete Ollama-compatible envelopes. Arcane still owns error
245
254
  normalization, chunk correlation, and host transport, but it does
@@ -120,8 +120,9 @@ directory as a repository. Native target scaffolds also retain `browser` and
120
120
  include the required icon. The result reports the workspace, app, descriptor,
121
121
  target, and created paths.
122
122
 
123
- `--apps-root .` creates a standalone root application using its installed npm
124
- SDK directly. The default `--apps-root apps` preserves `apps/<id>`. Root setup
123
+ Each standalone app's root is its repository root. The default `--apps-root .`
124
+ uses the installed npm SDK directly. Explicit `--apps-root apps` selects a
125
+ multi-app workspace with each app beneath `apps/<id>`. Root setup
125
126
  does not install dependencies or copy a runtime: run `npm install`, then
126
127
  `npm run import-map`. Until installation, its result reports the import map as
127
128
  pending with reason `sdk-install-required`.
@@ -152,7 +153,7 @@ idempotent only for files whose existing content satisfies the scaffold
152
153
  contract.
153
154
 
154
155
  `--apps-root .` selects root setup for a standalone workspace. Omission retains
155
- the configured layout, or `apps` for a new configuration. `init` never moves an
156
+ the configured layout, or `.` for a new standalone configuration. `init` never moves an
156
157
  existing application or converts the integrated Arcane OS layout.
157
158
 
158
159
  ### Example
@@ -217,8 +218,9 @@ the workspace does not already identify exactly one. The command accepts no
217
218
  positional arguments and supports app scope only. `arcane-os import-map` is the
218
219
  identical executable alias.
219
220
 
220
- The generated artifact is
221
- `apps/<id>/modules/arcane.importmap.json`. Its exact JSON is also installed in
221
+ The generated artifact is `modules/arcane.importmap.json` at a standalone
222
+ app's repository root, or `apps/<id>/modules/arcane.importmap.json` for an
223
+ explicit multi-app workspace. Its exact JSON is also installed in
222
224
  the configured entry and every other admitted browser document as `<script
223
225
  type="importmap" data-arcane-import-map>` before module loading. The complete
224
226
  runtime map derives its entries from the selected runtime and browser-runtime
@@ -228,15 +230,13 @@ portable runtime subpaths such as `arcane-os/preference-store` and
228
230
  modules. The result reports the complete map written to the selected
229
231
  application; no fixed entry count is a release contract.
230
232
 
231
- For `appsRoot: "."`, the artifact is `modules/arcane.importmap.json` at the
232
- application root. Set `"legacyAppPaths": false` in `arcane-packager.json` to
233
- omit SDK-generated `apps/<id>/` navigation/PWA compatibility files and aliases.
234
- The same workspace choice applies to `arcane dev` and `arcane package`; restart
235
- an already running dev server after changing it. The default remains `true`.
236
- Root PWA files, app/installation identity and selected authored files are
237
- preserved. Existing files are never deleted by this option. See
238
- [root-only generated output](pwa.md#root-only-generated-output) before changing
239
- the URLs needed by previously installed apps.
233
+ For `appsRoot: "."`, managed imports use the installed package paths and enabled
234
+ PWA files are written beside the root entry. The SDK generates no nested
235
+ `apps/<id>/` redirects or duplicate PWA files and no repository-root `arcane/`
236
+ projection. App and installation identity, saved data, and selected authored
237
+ files remain unchanged. The same layout applies to `arcane dev` and
238
+ `arcane package`. Generated and offline app files are committed; GitHub Actions
239
+ consume those committed files. See [root generated output](pwa.md#root-generated-output).
240
240
 
241
241
  SDK `0.5.17` preserves the physical workspace route count and ordered include
242
242
  list. External and modern integrated routes require `components`, `css`,
@@ -531,8 +531,9 @@ npm exec -- arcane check --app hello-world
531
531
 
532
532
  Creates one complete browser release beneath `dist/<id>/`, preserving the prior
533
533
  output until the replacement is complete. It consumes saved source and managed
534
- import maps, places app files beneath `apps/<id>/`, and retains the configured
535
- shared route destinations. When selected shared content supplies no root
534
+ import maps, keeps standalone app files at the output root, and retains the
535
+ configured shared route destinations. Explicit multi-app workspaces retain
536
+ their selected app beneath `apps/<id>/`. When selected shared content supplies no root
536
537
  `index.html`, the SDK generates one that opens the selected app entry.
537
538
  Source document bases and resource URLs therefore retain their development
538
539
  layout. Packaging does not run tests or checks automatically.
@@ -542,11 +543,10 @@ selected SDK content directly from `node_modules`. Only the portable output
542
543
  receives copies; no workspace `arcane/` projection is required. Its runtime URLs,
543
544
  managed import-map targets, and PWA inventory destinations match source serving.
544
545
 
545
- With `appsRoot: "."`, app files retain their root-relative layout. The optional
546
- root-config `legacyAppPaths: false` omits SDK-generated compatibility files
547
- beneath `apps/<id>/` from the planned and actual output. It does not omit
548
- explicitly selected authored resources at those paths or change the default
549
- packaged installation identity. Omission or `true` preserves existing behavior.
546
+ With `appsRoot: "."`, the planned and actual output retain root-relative app
547
+ files and direct npm package paths. The SDK adds no nested app redirects or
548
+ duplicate PWA worker/inventory files. Explicitly selected authored resources
549
+ and the default packaged installation identity remain unchanged.
550
550
 
551
551
  ```text
552
552
  arcane package [--app <id>] [--dry-run]
@@ -898,10 +898,10 @@ Resend key for all its requests. An absent named key never falls back to the
898
898
  default account. Existing root `RESEND_API_KEY` and
899
899
  `MAIL_PROFILES[name].RESEND_API_KEY` remain fallbacks when the corresponding
900
900
  nested key property is absent. A nested property containing null or an empty
901
- string means the selected key is absent and takes precedence over a legacy key.
901
+ string means the selected key is absent and takes precedence over a root or profile key.
902
902
 
903
903
  Set and delete preserve the file's other settings and profile containers. New
904
- keys are written to the nested mail member. Existing legacy keys are updated
904
+ keys are written to the nested mail member. Existing root or profile keys are updated
905
905
  in place unless the selected nested key property exists; delete removes both
906
906
  representations of only the selected key. Status returns the selected profile,
907
907
  `provider:'resend'`, `storage:'.arcane.env.json'`, and `exists`. Delete returns
@@ -963,7 +963,7 @@ paths. It consumes the configured provider timeout and retry guidance.
963
963
 
964
964
  The Resend credential comes from the selected `.arcane.env.json` entry.
965
965
  An explicit `--profile` overrides `arcane.config.json.mail.profile`; omitting
966
- both selects the default `mail.apiKey`, with the legacy fallback described
966
+ both selects the default `mail.apiKey`, with the root-key fallback described
967
967
  above. Neither the key nor report content is accepted through argv or process
968
968
  environment variables.
969
969
 
@@ -1006,7 +1006,7 @@ Add the listener's certificate paths to `arcane.config.json`:
1006
1006
 
1007
1007
  Supply an existing PEM certificate chain and its private key. Paths resolve
1008
1008
  relative to the selected configuration directory, or may be absolute. They
1009
- belong to the listener regardless of the provider key selected. Legacy root
1009
+ belong to the listener regardless of the provider key selected. Existing root
1010
1010
  `MAIL_TLS_CERT_PATH` and `MAIL_TLS_KEY_PATH` in `.arcane.env.json` remain
1011
1011
  fallbacks for omitted config paths. Explicit programmatic `certPath` and
1012
1012
  `keyPath` options override those files. Missing TLS settings name the fields