@midscene/cli 1.14.1-beta-20261008033312.0 → 1.15.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/dist/es/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { basename, dirname as external_node_path_dirname, extname, join, posix,
5
5
  import { interpolateEnvVars, midsceneYamlTargetKeys, parseYamlScript as yaml_parseYamlScript, resolveWebTarget } from "@midscene/core/yaml";
6
6
  import { getDebug } from "@midscene/shared/logger";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { lstat, readdir, readlink, realpath } from "node:fs/promises";
8
+ import { lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, writeFile } from "node:fs/promises";
9
9
  import { EventEmitter } from "node:events";
10
10
  import node_stream from "node:stream";
11
11
  import { StringDecoder } from "node:string_decoder";
@@ -13,7 +13,7 @@ import { fileURLToPath as external_url_fileURLToPath } from "url";
13
13
  import node_process, { cwd as external_node_process_cwd } from "node:process";
14
14
  import lodash_merge from "lodash.merge";
15
15
  import { loadDotenvConfig, runModelCommand } from "@midscene/test/internal/model-command";
16
- import { createHash } from "node:crypto";
16
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
17
17
  import { getMidsceneRunSubDir } from "@midscene/shared/common";
18
18
  import { buildChromeArgs, buildDownloadBehavior, defaultViewportHeight, defaultViewportWidth } from "@midscene/web/puppeteer-agent-launcher";
19
19
  import puppeteer from "puppeteer";
@@ -21,6 +21,10 @@ import { createYamlPlayer as yaml_runtime_createYamlPlayer } from "@midscene/tes
21
21
  import { stripVTControlCharacters } from "node:util";
22
22
  import { createRequire } from "node:module";
23
23
  import { serializeError } from "@midscene/shared/agent-tools/error-formatter";
24
+ import { spawn } from "node:child_process";
25
+ import { homedir } from "node:os";
26
+ import { createServer } from "node:http";
27
+ import { createRemoteJWKSet, jwtVerify } from "jose";
24
28
  import { classifyWorkflowDocumentFormat } from "@midscene/core/internal/test-runner";
25
29
  import * as __rspack_external_assert from "assert";
26
30
  import * as __rspack_external_fs from "fs";
@@ -2796,7 +2800,9 @@ var __webpack_modules__ = {
2796
2800
  module.exports = (string, columns, options)=>String(string).normalize().replace(/\r\n/g, '\n').split('\n').map((line)=>exec(line, columns, options)).join('\n');
2797
2801
  },
2798
2802
  "./src/index.ts" (__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
2799
- var package_namespaceObject = JSON.parse('{"rE":"1.14.1-beta-20261008033312.0"}');
2803
+ var package_namespaceObject = {
2804
+ rE: "1.15.0"
2805
+ };
2800
2806
  var brace_expansion = __webpack_require__("../../node_modules/.pnpm/brace-expansion@2.0.1/node_modules/brace-expansion/index.js");
2801
2807
  const MAX_PATTERN_LENGTH = 65536;
2802
2808
  const assertValidPattern = (pattern)=>{
@@ -8407,6 +8413,7 @@ Homepage: https://midscenejs.com
8407
8413
  Github: https://github.com/web-infra-dev/midscene
8408
8414
 
8409
8415
  Usage:
8416
+ $0 model siwc <login|refresh> [options]
8410
8417
  $0 [options] <path-to-yaml-script-file-or-directory>
8411
8418
  $0 [options] --files <yaml-file1> <yaml-file2 ...>
8412
8419
  $0 [options] --config <path-to-config-yaml-file>
@@ -8468,7 +8475,7 @@ Usage:
8468
8475
  type: 'boolean',
8469
8476
  description: 'Whether to dismiss the HarmonyOS keyboard after text input'
8470
8477
  }
8471
- }).version('version', 'Show version number', "1.14.1-beta-20261008033312.0").help().epilogue(`For complete list of configuration options, please visit:
8478
+ }).version('version', 'Show version number', "1.15.0").help().epilogue(`For complete list of configuration options, please visit:
8472
8479
  • Web options: https://midscenejs.com/automate-with-scripts-in-yaml#the-web-part
8473
8480
  • Android options: https://midscenejs.com/automate-with-scripts-in-yaml#the-android-part
8474
8481
  • HarmonyOS options: https://midscenejs.com/automate-with-scripts-in-yaml#the-harmony-part
@@ -12483,17 +12490,431 @@ defineYamlBatchTest(test, testOptions);
12483
12490
  const success = printExecutionSummary(results, summaryPath);
12484
12491
  return success ? exitCode : 1;
12485
12492
  }
12493
+ const defaultCredentialsPath = ()=>join(homedir(), '.midscene', 'siwc.json');
12494
+ async function readCredentialsFile(path) {
12495
+ const content = await readFile(path, 'utf8').catch((error)=>{
12496
+ if ('ENOENT' === error.code) return;
12497
+ throw new Error('Unable to read the local SIWC credentials file.');
12498
+ });
12499
+ if (void 0 === content) return;
12500
+ try {
12501
+ const value = JSON.parse(content);
12502
+ if (!value || 'string' != typeof value.credentials?.ext_agent_host_id || !value.credentials.ext_agent_host_id) throw new Error('Invalid SIWC credentials record.');
12503
+ return value;
12504
+ } catch {
12505
+ throw new Error('Invalid local SIWC credentials file.');
12506
+ }
12507
+ }
12508
+ async function readRegistration(path) {
12509
+ return (await readCredentialsFile(path))?.credentials;
12510
+ }
12511
+ async function saveRegistration(path, value) {
12512
+ const saved = await readCredentialsFile(path);
12513
+ const now = new Date().toISOString();
12514
+ const record = {
12515
+ ...saved,
12516
+ created_at: saved?.created_at ?? now,
12517
+ updated_at: now,
12518
+ credentials: value
12519
+ };
12520
+ const temporary = `${path}.${randomUUID()}.tmp`;
12521
+ try {
12522
+ await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, {
12523
+ mode: 384,
12524
+ flag: 'wx'
12525
+ });
12526
+ await rename(temporary, path);
12527
+ } finally{
12528
+ await rm(temporary, {
12529
+ force: true
12530
+ });
12531
+ }
12532
+ }
12533
+ async function lockCredentials(path) {
12534
+ await mkdir(external_node_path_dirname(path), {
12535
+ recursive: true,
12536
+ mode: 448
12537
+ });
12538
+ const lockPath = `${path}.lock`;
12539
+ try {
12540
+ await mkdir(lockPath, {
12541
+ mode: 448
12542
+ });
12543
+ } catch (error) {
12544
+ if ('EEXIST' === error.code) throw new Error(`Another SIWC command holds ${lockPath}. If it was interrupted, remove this lock only after confirming that no SIWC command is running.`);
12545
+ throw new Error('Unable to lock the local SIWC credentials file.');
12546
+ }
12547
+ return ()=>rm(lockPath, {
12548
+ recursive: true,
12549
+ force: true
12550
+ });
12551
+ }
12552
+ function requireCredentials(value) {
12553
+ if (!value || [
12554
+ 'client_id',
12555
+ 'subject',
12556
+ 'issuer',
12557
+ 'access_token',
12558
+ 'refresh_token',
12559
+ 'id_token'
12560
+ ].some((key)=>'string' != typeof value[key] || !value[key]) || !Array.isArray(value.scopes) || !value.scopes.every((scope)=>'string' == typeof scope) || !Number.isFinite(value.expires_at)) throw new Error('No valid SIWC credentials. Run midscene model siwc login first.');
12561
+ return value;
12562
+ }
12563
+ const ISSUER = 'https://auth.openai.com';
12564
+ const RESOURCE = 'https://api.openai.com/v1';
12565
+ const SCOPES = 'openid profile email offline_access resource.invoke chatgpt.tokens.use.direct';
12566
+ async function loginWithOpenAI(options, dependencies) {
12567
+ const hostId = options.hostId ?? `urn:uuid:${randomUUID()}`;
12568
+ const existingClientId = options.clientId;
12569
+ const state = randomBytes(32).toString('base64url');
12570
+ const nonce = randomBytes(32).toString('base64url');
12571
+ const verifier = randomBytes(32).toString('base64url');
12572
+ const signal = options.signal;
12573
+ signal.throwIfAborted();
12574
+ const server = createServer();
12575
+ const callback = new Promise((resolve, reject)=>{
12576
+ const consumed = {
12577
+ value: false
12578
+ };
12579
+ server.on('request', (request, response)=>{
12580
+ response.setHeader('Content-Type', 'text/plain; charset=utf-8');
12581
+ response.setHeader('Cache-Control', 'no-store');
12582
+ response.setHeader('Referrer-Policy', 'no-referrer');
12583
+ const url = new URL(request.url || '/', 'http://127.0.0.1');
12584
+ if ('GET' !== request.method || '/auth/callback' !== url.pathname) return void response.writeHead(404).end('Not found');
12585
+ if (1 !== url.searchParams.getAll('state').length || url.searchParams.get('state') !== state) return void response.writeHead(400).end('Invalid login state. Use the original authorization link.');
12586
+ if (consumed.value) return void response.writeHead(409).end('Authorization already received.');
12587
+ if (url.searchParams.has('error')) {
12588
+ consumed.value = true;
12589
+ response.writeHead(400).end('Authorization was not granted. Return to the terminal.');
12590
+ reject(new Error('OpenAI authorization was denied or failed.'));
12591
+ return;
12592
+ }
12593
+ const code = url.searchParams.get('code');
12594
+ const clientId = url.searchParams.get('client_id') || existingClientId;
12595
+ if (!code || !clientId || 'dynamic_agent_client' === clientId || existingClientId && clientId !== existingClientId || 1 !== url.searchParams.getAll('code').length || url.searchParams.getAll('client_id').length > 1) return void response.writeHead(400).end('Invalid authorization callback.');
12596
+ consumed.value = true;
12597
+ response.setHeader('Content-Type', 'text/html; charset=utf-8');
12598
+ response.end(`<!doctype html>
12599
+ <html lang="en">
12600
+ <head>
12601
+ <meta charset="utf-8">
12602
+ <meta name="viewport" content="width=device-width, initial-scale=1">
12603
+ <title>Authorization received · Midscene</title>
12604
+ <style>
12605
+ * { box-sizing: border-box; }
12606
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; background: #f6f7f9; color: #202124; font-family: system-ui, sans-serif; }
12607
+ main { width: 100%; max-width: 480px; padding: 40px 28px; text-align: center; background: white; border: 1px solid #e5e7eb; border-radius: 20px; box-shadow: 0 8px 32px #00000008; }
12608
+ h1 { margin: 0 0 20px; font-size: 24px; line-height: 1.4; }
12609
+ h1 span { color: #16834a; }
12610
+ p { margin: 8px 0 0; color: #596273; font-size: 15px; line-height: 1.6; }
12611
+ </style>
12612
+ </head>
12613
+ <body>
12614
+ <main>
12615
+ <h1><span aria-hidden="true">✓</span> Authorization received</h1>
12616
+ <p>You can close this tab.</p>
12617
+ <p>Check your terminal for the sign-in result and access token.</p>
12618
+ </main>
12619
+ </body>
12620
+ </html>`);
12621
+ resolve({
12622
+ code,
12623
+ clientId
12624
+ });
12625
+ });
12626
+ server.on('error', reject);
12627
+ });
12628
+ callback.catch(()=>void 0);
12629
+ const abort = ()=>server.emit('error', new Error('OpenAI login cancelled or timed out.'));
12630
+ signal.addEventListener('abort', abort, {
12631
+ once: true
12632
+ });
12633
+ try {
12634
+ await new Promise((resolve, reject)=>{
12635
+ server.once('error', reject);
12636
+ server.listen(options.port ?? 0, '127.0.0.1', ()=>{
12637
+ server.removeListener('error', reject);
12638
+ resolve();
12639
+ });
12640
+ });
12641
+ signal.throwIfAborted();
12642
+ const redirectUri = `http://127.0.0.1:${server.address().port}/auth/callback`;
12643
+ const authorization = new URL(`${ISSUER}/api/accounts/authorize`);
12644
+ authorization.search = new URLSearchParams({
12645
+ client_id: existingClientId ?? 'dynamic_agent_client',
12646
+ ...existingClientId ? {} : {
12647
+ agent_name_hint: 'Midscene'
12648
+ },
12649
+ ext_agent_host_id: hostId,
12650
+ response_type: 'code',
12651
+ redirect_uri: redirectUri,
12652
+ scope: SCOPES,
12653
+ resource: RESOURCE,
12654
+ state,
12655
+ nonce,
12656
+ code_challenge_method: 'S256',
12657
+ code_challenge: createHash('sha256').update(verifier).digest('base64url')
12658
+ }).toString();
12659
+ await options.onAuthorization(authorization.toString());
12660
+ const { code, clientId } = await callback;
12661
+ signal.throwIfAborted();
12662
+ const request = dependencies?.fetch ?? fetch;
12663
+ const result = await request(`${ISSUER}/api/accounts/oauth/token`, {
12664
+ method: 'POST',
12665
+ headers: {
12666
+ 'Content-Type': 'application/x-www-form-urlencoded'
12667
+ },
12668
+ body: new URLSearchParams({
12669
+ grant_type: 'authorization_code',
12670
+ client_id: clientId,
12671
+ code,
12672
+ code_verifier: verifier,
12673
+ redirect_uri: redirectUri,
12674
+ resource: RESOURCE
12675
+ }),
12676
+ signal,
12677
+ redirect: 'error'
12678
+ });
12679
+ if (!result.ok) throw new Error(`OpenAI token exchange failed (HTTP ${result.status}). Run midscene model siwc login again.`);
12680
+ const tokens = await result.json();
12681
+ if (!tokens || 'string' != typeof tokens.access_token || !tokens.access_token || 'string' != typeof tokens.refresh_token || !tokens.refresh_token || 'string' != typeof tokens.id_token || 'string' != typeof tokens.scope || 'number' != typeof tokens.expires_in || !Number.isFinite(tokens.expires_in) || tokens.expires_in <= 0 || 'string' != typeof tokens.token_type || 'bearer' !== tokens.token_type.toLowerCase()) throw new Error('OpenAI returned an invalid token response.');
12682
+ const scopes = tokens.scope.split(/\s+/);
12683
+ if (!scopes.includes('chatgpt.tokens.use.direct')) throw new Error('ChatGPT plan usage was not authorized. Run midscene model siwc login again and grant access.');
12684
+ const keys = dependencies?.keys ?? createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`));
12685
+ const { payload } = await jwtVerify(tokens.id_token, keys, {
12686
+ issuer: ISSUER,
12687
+ audience: clientId,
12688
+ algorithms: [
12689
+ 'RS256'
12690
+ ],
12691
+ requiredClaims: [
12692
+ 'sub',
12693
+ 'exp',
12694
+ 'iat',
12695
+ 'nonce'
12696
+ ]
12697
+ });
12698
+ if (payload.nonce !== nonce || !payload.sub) throw new Error('OpenAI login identity or nonce did not match.');
12699
+ signal.throwIfAborted();
12700
+ return {
12701
+ ext_agent_host_id: hostId,
12702
+ client_id: clientId,
12703
+ subject: payload.sub,
12704
+ issuer: ISSUER,
12705
+ ...'string' == typeof payload.email ? {
12706
+ email: payload.email
12707
+ } : {},
12708
+ access_token: tokens.access_token,
12709
+ refresh_token: tokens.refresh_token,
12710
+ id_token: tokens.id_token,
12711
+ scopes,
12712
+ expires_at: Date.now() + 1000 * tokens.expires_in
12713
+ };
12714
+ } finally{
12715
+ signal.removeEventListener('abort', abort);
12716
+ server.closeAllConnections();
12717
+ await new Promise((resolve)=>server.close(()=>resolve()));
12718
+ }
12719
+ }
12720
+ async function refreshOpenAICredentials(saved, signal, dependencies) {
12721
+ const issuer = 'https://auth.openai.com';
12722
+ signal.throwIfAborted();
12723
+ const response = await (dependencies?.fetch ?? fetch)(`${issuer}/api/accounts/oauth/token`, {
12724
+ method: 'POST',
12725
+ headers: {
12726
+ 'Content-Type': 'application/x-www-form-urlencoded'
12727
+ },
12728
+ body: new URLSearchParams({
12729
+ grant_type: 'refresh_token',
12730
+ client_id: saved.client_id,
12731
+ refresh_token: saved.refresh_token,
12732
+ resource: 'https://api.openai.com/v1'
12733
+ }),
12734
+ signal,
12735
+ redirect: 'error'
12736
+ });
12737
+ if (!response.ok) throw new Error('OpenAI refresh failed.');
12738
+ const tokens = await response.json();
12739
+ if (!tokens || 'string' != typeof tokens.access_token || !tokens.access_token || 'string' != typeof tokens.refresh_token || !tokens.refresh_token || 'number' != typeof tokens.expires_in || !Number.isFinite(tokens.expires_in) || tokens.expires_in <= 0 || 'string' != typeof tokens.token_type || 'bearer' !== tokens.token_type.toLowerCase() || 'string' != typeof tokens.scope) throw new Error('OpenAI returned an invalid refresh response.');
12740
+ const scopes = tokens.scope.split(/\s+/);
12741
+ if (!scopes.includes('chatgpt.tokens.use.direct')) throw new Error('ChatGPT plan usage is no longer authorized.');
12742
+ if (void 0 !== tokens.id_token) {
12743
+ if ('string' != typeof tokens.id_token || !tokens.id_token) throw new Error('Invalid refreshed ID token.');
12744
+ const keys = dependencies?.keys ?? createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));
12745
+ const { payload } = await jwtVerify(tokens.id_token, keys, {
12746
+ issuer,
12747
+ audience: saved.client_id,
12748
+ algorithms: [
12749
+ 'RS256'
12750
+ ],
12751
+ requiredClaims: [
12752
+ 'sub',
12753
+ 'exp',
12754
+ 'iat'
12755
+ ]
12756
+ });
12757
+ if (payload.sub !== saved.subject) throw new Error('Refreshed identity does not match the saved account.');
12758
+ }
12759
+ signal.throwIfAborted();
12760
+ return {
12761
+ ...saved,
12762
+ access_token: tokens.access_token,
12763
+ refresh_token: tokens.refresh_token,
12764
+ id_token: tokens.id_token ?? saved.id_token,
12765
+ scopes,
12766
+ expires_at: Date.now() + 1000 * tokens.expires_in
12767
+ };
12768
+ }
12769
+ const USAGE = `Usage:
12770
+ midscene model siwc login [--no-open] [--port <port>]
12771
+ midscene model siwc refresh
12772
+
12773
+ Sign in with ChatGPT or refresh the saved credentials.
12774
+ Full credentials are saved to ~/.midscene/siwc.json. Only the access token is printed to stdout.
12775
+ Midscene does not collect your credentials.
12776
+
12777
+ Options (login):
12778
+ --no-open Print the authorization URL without opening a browser
12779
+ --port <port> Loopback callback port (default: an available port)
12780
+ --help, -h Show this help
12781
+ `;
12782
+ async function openBrowser(url) {
12783
+ const command = 'darwin' === process.platform ? 'open' : 'win32' === process.platform ? 'rundll32' : 'xdg-open';
12784
+ const args = 'win32' === process.platform ? [
12785
+ 'url.dll,FileProtocolHandler',
12786
+ url
12787
+ ] : [
12788
+ url
12789
+ ];
12790
+ await new Promise((resolve, reject)=>{
12791
+ const child = spawn(command, args, {
12792
+ stdio: 'ignore',
12793
+ shell: false
12794
+ });
12795
+ child.once('error', reject);
12796
+ child.once('spawn', ()=>{
12797
+ child.unref();
12798
+ resolve();
12799
+ });
12800
+ });
12801
+ }
12802
+ async function runSiwcCommand(args, io = {
12803
+ stdout: console.log,
12804
+ stderr: console.error
12805
+ }, credentialsPath = defaultCredentialsPath()) {
12806
+ if (!args.length || args.includes('--help') || args.includes('-h')) {
12807
+ io.stdout(USAGE);
12808
+ return 0;
12809
+ }
12810
+ const [action, ...options] = args;
12811
+ if ('login' !== action && 'refresh' !== action) {
12812
+ io.stderr(`Unknown SIWC command: ${action}\n${USAGE}`);
12813
+ return 1;
12814
+ }
12815
+ const portOptions = {
12816
+ port: 0,
12817
+ noOpen: false
12818
+ };
12819
+ const seen = new Set();
12820
+ for(let index = 0; index < options.length; index++){
12821
+ const option = options[index];
12822
+ if ('login' !== action || seen.has(option) || ![
12823
+ '--port',
12824
+ '--no-open'
12825
+ ].includes(option)) {
12826
+ io.stderr(`Invalid midscene model siwc arguments.\n${USAGE}`);
12827
+ return 1;
12828
+ }
12829
+ seen.add(option);
12830
+ if ('--no-open' === option) portOptions.noOpen = true;
12831
+ else {
12832
+ const value = options[++index];
12833
+ if (!value || !/^\d+$/.test(value) || Number(value) > 65535) {
12834
+ io.stderr('--port must be an integer between 0 and 65535.');
12835
+ return 1;
12836
+ }
12837
+ portOptions.port = Number(value);
12838
+ }
12839
+ }
12840
+ const unlock = await lockCredentials(credentialsPath).catch((error)=>{
12841
+ io.stderr(error.message);
12842
+ });
12843
+ if (!unlock) return 1;
12844
+ const controller = new AbortController();
12845
+ const cancel = ()=>controller.abort();
12846
+ const timer = setTimeout(cancel, 300000);
12847
+ process.once('SIGINT', cancel);
12848
+ process.once('SIGTERM', cancel);
12849
+ try {
12850
+ io.stderr(source_default().cyan.bold(`🔄 Starting OpenAI SIWC ${action}...`));
12851
+ const saved = await readRegistration(credentialsPath);
12852
+ const credentials = await (async ()=>{
12853
+ if ('refresh' === action) return refreshOpenAICredentials(requireCredentials(saved), controller.signal);
12854
+ const registration = saved ?? {
12855
+ ext_agent_host_id: `urn:uuid:${randomUUID()}`
12856
+ };
12857
+ if (!saved) await saveRegistration(credentialsPath, registration);
12858
+ const result = await loginWithOpenAI({
12859
+ port: portOptions.port,
12860
+ hostId: registration.ext_agent_host_id,
12861
+ clientId: registration.client_id,
12862
+ signal: controller.signal,
12863
+ onAuthorization: async (url)=>{
12864
+ io.stderr(`\n${source_default().bold('Continue with ChatGPT:')}\n${url}\n\n${source_default().gray('Waiting for authorization...')}`);
12865
+ if (!portOptions.noOpen) try {
12866
+ await openBrowser(url);
12867
+ } catch {
12868
+ io.stderr('Open the URL above in your browser to continue.');
12869
+ }
12870
+ }
12871
+ });
12872
+ if (registration.subject && result.subject !== registration.subject) throw new Error('Login identity does not match the saved account.');
12873
+ return result;
12874
+ })();
12875
+ await saveRegistration(credentialsPath, credentials);
12876
+ io.stderr('\n🔑 your Access token:');
12877
+ io.stdout(credentials.access_token);
12878
+ const expiresAt = new Intl.DateTimeFormat('en-GB', {
12879
+ dateStyle: 'medium',
12880
+ timeStyle: 'long'
12881
+ }).format(credentials.expires_at);
12882
+ const remainingMinutes = Math.ceil((credentials.expires_at - Date.now()) / 60000);
12883
+ const expiresIn = new Intl.RelativeTimeFormat('en', {
12884
+ numeric: 'always'
12885
+ }).format(60 === remainingMinutes ? 1 : remainingMinutes, 60 === remainingMinutes ? 'hour' : 'minute');
12886
+ io.stderr(`\nExpires at: ${expiresAt} (${expiresIn})`);
12887
+ io.stderr(`\n${source_default().green(`✅ SIWC ${action} succeeded.`)}\n\nYou can use this access token as your model API Key in Midscene by setting MIDSCENE_MODEL_API_KEY.\n\nFull credentials are saved locally to:\n${source_default().cyan(credentialsPath)}\n\n${source_default().gray('Midscene does not collect your credentials.')}`);
12888
+ return 0;
12889
+ } catch {
12890
+ io.stderr(source_default().red(controller.signal.aborted ? `OpenAI ${action} cancelled or timed out.` : `OpenAI ${action} failed. Check the local credentials file permissions or run midscene model siwc login to authorize again.`));
12891
+ return 1;
12892
+ } finally{
12893
+ clearTimeout(timer);
12894
+ process.removeListener('SIGINT', cancel);
12895
+ process.removeListener('SIGTERM', cancel);
12896
+ await unlock();
12897
+ }
12898
+ }
12486
12899
  const model_command_runModelCommand = (rawArgs, deps, io = {
12487
12900
  stdout: console.log,
12488
12901
  stderr: console.error
12489
- })=>runModelCommand(rawArgs, {
12902
+ })=>{
12903
+ const [, action, ...restArgs] = rawArgs;
12904
+ if ('siwc' === action) return runSiwcCommand(restArgs, io);
12905
+ const withSiwcUsage = (message)=>message.replace(' midscene model verify\n', ' midscene model verify\n midscene model siwc <login|refresh> [options]\n');
12906
+ return runModelCommand(rawArgs, {
12490
12907
  loadDotenv: ()=>loadDotenvConfig({
12491
12908
  dotenvDebug: true,
12492
12909
  dotenvOverride: true,
12493
12910
  log: io.stdout
12494
12911
  }),
12495
12912
  ...deps
12496
- }, io, 'midscene');
12913
+ }, {
12914
+ stdout: (message)=>io.stdout(withSiwcUsage(message)),
12915
+ stderr: (message)=>io.stderr(withSiwcUsage(message))
12916
+ }, 'midscene');
12917
+ };
12497
12918
  const probeWorkflowFormat = (file)=>{
12498
12919
  try {
12499
12920
  const content = (0, __rspack_external_node_fs_5ea92f0c.readFileSync)(file, 'utf8').replace(/\$\{[^}\r\n]*\}/g, 'MIDSCENE_ENV');