@midscene/cli 1.16.1 → 1.16.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.
package/dist/lib/index.js CHANGED
@@ -3342,7 +3342,7 @@ var __webpack_modules__ = {
3342
3342
  return lines.join('\n');
3343
3343
  };
3344
3344
  const printer_formatYamlProgressSnapshot = (message, attempt, totalAttempts)=>totalAttempts > 1 ? `Attempt ${attempt}/${totalAttempts}\n${message}` : message;
3345
- const external_node_util_namespaceObject = require("node:util");
3345
+ var external_node_util_ = __webpack_require__("node:util");
3346
3346
  var external_node_process_ = __webpack_require__("node:process");
3347
3347
  const copyProperty = (to, from, property, ignoreNonConfigurable)=>{
3348
3348
  if ('length' === property || 'prototype' === property) return;
@@ -3683,7 +3683,7 @@ var __webpack_modules__ = {
3683
3683
  for (const content of contents){
3684
3684
  const rows = content.split('\n');
3685
3685
  for (const row of rows){
3686
- const text = (0, external_node_util_namespaceObject.stripVTControlCharacters)(row);
3686
+ const text = (0, external_node_util_.stripVTControlCharacters)(row);
3687
3687
  count += Math.max(1, Math.ceil(text.length / columns));
3688
3688
  }
3689
3689
  }
@@ -4588,7 +4588,7 @@ defineYamlBatchTest(test, testOptions);
4588
4588
  var core_ = __webpack_require__("@midscene/core");
4589
4589
  const cli_namespaceObject = require("@midscene/shared/cli");
4590
4590
  var package_namespaceObject = {
4591
- rE: "1.16.1"
4591
+ rE: "1.16.5"
4592
4592
  };
4593
4593
  var external_node_fs_ = __webpack_require__("node:fs");
4594
4594
  var external_node_fs_namespaceObject = /*#__PURE__*/ __webpack_require__.t(external_node_fs_, 2);
@@ -10273,7 +10273,7 @@ Usage:
10273
10273
  type: 'boolean',
10274
10274
  description: 'Whether to dismiss the HarmonyOS keyboard after text input'
10275
10275
  }
10276
- }).version('version', 'Show version number', "1.16.1").help().epilogue(`For complete list of configuration options, please visit:
10276
+ }).version('version', 'Show version number', "1.16.5").help().epilogue(`For complete list of configuration options, please visit:
10277
10277
  • Web options: https://midscenejs.com/automate-with-scripts-in-yaml#the-web-part
10278
10278
  • Android options: https://midscenejs.com/automate-with-scripts-in-yaml#the-android-part
10279
10279
  • HarmonyOS options: https://midscenejs.com/automate-with-scripts-in-yaml#the-harmony-part
@@ -12527,22 +12527,36 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
12527
12527
  var framework = __webpack_require__("./src/framework/index.ts");
12528
12528
  const external_node_child_process_namespaceObject = require("node:child_process");
12529
12529
  var external_node_crypto_ = __webpack_require__("node:crypto");
12530
+ const ai_model_namespaceObject = require("@midscene/core/ai-model");
12530
12531
  var chalk_source = __webpack_require__("../../node_modules/.pnpm/chalk@4.1.2/node_modules/chalk/source/index.js");
12531
12532
  var source_default = /*#__PURE__*/ __webpack_require__.n(chalk_source);
12532
12533
  const external_node_os_namespaceObject = require("node:os");
12534
+ var external_node_util_ = __webpack_require__("node:util");
12535
+ function describeSiwcError(error) {
12536
+ return (0, external_node_util_.inspect)(error, {
12537
+ depth: null,
12538
+ colors: false,
12539
+ maxArrayLength: null,
12540
+ maxStringLength: null
12541
+ });
12542
+ }
12533
12543
  const defaultCredentialsPath = ()=>(0, external_node_path_.join)((0, external_node_os_namespaceObject.homedir)(), '.midscene', 'siwc.json');
12534
12544
  async function readCredentialsFile(path) {
12535
12545
  const content = await (0, promises_namespaceObject.readFile)(path, 'utf8').catch((error)=>{
12536
12546
  if ('ENOENT' === error.code) return;
12537
- throw new Error('Unable to read the local SIWC credentials file.');
12547
+ throw new ai_model_namespaceObject.SiwcError('Unable to read the local SIWC credentials file.', {
12548
+ cause: error
12549
+ });
12538
12550
  });
12539
12551
  if (void 0 === content) return;
12540
12552
  try {
12541
12553
  const value = JSON.parse(content);
12542
- if (!value || 'string' != typeof value.credentials?.ext_agent_host_id || !value.credentials.ext_agent_host_id) throw new Error('Invalid SIWC credentials record.');
12554
+ if (!value || 'string' != typeof value.credentials?.ext_agent_host_id || !value.credentials.ext_agent_host_id) throw new ai_model_namespaceObject.SiwcError('Invalid SIWC credentials record.');
12543
12555
  return value;
12544
- } catch {
12545
- throw new Error('Invalid local SIWC credentials file.');
12556
+ } catch (error) {
12557
+ throw new ai_model_namespaceObject.SiwcError('Invalid local SIWC credentials file.', {
12558
+ cause: error
12559
+ });
12546
12560
  }
12547
12561
  }
12548
12562
  async function readRegistration(path) {
@@ -12581,8 +12595,10 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
12581
12595
  mode: 448
12582
12596
  });
12583
12597
  } catch (error) {
12584
- 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.`);
12585
- throw new Error('Unable to lock the local SIWC credentials file.');
12598
+ if ('EEXIST' === error.code) throw new ai_model_namespaceObject.SiwcError(`Another SIWC command holds ${lockPath}. If it was interrupted, remove this lock only after confirming that no SIWC command is running.`);
12599
+ throw new ai_model_namespaceObject.SiwcError('Unable to lock the local SIWC credentials file.', {
12600
+ cause: error
12601
+ });
12586
12602
  }
12587
12603
  return ()=>(0, promises_namespaceObject.rm)(lockPath, {
12588
12604
  recursive: true,
@@ -12597,20 +12613,14 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
12597
12613
  'access_token',
12598
12614
  'refresh_token',
12599
12615
  'id_token'
12600
- ].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.');
12616
+ ].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 ai_model_namespaceObject.SiwcError('No valid SIWC credentials. Run midscene model siwc login first.');
12601
12617
  return value;
12602
12618
  }
12603
12619
  const external_node_http_namespaceObject = require("node:http");
12604
- const external_jose_namespaceObject = require("jose");
12605
- const ISSUER = 'https://auth.openai.com';
12606
- const RESOURCE = 'https://api.openai.com/v1';
12607
- const SCOPES = 'openid profile email offline_access resource.invoke chatgpt.tokens.use.direct';
12608
12620
  async function loginWithOpenAI(options, dependencies) {
12609
12621
  const hostId = options.hostId ?? `urn:uuid:${(0, external_node_crypto_.randomUUID)()}`;
12610
12622
  const existingClientId = options.clientId;
12611
- const state = (0, external_node_crypto_.randomBytes)(32).toString('base64url');
12612
- const nonce = (0, external_node_crypto_.randomBytes)(32).toString('base64url');
12613
- const verifier = (0, external_node_crypto_.randomBytes)(32).toString('base64url');
12623
+ const context = {};
12614
12624
  const signal = options.signal;
12615
12625
  signal.throwIfAborted();
12616
12626
  const server = (0, external_node_http_namespaceObject.createServer)();
@@ -12624,17 +12634,24 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
12624
12634
  response.setHeader('Referrer-Policy', 'no-referrer');
12625
12635
  const url = new URL(request.url || '/', 'http://127.0.0.1');
12626
12636
  if ('GET' !== request.method || '/auth/callback' !== url.pathname) return void response.writeHead(404).end('Not found');
12627
- 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.');
12637
+ if (!context.attempt) return void response.writeHead(400).end('Authorization has not started.');
12628
12638
  if (consumed.value) return void response.writeHead(409).end('Authorization already received.');
12629
- if (url.searchParams.has('error')) {
12630
- consumed.value = true;
12631
- response.writeHead(400).end('Authorization was not granted. Return to the terminal.');
12632
- reject(new Error('OpenAI authorization was denied or failed.'));
12639
+ const parsed = (()=>{
12640
+ try {
12641
+ return (0, ai_model_namespaceObject.parseSiwcCallback)(new URL(request.url || '/', context.attempt.redirectUri).toString(), context.attempt);
12642
+ } catch (error) {
12643
+ response.writeHead(400).end('Invalid authorization callback.');
12644
+ if (url.searchParams.has('error')) {
12645
+ consumed.value = true;
12646
+ reject(error);
12647
+ }
12648
+ return;
12649
+ }
12650
+ })();
12651
+ if (!parsed) {
12652
+ if (!response.writableEnded) response.writeHead(400).end('Invalid login state. Use the original authorization link.');
12633
12653
  return;
12634
12654
  }
12635
- const code = url.searchParams.get('code');
12636
- const clientId = url.searchParams.get('client_id') || existingClientId;
12637
- 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.');
12638
12655
  consumed.value = true;
12639
12656
  response.setHeader('Content-Type', 'text/html; charset=utf-8');
12640
12657
  response.end(`<!doctype html>
@@ -12660,10 +12677,7 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
12660
12677
  </main>
12661
12678
  </body>
12662
12679
  </html>`);
12663
- resolve({
12664
- code,
12665
- clientId
12666
- });
12680
+ resolve(parsed);
12667
12681
  });
12668
12682
  server.on('error', reject);
12669
12683
  });
@@ -12682,132 +12696,22 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
12682
12696
  });
12683
12697
  signal.throwIfAborted();
12684
12698
  const redirectUri = `http://127.0.0.1:${server.address().port}/auth/callback`;
12685
- const authorization = new URL(`${ISSUER}/api/accounts/authorize`);
12686
- authorization.search = new URLSearchParams({
12687
- client_id: existingClientId ?? 'dynamic_agent_client',
12688
- ...existingClientId ? {} : {
12689
- agent_name_hint: 'Midscene'
12690
- },
12691
- ext_agent_host_id: hostId,
12692
- response_type: 'code',
12693
- redirect_uri: redirectUri,
12694
- scope: SCOPES,
12695
- resource: RESOURCE,
12696
- state,
12697
- nonce,
12698
- code_challenge_method: 'S256',
12699
- code_challenge: (0, external_node_crypto_.createHash)('sha256').update(verifier).digest('base64url')
12700
- }).toString();
12701
- await options.onAuthorization(authorization.toString());
12702
- const { code, clientId } = await callback;
12703
- signal.throwIfAborted();
12704
- const request = dependencies?.fetch ?? fetch;
12705
- const result = await request(`${ISSUER}/api/accounts/oauth/token`, {
12706
- method: 'POST',
12707
- headers: {
12708
- 'Content-Type': 'application/x-www-form-urlencoded'
12709
- },
12710
- body: new URLSearchParams({
12711
- grant_type: 'authorization_code',
12712
- client_id: clientId,
12713
- code,
12714
- code_verifier: verifier,
12715
- redirect_uri: redirectUri,
12716
- resource: RESOURCE
12717
- }),
12718
- signal,
12719
- redirect: 'error'
12699
+ const { attempt, url } = await (0, ai_model_namespaceObject.createSiwcAuthorization)({
12700
+ hostId,
12701
+ clientId: existingClientId,
12702
+ redirectUri,
12703
+ agentName: 'Midscene'
12720
12704
  });
12721
- if (!result.ok) throw new Error(`OpenAI token exchange failed (HTTP ${result.status}). Run midscene model siwc login again.`);
12722
- const tokens = await result.json();
12723
- 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.');
12724
- const scopes = tokens.scope.split(/\s+/);
12725
- 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.');
12726
- const keys = dependencies?.keys ?? (0, external_jose_namespaceObject.createRemoteJWKSet)(new URL(`${ISSUER}/.well-known/jwks.json`));
12727
- const { payload } = await (0, external_jose_namespaceObject.jwtVerify)(tokens.id_token, keys, {
12728
- issuer: ISSUER,
12729
- audience: clientId,
12730
- algorithms: [
12731
- 'RS256'
12732
- ],
12733
- requiredClaims: [
12734
- 'sub',
12735
- 'exp',
12736
- 'iat',
12737
- 'nonce'
12738
- ]
12739
- });
12740
- if (payload.nonce !== nonce || !payload.sub) throw new Error('OpenAI login identity or nonce did not match.');
12741
- signal.throwIfAborted();
12742
- return {
12743
- ext_agent_host_id: hostId,
12744
- client_id: clientId,
12745
- subject: payload.sub,
12746
- issuer: ISSUER,
12747
- ...'string' == typeof payload.email ? {
12748
- email: payload.email
12749
- } : {},
12750
- access_token: tokens.access_token,
12751
- refresh_token: tokens.refresh_token,
12752
- id_token: tokens.id_token,
12753
- scopes,
12754
- expires_at: Date.now() + 1000 * tokens.expires_in
12755
- };
12705
+ context.attempt = attempt;
12706
+ await options.onAuthorization(url);
12707
+ const result = await (0, ai_model_namespaceObject.withSiwcStage)('Authorization callback', ()=>callback);
12708
+ return await (0, ai_model_namespaceObject.exchangeSiwcCode)(attempt, result, signal, dependencies);
12756
12709
  } finally{
12757
12710
  signal.removeEventListener('abort', abort);
12758
12711
  server.closeAllConnections();
12759
12712
  await new Promise((resolve)=>server.close(()=>resolve()));
12760
12713
  }
12761
12714
  }
12762
- async function refreshOpenAICredentials(saved, signal, dependencies) {
12763
- const issuer = 'https://auth.openai.com';
12764
- signal.throwIfAborted();
12765
- const response = await (dependencies?.fetch ?? fetch)(`${issuer}/api/accounts/oauth/token`, {
12766
- method: 'POST',
12767
- headers: {
12768
- 'Content-Type': 'application/x-www-form-urlencoded'
12769
- },
12770
- body: new URLSearchParams({
12771
- grant_type: 'refresh_token',
12772
- client_id: saved.client_id,
12773
- refresh_token: saved.refresh_token,
12774
- resource: 'https://api.openai.com/v1'
12775
- }),
12776
- signal,
12777
- redirect: 'error'
12778
- });
12779
- if (!response.ok) throw new Error('OpenAI refresh failed.');
12780
- const tokens = await response.json();
12781
- 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.');
12782
- const scopes = tokens.scope.split(/\s+/);
12783
- if (!scopes.includes('chatgpt.tokens.use.direct')) throw new Error('ChatGPT plan usage is no longer authorized.');
12784
- if (void 0 !== tokens.id_token) {
12785
- if ('string' != typeof tokens.id_token || !tokens.id_token) throw new Error('Invalid refreshed ID token.');
12786
- const keys = dependencies?.keys ?? (0, external_jose_namespaceObject.createRemoteJWKSet)(new URL(`${issuer}/.well-known/jwks.json`));
12787
- const { payload } = await (0, external_jose_namespaceObject.jwtVerify)(tokens.id_token, keys, {
12788
- issuer,
12789
- audience: saved.client_id,
12790
- algorithms: [
12791
- 'RS256'
12792
- ],
12793
- requiredClaims: [
12794
- 'sub',
12795
- 'exp',
12796
- 'iat'
12797
- ]
12798
- });
12799
- if (payload.sub !== saved.subject) throw new Error('Refreshed identity does not match the saved account.');
12800
- }
12801
- signal.throwIfAborted();
12802
- return {
12803
- ...saved,
12804
- access_token: tokens.access_token,
12805
- refresh_token: tokens.refresh_token,
12806
- id_token: tokens.id_token ?? saved.id_token,
12807
- scopes,
12808
- expires_at: Date.now() + 1000 * tokens.expires_in
12809
- };
12810
- }
12811
12715
  const USAGE = `Usage:
12812
12716
  midscene model siwc login [--no-open] [--port <port>]
12813
12717
  midscene model siwc refresh
@@ -12880,7 +12784,7 @@ Options (login):
12880
12784
  }
12881
12785
  }
12882
12786
  const unlock = await lockCredentials(credentialsPath).catch((error)=>{
12883
- io.stderr(error.message);
12787
+ io.stderr(describeSiwcError(error));
12884
12788
  });
12885
12789
  if (!unlock) return 1;
12886
12790
  const controller = new AbortController();
@@ -12890,31 +12794,31 @@ Options (login):
12890
12794
  process.once('SIGTERM', cancel);
12891
12795
  try {
12892
12796
  io.stderr(source_default().cyan.bold(`🔄 Starting OpenAI SIWC ${action}...`));
12893
- const saved = await readRegistration(credentialsPath);
12797
+ const saved = await (0, ai_model_namespaceObject.withSiwcStage)('Read credentials', ()=>readRegistration(credentialsPath));
12894
12798
  const credentials = await (async ()=>{
12895
- if ('refresh' === action) return refreshOpenAICredentials(requireCredentials(saved), controller.signal);
12799
+ if ('refresh' === action) return (0, ai_model_namespaceObject.withSiwcStage)('Refresh', ()=>(0, ai_model_namespaceObject.refreshSiwcCredentials)(requireCredentials(saved), controller.signal));
12896
12800
  const registration = saved ?? {
12897
12801
  ext_agent_host_id: `urn:uuid:${(0, external_node_crypto_.randomUUID)()}`
12898
12802
  };
12899
- if (!saved) await saveRegistration(credentialsPath, registration);
12900
- const result = await loginWithOpenAI({
12901
- port: portOptions.port,
12902
- hostId: registration.ext_agent_host_id,
12903
- clientId: registration.client_id,
12904
- signal: controller.signal,
12905
- onAuthorization: async (url)=>{
12906
- io.stderr(`\n${source_default().bold('Continue with ChatGPT:')}\n${url}\n\n${source_default().gray('Waiting for authorization...')}`);
12907
- if (!portOptions.noOpen) try {
12908
- await openBrowser(url);
12909
- } catch {
12910
- io.stderr('Open the URL above in your browser to continue.');
12803
+ if (!saved) await (0, ai_model_namespaceObject.withSiwcStage)('Save registration', ()=>saveRegistration(credentialsPath, registration));
12804
+ const result = await (0, ai_model_namespaceObject.withSiwcStage)('Login', ()=>loginWithOpenAI({
12805
+ port: portOptions.port,
12806
+ hostId: registration.ext_agent_host_id,
12807
+ clientId: registration.client_id,
12808
+ signal: controller.signal,
12809
+ onAuthorization: async (url)=>{
12810
+ io.stderr(`\n${source_default().bold('Continue with ChatGPT:')}\n${url}\n\n${source_default().gray('Waiting for authorization...')}`);
12811
+ if (!portOptions.noOpen) try {
12812
+ await openBrowser(url);
12813
+ } catch {
12814
+ io.stderr('Open the URL above in your browser to continue.');
12815
+ }
12911
12816
  }
12912
- }
12913
- });
12914
- if (registration.subject && result.subject !== registration.subject) throw new Error('Login identity does not match the saved account.');
12817
+ }));
12818
+ if (registration.subject && result.subject !== registration.subject) throw new ai_model_namespaceObject.SiwcError('Login identity does not match the saved account.');
12915
12819
  return result;
12916
12820
  })();
12917
- await saveRegistration(credentialsPath, credentials);
12821
+ await (0, ai_model_namespaceObject.withSiwcStage)('Save credentials', ()=>saveRegistration(credentialsPath, credentials));
12918
12822
  io.stderr('\n🔑 your Access token:');
12919
12823
  io.stdout(credentials.access_token);
12920
12824
  const expiresAt = new Intl.DateTimeFormat('en-GB', {
@@ -12928,8 +12832,8 @@ Options (login):
12928
12832
  io.stderr(`\nExpires at: ${expiresAt} (${expiresIn})`);
12929
12833
  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.')}`);
12930
12834
  return 0;
12931
- } catch {
12932
- 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.`));
12835
+ } catch (error) {
12836
+ io.stderr(source_default().red(controller.signal.aborted ? `OpenAI ${action} cancelled or timed out.\n${describeSiwcError(error)}` : `OpenAI ${action} failed.\n${describeSiwcError(error)}`));
12933
12837
  return 1;
12934
12838
  } finally{
12935
12839
  clearTimeout(timer);
@@ -13123,6 +13027,10 @@ Options (login):
13123
13027
  "use strict";
13124
13028
  module.exports = require("node:url");
13125
13029
  },
13030
+ "node:util" (module) {
13031
+ "use strict";
13032
+ module.exports = require("node:util");
13033
+ },
13126
13034
  os (module) {
13127
13035
  "use strict";
13128
13036
  module.exports = require("os");