@midscene/cli 1.16.2 → 1.16.6-beta-20261023073257.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
@@ -13,18 +13,18 @@ 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, randomBytes, randomUUID } from "node:crypto";
16
+ import { createHash, 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";
20
20
  import { createYamlPlayer as yaml_runtime_createYamlPlayer } from "@midscene/test/internal/yaml-runtime";
21
- import { stripVTControlCharacters } from "node:util";
21
+ import { inspect, stripVTControlCharacters } from "node:util";
22
22
  import { createRequire } from "node:module";
23
23
  import { serializeError } from "@midscene/shared/agent-tools/error-formatter";
24
24
  import { spawn } from "node:child_process";
25
+ import { SiwcError, createSiwcAuthorization, exchangeSiwcCode, parseSiwcCallback, refreshSiwcCredentials, withSiwcStage } from "@midscene/core/ai-model";
25
26
  import { homedir } from "node:os";
26
27
  import { createServer } from "node:http";
27
- import { createRemoteJWKSet, jwtVerify } from "jose";
28
28
  import { classifyWorkflowDocumentFormat } from "@midscene/core/internal/test-runner";
29
29
  import * as __rspack_external_assert from "assert";
30
30
  import * as __rspack_external_fs from "fs";
@@ -2800,9 +2800,7 @@ var __webpack_modules__ = {
2800
2800
  module.exports = (string, columns, options)=>String(string).normalize().replace(/\r\n/g, '\n').split('\n').map((line)=>exec(line, columns, options)).join('\n');
2801
2801
  },
2802
2802
  "./src/index.ts" (__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
2803
- var package_namespaceObject = {
2804
- rE: "1.16.2"
2805
- };
2803
+ var package_namespaceObject = JSON.parse('{"rE":"1.16.6-beta-20261023073257.0"}');
2806
2804
  var brace_expansion = __webpack_require__("../../node_modules/.pnpm/brace-expansion@2.0.1/node_modules/brace-expansion/index.js");
2807
2805
  const MAX_PATTERN_LENGTH = 65536;
2808
2806
  const assertValidPattern = (pattern)=>{
@@ -8475,7 +8473,7 @@ Usage:
8475
8473
  type: 'boolean',
8476
8474
  description: 'Whether to dismiss the HarmonyOS keyboard after text input'
8477
8475
  }
8478
- }).version('version', 'Show version number', "1.16.2").help().epilogue(`For complete list of configuration options, please visit:
8476
+ }).version('version', 'Show version number', "1.16.6-beta-20261023073257.0").help().epilogue(`For complete list of configuration options, please visit:
8479
8477
  • Web options: https://midscenejs.com/automate-with-scripts-in-yaml#the-web-part
8480
8478
  • Android options: https://midscenejs.com/automate-with-scripts-in-yaml#the-android-part
8481
8479
  • HarmonyOS options: https://midscenejs.com/automate-with-scripts-in-yaml#the-harmony-part
@@ -12490,19 +12488,31 @@ defineYamlBatchTest(test, testOptions);
12490
12488
  const success = printExecutionSummary(results, summaryPath);
12491
12489
  return success ? exitCode : 1;
12492
12490
  }
12491
+ function describeSiwcError(error) {
12492
+ return inspect(error, {
12493
+ depth: null,
12494
+ colors: false,
12495
+ maxArrayLength: null,
12496
+ maxStringLength: null
12497
+ });
12498
+ }
12493
12499
  const defaultCredentialsPath = ()=>join(homedir(), '.midscene', 'siwc.json');
12494
12500
  async function readCredentialsFile(path) {
12495
12501
  const content = await readFile(path, 'utf8').catch((error)=>{
12496
12502
  if ('ENOENT' === error.code) return;
12497
- throw new Error('Unable to read the local SIWC credentials file.');
12503
+ throw new SiwcError('Unable to read the local SIWC credentials file.', {
12504
+ cause: error
12505
+ });
12498
12506
  });
12499
12507
  if (void 0 === content) return;
12500
12508
  try {
12501
12509
  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.');
12510
+ if (!value || 'string' != typeof value.credentials?.ext_agent_host_id || !value.credentials.ext_agent_host_id) throw new SiwcError('Invalid SIWC credentials record.');
12503
12511
  return value;
12504
- } catch {
12505
- throw new Error('Invalid local SIWC credentials file.');
12512
+ } catch (error) {
12513
+ throw new SiwcError('Invalid local SIWC credentials file.', {
12514
+ cause: error
12515
+ });
12506
12516
  }
12507
12517
  }
12508
12518
  async function readRegistration(path) {
@@ -12541,8 +12551,10 @@ defineYamlBatchTest(test, testOptions);
12541
12551
  mode: 448
12542
12552
  });
12543
12553
  } 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.');
12554
+ if ('EEXIST' === error.code) throw new SiwcError(`Another SIWC command holds ${lockPath}. If it was interrupted, remove this lock only after confirming that no SIWC command is running.`);
12555
+ throw new SiwcError('Unable to lock the local SIWC credentials file.', {
12556
+ cause: error
12557
+ });
12546
12558
  }
12547
12559
  return ()=>rm(lockPath, {
12548
12560
  recursive: true,
@@ -12557,18 +12569,13 @@ defineYamlBatchTest(test, testOptions);
12557
12569
  'access_token',
12558
12570
  'refresh_token',
12559
12571
  '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.');
12572
+ ].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 SiwcError('No valid SIWC credentials. Run midscene model siwc login first.');
12561
12573
  return value;
12562
12574
  }
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
12575
  async function loginWithOpenAI(options, dependencies) {
12567
12576
  const hostId = options.hostId ?? `urn:uuid:${randomUUID()}`;
12568
12577
  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');
12578
+ const context = {};
12572
12579
  const signal = options.signal;
12573
12580
  signal.throwIfAborted();
12574
12581
  const server = createServer();
@@ -12582,17 +12589,24 @@ defineYamlBatchTest(test, testOptions);
12582
12589
  response.setHeader('Referrer-Policy', 'no-referrer');
12583
12590
  const url = new URL(request.url || '/', 'http://127.0.0.1');
12584
12591
  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.');
12592
+ if (!context.attempt) return void response.writeHead(400).end('Authorization has not started.');
12586
12593
  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.'));
12594
+ const parsed = (()=>{
12595
+ try {
12596
+ return parseSiwcCallback(new URL(request.url || '/', context.attempt.redirectUri).toString(), context.attempt);
12597
+ } catch (error) {
12598
+ response.writeHead(400).end('Invalid authorization callback.');
12599
+ if (url.searchParams.has('error')) {
12600
+ consumed.value = true;
12601
+ reject(error);
12602
+ }
12603
+ return;
12604
+ }
12605
+ })();
12606
+ if (!parsed) {
12607
+ if (!response.writableEnded) response.writeHead(400).end('Invalid login state. Use the original authorization link.');
12591
12608
  return;
12592
12609
  }
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
12610
  consumed.value = true;
12597
12611
  response.setHeader('Content-Type', 'text/html; charset=utf-8');
12598
12612
  response.end(`<!doctype html>
@@ -12618,10 +12632,7 @@ defineYamlBatchTest(test, testOptions);
12618
12632
  </main>
12619
12633
  </body>
12620
12634
  </html>`);
12621
- resolve({
12622
- code,
12623
- clientId
12624
- });
12635
+ resolve(parsed);
12625
12636
  });
12626
12637
  server.on('error', reject);
12627
12638
  });
@@ -12640,132 +12651,22 @@ defineYamlBatchTest(test, testOptions);
12640
12651
  });
12641
12652
  signal.throwIfAborted();
12642
12653
  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'
12654
+ const { attempt, url } = await createSiwcAuthorization({
12655
+ hostId,
12656
+ clientId: existingClientId,
12657
+ redirectUri,
12658
+ agentName: 'Midscene'
12678
12659
  });
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
- };
12660
+ context.attempt = attempt;
12661
+ await options.onAuthorization(url);
12662
+ const result = await withSiwcStage('Authorization callback', ()=>callback);
12663
+ return await exchangeSiwcCode(attempt, result, signal, dependencies);
12714
12664
  } finally{
12715
12665
  signal.removeEventListener('abort', abort);
12716
12666
  server.closeAllConnections();
12717
12667
  await new Promise((resolve)=>server.close(()=>resolve()));
12718
12668
  }
12719
12669
  }
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
12670
  const USAGE = `Usage:
12770
12671
  midscene model siwc login [--no-open] [--port <port>]
12771
12672
  midscene model siwc refresh
@@ -12838,7 +12739,7 @@ Options (login):
12838
12739
  }
12839
12740
  }
12840
12741
  const unlock = await lockCredentials(credentialsPath).catch((error)=>{
12841
- io.stderr(error.message);
12742
+ io.stderr(describeSiwcError(error));
12842
12743
  });
12843
12744
  if (!unlock) return 1;
12844
12745
  const controller = new AbortController();
@@ -12848,31 +12749,31 @@ Options (login):
12848
12749
  process.once('SIGTERM', cancel);
12849
12750
  try {
12850
12751
  io.stderr(source_default().cyan.bold(`🔄 Starting OpenAI SIWC ${action}...`));
12851
- const saved = await readRegistration(credentialsPath);
12752
+ const saved = await withSiwcStage('Read credentials', ()=>readRegistration(credentialsPath));
12852
12753
  const credentials = await (async ()=>{
12853
- if ('refresh' === action) return refreshOpenAICredentials(requireCredentials(saved), controller.signal);
12754
+ if ('refresh' === action) return withSiwcStage('Refresh', ()=>refreshSiwcCredentials(requireCredentials(saved), controller.signal));
12854
12755
  const registration = saved ?? {
12855
12756
  ext_agent_host_id: `urn:uuid:${randomUUID()}`
12856
12757
  };
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.');
12758
+ if (!saved) await withSiwcStage('Save registration', ()=>saveRegistration(credentialsPath, registration));
12759
+ const result = await withSiwcStage('Login', ()=>loginWithOpenAI({
12760
+ port: portOptions.port,
12761
+ hostId: registration.ext_agent_host_id,
12762
+ clientId: registration.client_id,
12763
+ signal: controller.signal,
12764
+ onAuthorization: async (url)=>{
12765
+ io.stderr(`\n${source_default().bold('Continue with ChatGPT:')}\n${url}\n\n${source_default().gray('Waiting for authorization...')}`);
12766
+ if (!portOptions.noOpen) try {
12767
+ await openBrowser(url);
12768
+ } catch {
12769
+ io.stderr('Open the URL above in your browser to continue.');
12770
+ }
12869
12771
  }
12870
- }
12871
- });
12872
- if (registration.subject && result.subject !== registration.subject) throw new Error('Login identity does not match the saved account.');
12772
+ }));
12773
+ if (registration.subject && result.subject !== registration.subject) throw new SiwcError('Login identity does not match the saved account.');
12873
12774
  return result;
12874
12775
  })();
12875
- await saveRegistration(credentialsPath, credentials);
12776
+ await withSiwcStage('Save credentials', ()=>saveRegistration(credentialsPath, credentials));
12876
12777
  io.stderr('\n🔑 your Access token:');
12877
12778
  io.stdout(credentials.access_token);
12878
12779
  const expiresAt = new Intl.DateTimeFormat('en-GB', {
@@ -12886,8 +12787,8 @@ Options (login):
12886
12787
  io.stderr(`\nExpires at: ${expiresAt} (${expiresIn})`);
12887
12788
  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
12789
  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.`));
12790
+ } catch (error) {
12791
+ io.stderr(source_default().red(controller.signal.aborted ? `OpenAI ${action} cancelled or timed out.\n${describeSiwcError(error)}` : `OpenAI ${action} failed.\n${describeSiwcError(error)}`));
12891
12792
  return 1;
12892
12793
  } finally{
12893
12794
  clearTimeout(timer);