arcane-os 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +97 -19
  4. package/browser-runtime/ai/dbopfs-model-parts.mjs +12 -5
  5. package/browser-runtime/ai/dbopfs-model-resources.mjs +2 -5
  6. package/browser-runtime/core/app-control.mjs +439 -0
  7. package/docs/reference/ai/browser-speech.md +11 -0
  8. package/docs/reference/ai/browser-wasm.md +21 -10
  9. package/docs/reference/cli.md +9 -1
  10. package/docs/reference/core-native-packaging.md +14 -0
  11. package/docs/reference/inventory/package-api.json +23 -9
  12. package/docs/reference/inventory/runtime-components.json +2 -1
  13. package/docs/reference/native-app-control.md +147 -0
  14. package/docs/reference/native-decisions.md +74 -16
  15. package/docs/reference/runtime-components.md +20 -0
  16. package/docs/reference/sdk-api.md +49 -6
  17. package/package.json +2 -1
  18. package/runtime/arcane/components/chat.html +140 -18
  19. package/runtime/arcane/modules/AI.js +10 -5
  20. package/src/cli/app-control.mjs +106 -0
  21. package/src/cli/main.mjs +5 -0
  22. package/src/core/app-control.mjs +23 -0
  23. package/src/core/hosts/windows/ArcaneAppControl.cs +525 -0
  24. package/src/core/hosts/windows/ArcaneCoreProcess.cs +2 -63
  25. package/src/core/hosts/windows/ArcaneFrameTransport.cs +80 -0
  26. package/src/core/hosts/windows/ArcaneHost.cs +45 -7
  27. package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
  28. package/src/core/hosts/windows/ArcaneWindowControl.cs +248 -0
  29. package/src/core/services/decisions.mjs +16 -2
  30. package/src/core/shared-host.mjs +7 -91
  31. package/src/core/socket-connection.mjs +109 -0
  32. package/src/local-ai/decision-tokenizer-worker.mjs +1 -1
  33. package/src/local-ai/decision-tokenizer.mjs +2 -2
  34. package/src/local-ai/decisions.mjs +33 -10
@@ -886,6 +886,7 @@
886
886
  const aiActivationButton=shadowRoot.querySelector('#ai_activation_button');
887
887
  const [
888
888
  {default:MD},
889
+ {hydrateMarkdownMedia},
889
890
  {default:FileEntity},
890
891
  {ConversationSubmissionBarrier,requireConversationTimeboxDelivery},
891
892
  {requestAIRuntimeIntent,subscribeAIRuntimeState},
@@ -893,6 +894,7 @@
893
894
  {getBrowserDeviceClass}
894
895
  ]=await Promise.all([
895
896
  import('../modules/MD.js'),
897
+ import('../modules/MarkdownMedia.js'),
896
898
  import('../entities/File.js'),
897
899
  import('../modules/ConversationTimebox.js'),
898
900
  import('../modules/AIRuntimeState.js'),
@@ -946,15 +948,19 @@
946
948
  let destroyed=false;
947
949
  let recognitionDraft=null;
948
950
  let writingRecognitionDraft=false;
951
+ const transcriptMedia=new Map();
952
+ let printMediaRetains=null;
949
953
  const printViewPromise=import('../modules/PrintView.js').then(
950
954
  function registerChatPrint({createPrintView}){
951
955
  if(destroyed) return null;
952
956
  return createPrintView({
953
957
  host,
954
- content:function chatPrintContent(){return chatOutput;},
958
+ content:renderedChatPrintContent,
955
959
  title:function chatPrintTitle(){return host.printTitle||'Conversation';},
956
960
  active:function chatPrintActive(){return !destroyed&&chatOutput.children.length>0;},
957
961
  signal:aiRuntimeStateAbortController.signal,
962
+ retain:retainChatPrintMedia,
963
+ prepare:prepareChatPrintMedia,
958
964
  onError:reportChatPrintFailure
959
965
  });
960
966
  }
@@ -1190,6 +1196,7 @@
1190
1196
  for(const item of record.removedNodes) {
1191
1197
  if(item.localName === 'li' && item.parentElement !== chatOutput) {
1192
1198
  transcriptResizeObserver?.unobserve(item);
1199
+ releaseTranscriptMessageMedia(item);
1193
1200
  }
1194
1201
  }
1195
1202
  for(const item of record.addedNodes) {
@@ -1427,14 +1434,72 @@
1427
1434
  return entry;
1428
1435
  }
1429
1436
 
1437
+ function renderTranscriptMarkdown(target,content){
1438
+ if(destroyed) return null;
1439
+ const template=target.ownerDocument.createElement('template');
1440
+ template.innerHTML=new MD(content).rendered;
1441
+ const pending=[...template.content.querySelectorAll('img')].some(
1442
+ function localChatImage(image){
1443
+ return image.getAttribute('src')?.startsWith('arcane-media:');
1444
+ }
1445
+ );
1446
+ // Remove local-reference src values while detached; text renders now,
1447
+ // and the shared media owner observes independent storage and decoding.
1448
+ const handle=hydrateMarkdownMedia(template.content,{
1449
+ signal:aiRuntimeStateAbortController.signal
1450
+ });
1451
+ releaseTranscriptMarkdown(target);
1452
+ const media={handle,pending,error:null,status:null,ready:null};
1453
+ transcriptMedia.set(target,media);
1454
+ if(printMediaRetains){
1455
+ printMediaRetains.add(handle.retain());
1456
+ }
1457
+ target.raw=content;
1458
+ target.replaceChildren(template.content);
1459
+ media.ready=handle.ready.then(function transcriptImagesReady(){
1460
+ media.pending=false;
1461
+ },function transcriptImagesFailed(error){
1462
+ media.pending=false;
1463
+ media.error=error;
1464
+ if(destroyed||transcriptMedia.get(target)!==media) return;
1465
+ arcaneLogging.error('Unable to display saved Chat images.',error);
1466
+ const status=target.ownerDocument.createElement('p');
1467
+ status.className='message_media_status';
1468
+ status.setAttribute('role','status');
1469
+ status.textContent='Some saved images could not be displayed.';
1470
+ media.status=status;
1471
+ target.after(status);
1472
+ scrollTranscriptToBottom();
1473
+ });
1474
+ return media;
1475
+ }
1476
+
1477
+ function releaseTranscriptMarkdown(target){
1478
+ const media=transcriptMedia.get(target);
1479
+ if(!media) return;
1480
+ transcriptMedia.delete(target);
1481
+ media.status?.remove();
1482
+ media.handle.destroy();
1483
+ }
1484
+
1485
+ function releaseTranscriptMessageMedia(item){
1486
+ const markdown=item?.querySelector('.markdown');
1487
+ if(markdown) releaseTranscriptMarkdown(markdown);
1488
+ }
1489
+
1490
+ function removeTranscriptMessage(item){
1491
+ if(!item) return;
1492
+ releaseTranscriptMessageMedia(item);
1493
+ item.remove();
1494
+ }
1495
+
1430
1496
  function setTranscriptMessageContent(item,content,timestamp){
1431
1497
  if(!is.string(content)){
1432
1498
  throw new TypeError('Chat session message content must be a string.');
1433
1499
  }
1434
1500
  item.querySelector('.thinking')?.remove();
1435
1501
  const markdown=item.querySelector('.markdown');
1436
- markdown.raw=content;
1437
- markdown.innerHTML=new MD(content).rendered;
1502
+ renderTranscriptMarkdown(markdown,content);
1438
1503
  if(timestamp!==undefined){
1439
1504
  item.querySelector('.message_timestamp')?.replaceWith(
1440
1505
  transcriptTime(timestamp)
@@ -1495,12 +1560,11 @@
1495
1560
  header.append(heading);
1496
1561
  const markdown=chatOutput.ownerDocument.createElement('div');
1497
1562
  markdown.className='markdown';
1498
- markdown.raw=content;
1499
- markdown.innerHTML=new MD(content).rendered;
1500
1563
  item.append(header,markdown,transcriptTime(timestamp));
1501
1564
  for(const call of normalizeVisibleToolCalls(toolCalls)){
1502
1565
  appendVisibleToolCall(item,call);
1503
1566
  }
1567
+ renderTranscriptMarkdown(markdown,content);
1504
1568
  return item;
1505
1569
  }
1506
1570
 
@@ -1642,6 +1706,9 @@
1642
1706
  setPendingStructuralToolCalls(
1643
1707
  actionable?[...restoredPendingToolCalls.values()]:[]
1644
1708
  );
1709
+ for(const item of chatOutput.children){
1710
+ releaseTranscriptMessageMedia(item);
1711
+ }
1645
1712
  chatOutput.replaceChildren(fragment);
1646
1713
  transcriptFollowing = true;
1647
1714
  transcriptScrollTop = chatOutput.scrollTop;
@@ -3034,9 +3101,7 @@
3034
3101
  thinkingHTML.remove();
3035
3102
  }
3036
3103
 
3037
- target.raw ? target.raw+=text : target.raw=text;
3038
-
3039
- target.innerHTML=new MD(target.raw).rendered;
3104
+ renderTranscriptMarkdown(target,target.raw?target.raw+text:text);
3040
3105
 
3041
3106
  scrollTranscriptToBottom();
3042
3107
  }
@@ -3393,8 +3458,7 @@
3393
3458
  message.querySelector('.thinking')?.remove();
3394
3459
  const markdown=message.querySelector('.markdown');
3395
3460
  const text=visibleErrorMessage(error,'The AI request failed.');
3396
- markdown.raw=text;
3397
- markdown.innerHTML=new MD(text).rendered;
3461
+ renderTranscriptMarkdown(markdown,text);
3398
3462
  scrollTranscriptToBottom();
3399
3463
  return true;
3400
3464
  }
@@ -3421,8 +3485,8 @@
3421
3485
  const response=[...chatOutput.children].find(
3422
3486
  candidate=>candidate.id===`message-${messageId}`
3423
3487
  )||null;
3424
- response?.remove();
3425
- for(const request of requestMessages) request.remove();
3488
+ removeTranscriptMessage(response);
3489
+ for(const request of requestMessages) removeTranscriptMessage(request);
3426
3490
  const existing=textArea.value;
3427
3491
  if(existing!==text){
3428
3492
  textArea.value=existing
@@ -3500,7 +3564,7 @@
3500
3564
  function removeTransientSessionCards(){
3501
3565
  if(retainTurn||!transientCards.length) return;
3502
3566
  // The submitter supplies only nodes created for this submission.
3503
- for(const card of transientCards) card.remove();
3567
+ for(const card of transientCards) removeTranscriptMessage(card);
3504
3568
  transientCards.length=0;
3505
3569
  scrollTranscriptToBottom();
3506
3570
  }
@@ -3675,9 +3739,9 @@
3675
3739
  const response=[...chatOutput.children].find(
3676
3740
  candidate=>candidate.id===`message-${messageId}`
3677
3741
  )||null;
3678
- response?.remove();
3742
+ removeTranscriptMessage(response);
3679
3743
  for(const requestMessage of requestMessages){
3680
- requestMessage.remove();
3744
+ removeTranscriptMessage(requestMessage);
3681
3745
  }
3682
3746
  scrollTranscriptToBottom();
3683
3747
  }else if(retainTurn){
@@ -3692,7 +3756,7 @@
3692
3756
  arcaneLogging.error('Arcane chat request failed.',error);
3693
3757
  if(sessionRequestMessages.every(message=>message.role==='tool')){
3694
3758
  for(const requestMessage of requestMessages){
3695
- requestMessage.remove();
3759
+ removeTranscriptMessage(requestMessage);
3696
3760
  }
3697
3761
  }
3698
3762
  if(retainTurn) renderSessionMessageFailure(messageId,error);
@@ -4136,11 +4200,64 @@
4136
4200
  return message;
4137
4201
  }
4138
4202
 
4203
+ function renderedChatPrintContent(){
4204
+ for(const media of transcriptMedia.values()){
4205
+ if(media.pending){
4206
+ const error=new Error('Saved images are still loading. Print again when they finish.');
4207
+ error.code='ARCANE_MARKDOWN_MEDIA_PENDING';
4208
+ throw error;
4209
+ }
4210
+ if(media.error) throw media.error;
4211
+ }
4212
+ return chatOutput;
4213
+ }
4214
+
4215
+ function retainChatPrintMedia(){
4216
+ renderedChatPrintContent();
4217
+ const releases=new Set();
4218
+ for(const media of transcriptMedia.values()){
4219
+ releases.add(media.handle.retain());
4220
+ }
4221
+ printMediaRetains=releases;
4222
+ return function releaseChatPrintMedia(){
4223
+ if(printMediaRetains===releases){
4224
+ printMediaRetains=null;
4225
+ }
4226
+ for(const release of releases) release();
4227
+ releases.clear();
4228
+ };
4229
+ }
4230
+
4231
+ async function prepareChatPrintMedia(signal){
4232
+ let cancel;
4233
+ const cancelled=new Promise(function waitForChatPrintCancellation(resolve){cancel=resolve;});
4234
+ signal?.addEventListener('abort',cancel,{once:true});
4235
+ try{
4236
+ while(!destroyed&&!signal?.aborted){
4237
+ const selected=[...transcriptMedia];
4238
+ await Promise.race([
4239
+ Promise.all(selected.map(function readyChatMedia([,media]){return media.ready;})),
4240
+ cancelled
4241
+ ]);
4242
+ if(destroyed||signal?.aborted) return false;
4243
+ if(selected.length!==transcriptMedia.size||selected.some(
4244
+ function replacedChatMedia([target,media]){return transcriptMedia.get(target)!==media;}
4245
+ )) continue;
4246
+ renderedChatPrintContent();
4247
+ return true;
4248
+ }
4249
+ return false;
4250
+ }finally{
4251
+ signal?.removeEventListener('abort',cancel);
4252
+ }
4253
+ }
4254
+
4139
4255
  async function print(){
4140
4256
  if(destroyed) return false;
4141
4257
  try{
4142
4258
  const view=await printViewPromise;
4143
- return destroyed||!view?false:await view.print();
4259
+ if(destroyed||!view||!await prepareChatPrintMedia()) return false;
4260
+ return await view.print();
4144
4261
  }catch(error){
4145
4262
  reportChatPrintFailure(error);
4146
4263
  throw error;
@@ -4150,7 +4267,9 @@
4150
4267
  function reportChatPrintFailure(error){
4151
4268
  arcaneLogging.error('Unable to print the conversation.',error);
4152
4269
  if(!destroyed){
4153
- setSessionStatus('error','Unable to open print preview. Please try again.');
4270
+ setSessionStatus('error',error?.code==='ARCANE_MARKDOWN_MEDIA_PENDING'
4271
+ ?'Saved images are still loading. Print again when they finish.'
4272
+ :'Unable to open print preview. Please try again.');
4154
4273
  }
4155
4274
  }
4156
4275
 
@@ -4235,6 +4354,9 @@
4235
4354
  ownership.abort(destroyReason);
4236
4355
  }
4237
4356
  aiRuntimeStateAbortController.abort(destroyReason);
4357
+ for(const target of transcriptMedia.keys()){
4358
+ releaseTranscriptMarkdown(target);
4359
+ }
4238
4360
  aiActivationController.destroy();
4239
4361
  window.removeEventListener('pagehide',handlePageHide);
4240
4362
  window.removeEventListener('pageshow',handlePageShow);
@@ -5018,6 +5018,7 @@ class AI {
5018
5018
  model,
5019
5019
  structuredOutput=false,
5020
5020
  localOnly=false,
5021
+ speech=true,
5021
5022
  onChunk=function ignoreStreamChunk(){},
5022
5023
  toolText,
5023
5024
  onToolText,
@@ -5062,6 +5063,9 @@ class AI {
5062
5063
  if(localOnly!==true&&localOnly!==false){
5063
5064
  throw new TypeError('AI localOnly must be a boolean.');
5064
5065
  }
5066
+ if(speech!==true&&speech!==false){
5067
+ throw new TypeError('AI speech must be a boolean.');
5068
+ }
5065
5069
  if(localOnly&&!this.#hasLocalRoute('llm',this.llmService)){
5066
5070
  const error=new Error(
5067
5071
  'This AI request requires a configured local model.'
@@ -5163,7 +5167,7 @@ class AI {
5163
5167
  if(signal?.aborted){
5164
5168
  throw normalizeAIRequestAbort();
5165
5169
  }
5166
- this.#finishStreamingSpeech();
5170
+ if(speech)this.#finishStreamingSpeech();
5167
5171
  return result;
5168
5172
  }catch(error){
5169
5173
  if(handle){
@@ -5171,7 +5175,7 @@ class AI {
5171
5175
  function retainProviderStreamCleanupFailure() {}
5172
5176
  );
5173
5177
  }
5174
- this.stopAudio();
5178
+ if(speech)this.stopAudio();
5175
5179
  throw isAIRequestAbort(error,signal)
5176
5180
  ?normalizeAIRequestAbort(error)
5177
5181
  :error;
@@ -5223,10 +5227,10 @@ class AI {
5223
5227
  if(signal?.aborted){
5224
5228
  throw normalizeAIRequestAbort();
5225
5229
  }
5226
- this.#finishStreamingSpeech();
5230
+ if(speech)this.#finishStreamingSpeech();
5227
5231
  return result;
5228
5232
  }catch(error){
5229
- this.stopAudio();
5233
+ if(speech)this.stopAudio();
5230
5234
  throw isAIRequestAbort(error,signal)
5231
5235
  ?normalizeAIRequestAbort(error)
5232
5236
  :error;
@@ -6026,7 +6030,8 @@ class AI {
6026
6030
  }
6027
6031
  throw error;
6028
6032
  }finally{
6029
- if(!speechTurnCompleted){
6033
+ // Provider transports leave speech lifecycle to their public caller.
6034
+ if(finishSpeech&&!speechTurnCompleted){
6030
6035
  this.stopAudio();
6031
6036
  }
6032
6037
  }
@@ -0,0 +1,106 @@
1
+ import {readFile, writeFile} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {connectAppControl} from '../core/app-control.mjs';
4
+ import {CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
5
+
6
+ const HELP_TEXT=`Usage:
7
+ arcane app-control status --endpoint <pipe-or-socket>
8
+ arcane app-control inspect --endpoint <pipe-or-socket> [--selector <css-selector>]
9
+ arcane app-control capture --endpoint <pipe-or-socket> --output <png-file>
10
+ arcane app-control act --endpoint <pipe-or-socket> --request <json-file>
11
+
12
+ Connects to the selected running native window. Results are complete JSON.
13
+ Capture writes the PNG to --output and prints its metadata. Request and output
14
+ filenames are relative to the current directory. This command does not launch
15
+ an application or change its profile. --output selects the capture filename;
16
+ the other Arcane commands retain their existing output-format option.
17
+ `;
18
+
19
+ function usage(message) {
20
+ throw new CoreError({code: 'APP_CONTROL_USAGE', message, exitCode: 2});
21
+ }
22
+
23
+ function parseArguments(argv) {
24
+ if (!argv.length || argv.includes('--help') || argv.includes('-h')) return {help: true};
25
+ const [action] = argv;
26
+ const allowed = {
27
+ status: ['endpoint'],
28
+ inspect: ['endpoint', 'selector'],
29
+ capture: ['endpoint', 'output'],
30
+ act: ['endpoint', 'request']
31
+ }[action];
32
+ if (!Array.isArray(allowed)) usage('Expected app-control status, inspect, capture, or act.');
33
+ const values = {};
34
+ for (let index = 1; index < argv.length; index += 1) {
35
+ const argument = argv[index];
36
+ const equal = argument.indexOf('=');
37
+ const option = equal === -1 ? argument : argument.slice(0, equal);
38
+ if (!option.startsWith('--') || !allowed.includes(option.slice(2))) {
39
+ usage(`Unexpected argument for app-control ${action}: ${argument}`);
40
+ }
41
+ const value = equal === -1 ? argv[++index] : argument.slice(equal + 1);
42
+ if (value === undefined || value === '' || (equal === -1 && value.startsWith('--'))) {
43
+ usage(`${option} requires a value.`);
44
+ }
45
+ values[option.slice(2)] = value;
46
+ }
47
+ if (!values.endpoint) usage('App control requires --endpoint <pipe-or-socket>.');
48
+ if (action === 'capture' && !values.output) usage('App-control capture requires --output <png-file>.');
49
+ if (action === 'act' && !values.request) usage('App-control act requires --request <json-file>.');
50
+ return {action, ...values};
51
+ }
52
+
53
+ /** This command owns its filename option before the general output-mode parser. */
54
+ export async function runAppControlCli(argv, {cwd = process.cwd(), stdout = process.stdout,
55
+ stderr = process.stderr, controller = new AbortController()} = {}) {
56
+ let connection;
57
+ let result;
58
+ const failures = [];
59
+ function failed(error) {
60
+ if (!failures.includes(error)) failures.push(error);
61
+ }
62
+ function interrupted() { controller.abort(new Error('Interrupted')); }
63
+ process.once('SIGINT', interrupted);
64
+ process.once('SIGTERM', interrupted);
65
+ try {
66
+ const selection = parseArguments(argv);
67
+ if (selection.help) {
68
+ stdout.write(HELP_TEXT);
69
+ return 0;
70
+ }
71
+ stderr.write(`${JSON.stringify({status: 'running', operation: selection.action,
72
+ endpoint: selection.endpoint})}\n`);
73
+ const signal = controller.signal;
74
+ const parameters = selection.action === 'act'
75
+ ? JSON.parse(await readFile(path.resolve(cwd, selection.request), {encoding: 'utf8', signal}))
76
+ : selection.selector === undefined ? {} : {selector: selection.selector};
77
+ connection = await connectAppControl({endpoint: selection.endpoint, signal, onError: failed});
78
+ if (selection.action === 'status') result = await connection.status({signal});
79
+ else if (selection.action === 'inspect') result = await connection.inspect(parameters, {signal});
80
+ else if (selection.action === 'act') result = await connection.act(parameters, {signal});
81
+ else {
82
+ result = await connection.capture(parameters, {signal});
83
+ const output = path.resolve(cwd, selection.output);
84
+ await writeFile(output, Buffer.from(result.data, 'base64'), {signal});
85
+ const {data, ...metadata} = result;
86
+ result = {...metadata, output};
87
+ }
88
+ } catch (error) {
89
+ failed(error);
90
+ } finally {
91
+ if (connection) {
92
+ try { await connection.close(); } catch (error) { failed(error); }
93
+ }
94
+ process.removeListener('SIGINT', interrupted);
95
+ process.removeListener('SIGTERM', interrupted);
96
+ }
97
+ if (failures.length) {
98
+ const error = failures.length === 1 ? failures[0]
99
+ : new AggregateError(failures, 'App control completed with errors.');
100
+ stderr.write(`${JSON.stringify({...(result === undefined ? {} : {result}),
101
+ error: serializeCoreError(error)}, null, 2)}\n`);
102
+ return controller.signal.aborted ? 130 : error.exitCode ?? 1;
103
+ }
104
+ stdout.write(`${JSON.stringify(result, null, 2)}\n`);
105
+ return 0;
106
+ }
package/src/cli/main.mjs CHANGED
@@ -6,6 +6,7 @@ import {ArcaneError,ERROR_CODES,normalizeError} from '../errors.mjs';
6
6
  import {CLI_NAME,SDK_NAME,SDK_VERSION,OUTPUT_MODES} from '../constants.mjs';
7
7
  import {loadArcaneNativeProvider} from '../native-provider-loader.mjs';
8
8
  import {APP_BUNDLE_EXTENSION} from '../release-bundle.mjs';
9
+ import {runAppControlCli} from './app-control.mjs';
9
10
 
10
11
  const is = new Is(false);
11
12
 
@@ -82,6 +83,7 @@ Usage:
82
83
  ${CLI_NAME} run [--target <target>] [--app <id>] [--cert <pem>] [--key <pem>] [--port 8000] [--http-port 0] [--arcane-root <directory>] [--output-root <directory>] [--format <format>] [--signing <mode>]
83
84
  ${CLI_NAME} update-check
84
85
  ${CLI_NAME} targets
86
+ ${CLI_NAME} app-control status|inspect|capture|act --endpoint <pipe-or-socket> [--help]
85
87
  ${CLI_NAME} repo status|pull|push
86
88
  ${CLI_NAME} mail key set [profile] [--secret-stdin]
87
89
  ${CLI_NAME} mail key status [profile]
@@ -942,6 +944,9 @@ export async function runCli(argv=process.argv.slice(2),{
942
944
  loadNativeProvider=loadArcaneNativeProvider,
943
945
  controller=new AbortController()
944
946
  }={}){
947
+ if(argv[0]==='app-control'){
948
+ return runAppControlCli(argv.slice(1),{cwd,stdout,stderr,controller});
949
+ }
945
950
  let reporter;
946
951
  let command='help';
947
952
  const onSignal=()=>controller.abort(new Error('Interrupted'));
@@ -0,0 +1,23 @@
1
+ import {connectCoreSocket, createCoreSocketConnection} from './socket-connection.mjs';
2
+
3
+ function reportAppControlError(error) { console.error('Arcane app control failed:', error); }
4
+
5
+ /** Connect to one explicitly selected running native window without launching it. */
6
+ export async function connectAppControl({endpoint, signal, onError = reportAppControlError} = {}) {
7
+ if (typeof endpoint !== 'string' || !endpoint) {
8
+ throw new TypeError('App control requires an explicit local pipe/socket endpoint.');
9
+ }
10
+ const socket = await connectCoreSocket(endpoint, signal, onError);
11
+ const connection = createCoreSocketConnection({socket, endpoint, signal, onError,
12
+ name: 'app-control', closedMessage: 'The app-control connection closed.'});
13
+ const {client} = connection;
14
+ return {
15
+ endpoint,
16
+ closed: connection.closed,
17
+ status(options) { return client.invoke('app.control.status', {}, options); },
18
+ inspect(parameters = {}, options) { return client.invoke('app.control.inspect', parameters, options); },
19
+ capture(parameters = {}, options) { return client.invoke('app.control.capture', parameters, options); },
20
+ act(parameters, options) { return client.invoke('app.control.act', parameters, options); },
21
+ close() { return connection.close(); }
22
+ };
23
+ }