@midscene/cli 1.14.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/lib/index.js CHANGED
@@ -2451,232 +2451,6 @@ var __webpack_modules__ = {
2451
2451
  ]
2452
2452
  };
2453
2453
  },
2454
- "../../node_modules/.pnpm/dotenv@16.4.5/node_modules/dotenv/lib/main.js" (module, __unused_rspack_exports, __webpack_require__) {
2455
- const fs = __webpack_require__("fs");
2456
- const path = __webpack_require__("path");
2457
- const os = __webpack_require__("os");
2458
- const crypto = __webpack_require__("crypto");
2459
- const packageJson = __webpack_require__("../../node_modules/.pnpm/dotenv@16.4.5/node_modules/dotenv/package.json");
2460
- const version = packageJson.version;
2461
- const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
2462
- function parse(src) {
2463
- const obj = {};
2464
- let lines = src.toString();
2465
- lines = lines.replace(/\r\n?/mg, '\n');
2466
- let match;
2467
- while(null != (match = LINE.exec(lines))){
2468
- const key = match[1];
2469
- let value = match[2] || '';
2470
- value = value.trim();
2471
- const maybeQuote = value[0];
2472
- value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2');
2473
- if ('"' === maybeQuote) {
2474
- value = value.replace(/\\n/g, '\n');
2475
- value = value.replace(/\\r/g, '\r');
2476
- }
2477
- obj[key] = value;
2478
- }
2479
- return obj;
2480
- }
2481
- function _parseVault(options) {
2482
- const vaultPath = _vaultPath(options);
2483
- const result = DotenvModule.configDotenv({
2484
- path: vaultPath
2485
- });
2486
- if (!result.parsed) {
2487
- const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
2488
- err.code = 'MISSING_DATA';
2489
- throw err;
2490
- }
2491
- const keys = _dotenvKey(options).split(',');
2492
- const length = keys.length;
2493
- let decrypted;
2494
- for(let i = 0; i < length; i++)try {
2495
- const key = keys[i].trim();
2496
- const attrs = _instructions(result, key);
2497
- decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
2498
- break;
2499
- } catch (error) {
2500
- if (i + 1 >= length) throw error;
2501
- }
2502
- return DotenvModule.parse(decrypted);
2503
- }
2504
- function _log(message) {
2505
- console.log(`[dotenv@${version}][INFO] ${message}`);
2506
- }
2507
- function _warn(message) {
2508
- console.log(`[dotenv@${version}][WARN] ${message}`);
2509
- }
2510
- function _debug(message) {
2511
- console.log(`[dotenv@${version}][DEBUG] ${message}`);
2512
- }
2513
- function _dotenvKey(options) {
2514
- if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY;
2515
- if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) return process.env.DOTENV_KEY;
2516
- return '';
2517
- }
2518
- function _instructions(result, dotenvKey) {
2519
- let uri;
2520
- try {
2521
- uri = new URL(dotenvKey);
2522
- } catch (error) {
2523
- if ('ERR_INVALID_URL' === error.code) {
2524
- const err = new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development');
2525
- err.code = 'INVALID_DOTENV_KEY';
2526
- throw err;
2527
- }
2528
- throw error;
2529
- }
2530
- const key = uri.password;
2531
- if (!key) {
2532
- const err = new Error('INVALID_DOTENV_KEY: Missing key part');
2533
- err.code = 'INVALID_DOTENV_KEY';
2534
- throw err;
2535
- }
2536
- const environment = uri.searchParams.get('environment');
2537
- if (!environment) {
2538
- const err = new Error('INVALID_DOTENV_KEY: Missing environment part');
2539
- err.code = 'INVALID_DOTENV_KEY';
2540
- throw err;
2541
- }
2542
- const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
2543
- const ciphertext = result.parsed[environmentKey];
2544
- if (!ciphertext) {
2545
- const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
2546
- err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT';
2547
- throw err;
2548
- }
2549
- return {
2550
- ciphertext,
2551
- key
2552
- };
2553
- }
2554
- function _vaultPath(options) {
2555
- let possibleVaultPath = null;
2556
- if (options && options.path && options.path.length > 0) if (Array.isArray(options.path)) {
2557
- for (const filepath of options.path)if (fs.existsSync(filepath)) possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault`;
2558
- } else possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault`;
2559
- else possibleVaultPath = path.resolve(process.cwd(), '.env.vault');
2560
- if (fs.existsSync(possibleVaultPath)) return possibleVaultPath;
2561
- return null;
2562
- }
2563
- function _resolveHome(envPath) {
2564
- return '~' === envPath[0] ? path.join(os.homedir(), envPath.slice(1)) : envPath;
2565
- }
2566
- function _configVault(options) {
2567
- _log('Loading env from encrypted .env.vault');
2568
- const parsed = DotenvModule._parseVault(options);
2569
- let processEnv = process.env;
2570
- if (options && null != options.processEnv) processEnv = options.processEnv;
2571
- DotenvModule.populate(processEnv, parsed, options);
2572
- return {
2573
- parsed
2574
- };
2575
- }
2576
- function configDotenv(options) {
2577
- const dotenvPath = path.resolve(process.cwd(), '.env');
2578
- let encoding = 'utf8';
2579
- const debug = Boolean(options && options.debug);
2580
- if (options && options.encoding) encoding = options.encoding;
2581
- else if (debug) _debug('No encoding is specified. UTF-8 is used by default');
2582
- let optionPaths = [
2583
- dotenvPath
2584
- ];
2585
- if (options && options.path) if (Array.isArray(options.path)) {
2586
- optionPaths = [];
2587
- for (const filepath of options.path)optionPaths.push(_resolveHome(filepath));
2588
- } else optionPaths = [
2589
- _resolveHome(options.path)
2590
- ];
2591
- let lastError;
2592
- const parsedAll = {};
2593
- for (const path of optionPaths)try {
2594
- const parsed = DotenvModule.parse(fs.readFileSync(path, {
2595
- encoding
2596
- }));
2597
- DotenvModule.populate(parsedAll, parsed, options);
2598
- } catch (e1) {
2599
- if (debug) _debug(`Failed to load ${path} ${e1.message}`);
2600
- lastError = e1;
2601
- }
2602
- let processEnv = process.env;
2603
- if (options && null != options.processEnv) processEnv = options.processEnv;
2604
- DotenvModule.populate(processEnv, parsedAll, options);
2605
- if (lastError) return {
2606
- parsed: parsedAll,
2607
- error: lastError
2608
- };
2609
- return {
2610
- parsed: parsedAll
2611
- };
2612
- }
2613
- function config(options) {
2614
- if (0 === _dotenvKey(options).length) return DotenvModule.configDotenv(options);
2615
- const vaultPath = _vaultPath(options);
2616
- if (!vaultPath) {
2617
- _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
2618
- return DotenvModule.configDotenv(options);
2619
- }
2620
- return DotenvModule._configVault(options);
2621
- }
2622
- function decrypt(encrypted, keyStr) {
2623
- const key = Buffer.from(keyStr.slice(-64), 'hex');
2624
- let ciphertext = Buffer.from(encrypted, 'base64');
2625
- const nonce = ciphertext.subarray(0, 12);
2626
- const authTag = ciphertext.subarray(-16);
2627
- ciphertext = ciphertext.subarray(12, -16);
2628
- try {
2629
- const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce);
2630
- aesgcm.setAuthTag(authTag);
2631
- return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
2632
- } catch (error) {
2633
- const isRange = error instanceof RangeError;
2634
- const invalidKeyLength = 'Invalid key length' === error.message;
2635
- const decryptionFailed = 'Unsupported state or unable to authenticate data' === error.message;
2636
- if (isRange || invalidKeyLength) {
2637
- const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)');
2638
- err.code = 'INVALID_DOTENV_KEY';
2639
- throw err;
2640
- }
2641
- if (decryptionFailed) {
2642
- const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY');
2643
- err.code = 'DECRYPTION_FAILED';
2644
- throw err;
2645
- }
2646
- throw error;
2647
- }
2648
- }
2649
- function populate(processEnv, parsed, options = {}) {
2650
- const debug = Boolean(options && options.debug);
2651
- const override = Boolean(options && options.override);
2652
- if ('object' != typeof parsed) {
2653
- const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate');
2654
- err.code = 'OBJECT_REQUIRED';
2655
- throw err;
2656
- }
2657
- for (const key of Object.keys(parsed))if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
2658
- if (true === override) processEnv[key] = parsed[key];
2659
- if (debug) true === override ? _debug(`"${key}" is already defined and WAS overwritten`) : _debug(`"${key}" is already defined and was NOT overwritten`);
2660
- } else processEnv[key] = parsed[key];
2661
- }
2662
- const DotenvModule = {
2663
- configDotenv,
2664
- _configVault,
2665
- _parseVault,
2666
- config,
2667
- decrypt,
2668
- parse,
2669
- populate
2670
- };
2671
- module.exports.configDotenv = DotenvModule.configDotenv;
2672
- module.exports._configVault = DotenvModule._configVault;
2673
- module.exports._parseVault = DotenvModule._parseVault;
2674
- module.exports.config = DotenvModule.config;
2675
- module.exports.decrypt = DotenvModule.decrypt;
2676
- module.exports.parse = DotenvModule.parse;
2677
- module.exports.populate = DotenvModule.populate;
2678
- module.exports = DotenvModule;
2679
- },
2680
2454
  "../../node_modules/.pnpm/emoji-regex@8.0.0/node_modules/emoji-regex/index.js" (module) {
2681
2455
  "use strict";
2682
2456
  module.exports = function() {
@@ -3014,7 +2788,7 @@ var __webpack_modules__ = {
3014
2788
  });
3015
2789
  var external_node_fs_ = __webpack_require__("node:fs");
3016
2790
  var external_node_path_ = __webpack_require__("node:path");
3017
- const external_node_crypto_namespaceObject = require("node:crypto");
2791
+ var external_node_crypto_ = __webpack_require__("node:crypto");
3018
2792
  var core_ = __webpack_require__("@midscene/core");
3019
2793
  const common_namespaceObject = require("@midscene/shared/common");
3020
2794
  var logger_ = __webpack_require__("@midscene/shared/logger");
@@ -3171,7 +2945,7 @@ var __webpack_modules__ = {
3171
2945
  const safeReportNamePart = (value)=>value.replace(/[:*?"<>|# ]/g, '-');
3172
2946
  const createRetryReportName = (file)=>{
3173
2947
  const fileName = (0, external_node_path_.basename)(file, (0, external_node_path_.extname)(file)) || 'yaml';
3174
- const fileHash = (0, external_node_crypto_namespaceObject.createHash)('sha1').update((0, external_node_path_.resolve)(file)).digest('hex').slice(0, 8);
2948
+ const fileHash = (0, external_node_crypto_.createHash)('sha1').update((0, external_node_path_.resolve)(file)).digest('hex').slice(0, 8);
3175
2949
  return `${safeReportNamePart(fileName)}-${fileHash}-retry-attempts`;
3176
2950
  };
3177
2951
  const createRetryAttemptReport = (result)=>{
@@ -4814,7 +4588,7 @@ defineYamlBatchTest(test, testOptions);
4814
4588
  var core_ = __webpack_require__("@midscene/core");
4815
4589
  const cli_namespaceObject = require("@midscene/shared/cli");
4816
4590
  var package_namespaceObject = {
4817
- rE: "1.14.0"
4591
+ rE: "1.15.0"
4818
4592
  };
4819
4593
  var external_node_fs_ = __webpack_require__("node:fs");
4820
4594
  var external_node_fs_namespaceObject = /*#__PURE__*/ __webpack_require__.t(external_node_fs_, 2);
@@ -10437,9 +10211,11 @@ Homepage: https://midscenejs.com
10437
10211
  Github: https://github.com/web-infra-dev/midscene
10438
10212
 
10439
10213
  Usage:
10214
+ $0 model siwc <login|refresh> [options]
10440
10215
  $0 [options] <path-to-yaml-script-file-or-directory>
10441
10216
  $0 [options] --files <yaml-file1> <yaml-file2 ...>
10442
- $0 [options] --config <path-to-config-yaml-file>`).options({
10217
+ $0 [options] --config <path-to-config-yaml-file>
10218
+ $0 model verify Check model connectivity and Midscene compatibility`).options({
10443
10219
  files: {
10444
10220
  type: 'array',
10445
10221
  string: true,
@@ -10497,7 +10273,7 @@ Usage:
10497
10273
  type: 'boolean',
10498
10274
  description: 'Whether to dismiss the HarmonyOS keyboard after text input'
10499
10275
  }
10500
- }).version('version', 'Show version number', "1.14.0").help().epilogue(`For complete list of configuration options, please visit:
10276
+ }).version('version', 'Show version number', "1.15.0").help().epilogue(`For complete list of configuration options, please visit:
10501
10277
  • Web options: https://midscenejs.com/automate-with-scripts-in-yaml#the-web-part
10502
10278
  • Android options: https://midscenejs.com/automate-with-scripts-in-yaml#the-android-part
10503
10279
  • HarmonyOS options: https://midscenejs.com/automate-with-scripts-in-yaml#the-harmony-part
@@ -12747,188 +12523,440 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
12747
12523
  globalConfig: pickYamlTargetConfig(options)
12748
12524
  };
12749
12525
  }
12750
- var main = __webpack_require__("../../node_modules/.pnpm/dotenv@16.4.5/node_modules/dotenv/lib/main.js");
12751
- var main_default = /*#__PURE__*/ __webpack_require__.n(main);
12752
- function loadDotenvConfig(options = {}) {
12753
- const dotEnvConfigFile = (0, external_node_path_.join)(options.cwd ?? process.cwd(), '.env');
12754
- if (!(0, external_node_fs_.existsSync)(dotEnvConfigFile)) return;
12755
- options.log?.(` Env file: ${dotEnvConfigFile}`);
12756
- main_default().config({
12757
- path: dotEnvConfigFile,
12758
- debug: options.dotenvDebug,
12759
- override: options.dotenvOverride
12760
- });
12761
- }
12526
+ const model_command_namespaceObject = require("@midscene/test/internal/model-command");
12762
12527
  var framework = __webpack_require__("./src/framework/index.ts");
12763
- const env_namespaceObject = require("@midscene/shared/env");
12528
+ const external_node_child_process_namespaceObject = require("node:child_process");
12529
+ var external_node_crypto_ = __webpack_require__("node:crypto");
12764
12530
  var chalk_source = __webpack_require__("../../node_modules/.pnpm/chalk@4.1.2/node_modules/chalk/source/index.js");
12765
12531
  var source_default = /*#__PURE__*/ __webpack_require__.n(chalk_source);
12766
- const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
12767
- const MODEL_VERIFY_SEPARATOR = '────────────────────────────────────────';
12768
- const MODEL_COMMAND_USAGE = `Usage:
12769
- midscene model verify
12770
- midscene model eval
12771
- `;
12772
- function assertNoModelVerifyOptions(args) {
12773
- for (const arg of args)throw new Error(`Unknown option for midscene model verify: ${arg}`);
12774
- }
12775
- function shellSingleQuote(value) {
12776
- return `'${value.replace(/'/g, "'\\''")}'`;
12532
+ const external_node_os_namespaceObject = require("node:os");
12533
+ const defaultCredentialsPath = ()=>(0, external_node_path_.join)((0, external_node_os_namespaceObject.homedir)(), '.midscene', 'siwc.json');
12534
+ async function readCredentialsFile(path) {
12535
+ const content = await (0, promises_namespaceObject.readFile)(path, 'utf8').catch((error)=>{
12536
+ if ('ENOENT' === error.code) return;
12537
+ throw new Error('Unable to read the local SIWC credentials file.');
12538
+ });
12539
+ if (void 0 === content) return;
12540
+ try {
12541
+ 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.');
12543
+ return value;
12544
+ } catch {
12545
+ throw new Error('Invalid local SIWC credentials file.');
12546
+ }
12777
12547
  }
12778
- function buildChatCompletionsUrl(baseURL) {
12779
- const normalizedBaseURL = (baseURL || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, '');
12780
- if (normalizedBaseURL.endsWith('/chat/completions')) return normalizedBaseURL;
12781
- return `${normalizedBaseURL}/chat/completions`;
12548
+ async function readRegistration(path) {
12549
+ return (await readCredentialsFile(path))?.credentials;
12782
12550
  }
12783
- function buildCurlCommand(modelConfig) {
12784
- const payload = {
12785
- model: modelConfig.modelName,
12786
- messages: [
12787
- {
12788
- role: 'user',
12789
- content: 'What is 1+1?'
12790
- }
12791
- ]
12551
+ async function saveRegistration(path, value) {
12552
+ const saved = await readCredentialsFile(path);
12553
+ const now = new Date().toISOString();
12554
+ const record = {
12555
+ ...saved,
12556
+ created_at: saved?.created_at ?? now,
12557
+ updated_at: now,
12558
+ credentials: value
12792
12559
  };
12793
- return [
12794
- `curl -X POST ${shellSingleQuote(buildChatCompletionsUrl(modelConfig.openaiBaseURL))} \\`,
12795
- ` -H ${shellSingleQuote(`Authorization: Bearer ${modelConfig.openaiApiKey || ''}`)} \\`,
12796
- ` -H ${shellSingleQuote('Content-Type: application/json')} \\`,
12797
- ` -d ${shellSingleQuote(JSON.stringify(payload, null, 2))}`
12798
- ].join('\n');
12799
- }
12800
- function buildCurlDedupKey(modelConfig) {
12801
- return JSON.stringify({
12802
- baseURL: modelConfig.openaiBaseURL || DEFAULT_OPENAI_BASE_URL,
12803
- apiKey: modelConfig.openaiApiKey || '',
12804
- modelName: modelConfig.modelName
12805
- });
12806
- }
12807
- function buildModelVerifyCurlCommands(configs) {
12808
- const commandMap = new Map();
12809
- for (const item of configs){
12810
- const key = buildCurlDedupKey(item.modelConfig);
12811
- const existing = commandMap.get(key);
12812
- if (existing) {
12813
- existing.intents.push(item.intent);
12814
- continue;
12815
- }
12816
- commandMap.set(key, {
12817
- intents: [
12818
- item.intent
12819
- ],
12820
- curl: buildCurlCommand(item.modelConfig),
12821
- usesDefaultBaseURL: !item.modelConfig.openaiBaseURL
12560
+ const temporary = `${path}.${(0, external_node_crypto_.randomUUID)()}.tmp`;
12561
+ try {
12562
+ await (0, promises_namespaceObject.writeFile)(temporary, `${JSON.stringify(record, null, 2)}\n`, {
12563
+ mode: 384,
12564
+ flag: 'wx'
12565
+ });
12566
+ await (0, promises_namespaceObject.rename)(temporary, path);
12567
+ } finally{
12568
+ await (0, promises_namespaceObject.rm)(temporary, {
12569
+ force: true
12822
12570
  });
12823
12571
  }
12824
- return [
12825
- ...commandMap.values()
12826
- ];
12827
12572
  }
12828
- function formatModelVerifyFailureOutput(message, curlCommands) {
12829
- const details = message?.trim() || 'No failure details were generated.';
12830
- const curlSection = curlCommands.map((item)=>{
12831
- const baseUrlNote = item.usesDefaultBaseURL ? ' (base URL not configured; using OpenAI SDK default)' : '';
12832
- return source_default().gray(`# ${item.intents.join(', ')}${baseUrlNote}\n${item.curl}`);
12833
- }).join('\n\n');
12834
- return [
12835
- source_default().red.bold('āŒ Model verify failed with messages:'),
12836
- MODEL_VERIFY_SEPARATOR,
12837
- '',
12838
- details,
12839
- '',
12840
- MODEL_VERIFY_SEPARATOR,
12841
- 'Generated curl requests for basic API connectivity:',
12842
- 'If the error is a basic connectivity issue, use these requests to test the base URL, API key, and model name directly.',
12843
- 'These commands contain your API key. Do not share them publicly.',
12844
- '',
12845
- curlSection
12846
- ].join('\n');
12573
+ async function lockCredentials(path) {
12574
+ await (0, promises_namespaceObject.mkdir)((0, external_node_path_.dirname)(path), {
12575
+ recursive: true,
12576
+ mode: 448
12577
+ });
12578
+ const lockPath = `${path}.lock`;
12579
+ try {
12580
+ await (0, promises_namespaceObject.mkdir)(lockPath, {
12581
+ mode: 448
12582
+ });
12583
+ } 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.');
12586
+ }
12587
+ return ()=>(0, promises_namespaceObject.rm)(lockPath, {
12588
+ recursive: true,
12589
+ force: true
12590
+ });
12847
12591
  }
12848
- function getDefaultDeps(io) {
12849
- return {
12850
- loadDotenv: ()=>{
12851
- loadDotenvConfig({
12852
- dotenvDebug: true,
12853
- dotenvOverride: true,
12854
- log: io.stdout
12592
+ function requireCredentials(value) {
12593
+ if (!value || [
12594
+ 'client_id',
12595
+ 'subject',
12596
+ 'issuer',
12597
+ 'access_token',
12598
+ 'refresh_token',
12599
+ '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.');
12601
+ return value;
12602
+ }
12603
+ 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
+ async function loginWithOpenAI(options, dependencies) {
12609
+ const hostId = options.hostId ?? `urn:uuid:${(0, external_node_crypto_.randomUUID)()}`;
12610
+ 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');
12614
+ const signal = options.signal;
12615
+ signal.throwIfAborted();
12616
+ const server = (0, external_node_http_namespaceObject.createServer)();
12617
+ const callback = new Promise((resolve, reject)=>{
12618
+ const consumed = {
12619
+ value: false
12620
+ };
12621
+ server.on('request', (request, response)=>{
12622
+ response.setHeader('Content-Type', 'text/plain; charset=utf-8');
12623
+ response.setHeader('Cache-Control', 'no-store');
12624
+ response.setHeader('Referrer-Policy', 'no-referrer');
12625
+ const url = new URL(request.url || '/', 'http://127.0.0.1');
12626
+ 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.');
12628
+ 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.'));
12633
+ return;
12634
+ }
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
+ consumed.value = true;
12639
+ response.setHeader('Content-Type', 'text/html; charset=utf-8');
12640
+ response.end(`<!doctype html>
12641
+ <html lang="en">
12642
+ <head>
12643
+ <meta charset="utf-8">
12644
+ <meta name="viewport" content="width=device-width, initial-scale=1">
12645
+ <title>Authorization received Ā· Midscene</title>
12646
+ <style>
12647
+ * { box-sizing: border-box; }
12648
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; background: #f6f7f9; color: #202124; font-family: system-ui, sans-serif; }
12649
+ 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; }
12650
+ h1 { margin: 0 0 20px; font-size: 24px; line-height: 1.4; }
12651
+ h1 span { color: #16834a; }
12652
+ p { margin: 8px 0 0; color: #596273; font-size: 15px; line-height: 1.6; }
12653
+ </style>
12654
+ </head>
12655
+ <body>
12656
+ <main>
12657
+ <h1><span aria-hidden="true">āœ“</span> Authorization received</h1>
12658
+ <p>You can close this tab.</p>
12659
+ <p>Check your terminal for the sign-in result and access token.</p>
12660
+ </main>
12661
+ </body>
12662
+ </html>`);
12663
+ resolve({
12664
+ code,
12665
+ clientId
12855
12666
  });
12856
- },
12857
- getModelConfig: (intent)=>env_namespaceObject.globalModelConfigManager.getModelConfig(intent),
12858
- verifyModel: core_.runConnectivityTest
12859
- };
12860
- }
12861
- async function runModelVerifyCommand(args, deps, io) {
12667
+ });
12668
+ server.on('error', reject);
12669
+ });
12670
+ callback.catch(()=>void 0);
12671
+ const abort = ()=>server.emit('error', new Error('OpenAI login cancelled or timed out.'));
12672
+ signal.addEventListener('abort', abort, {
12673
+ once: true
12674
+ });
12862
12675
  try {
12863
- if (args.includes('--help') || args.includes('-h')) {
12864
- io.stdout(MODEL_COMMAND_USAGE);
12865
- return 0;
12866
- }
12867
- assertNoModelVerifyOptions(args);
12868
- io.stdout('Model verify started. This usually takes about 5 seconds.\n');
12869
- deps.loadDotenv();
12870
- io.stdout('');
12871
- const defaultModelConfig = deps.getModelConfig('default');
12872
- const planningModelConfig = deps.getModelConfig('planning');
12873
- const insightModelConfig = deps.getModelConfig('insight');
12874
- const curlCommands = buildModelVerifyCurlCommands([
12875
- {
12876
- intent: 'default',
12877
- modelConfig: defaultModelConfig
12676
+ await new Promise((resolve, reject)=>{
12677
+ server.once('error', reject);
12678
+ server.listen(options.port ?? 0, '127.0.0.1', ()=>{
12679
+ server.removeListener('error', reject);
12680
+ resolve();
12681
+ });
12682
+ });
12683
+ signal.throwIfAborted();
12684
+ 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'
12878
12690
  },
12879
- {
12880
- intent: 'planning',
12881
- modelConfig: planningModelConfig
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'
12882
12709
  },
12883
- {
12884
- intent: 'insight',
12885
- modelConfig: insightModelConfig
12886
- }
12887
- ]);
12888
- const result = await deps.verifyModel({
12889
- defaultModelConfig,
12890
- planningModelConfig,
12891
- insightModelConfig
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'
12892
12720
  });
12893
- if (result.passed) {
12894
- io.stdout('āœ… Model verify passed.');
12895
- return 0;
12896
- }
12897
- io.stderr(formatModelVerifyFailureOutput(result.message, curlCommands));
12898
- return 1;
12899
- } catch (error) {
12900
- const message = error instanceof Error ? error.message : String(error);
12901
- io.stderr([
12902
- MODEL_VERIFY_SEPARATOR,
12903
- 'āŒ Model verify failed with messages:',
12904
- '',
12905
- message,
12906
- MODEL_VERIFY_SEPARATOR
12907
- ].join('\n'));
12908
- return 1;
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
+ };
12756
+ } finally{
12757
+ signal.removeEventListener('abort', abort);
12758
+ server.closeAllConnections();
12759
+ await new Promise((resolve)=>server.close(()=>resolve()));
12760
+ }
12761
+ }
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.');
12909
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
+ const USAGE = `Usage:
12812
+ midscene model siwc login [--no-open] [--port <port>]
12813
+ midscene model siwc refresh
12814
+
12815
+ Sign in with ChatGPT or refresh the saved credentials.
12816
+ Full credentials are saved to ~/.midscene/siwc.json. Only the access token is printed to stdout.
12817
+ Midscene does not collect your credentials.
12818
+
12819
+ Options (login):
12820
+ --no-open Print the authorization URL without opening a browser
12821
+ --port <port> Loopback callback port (default: an available port)
12822
+ --help, -h Show this help
12823
+ `;
12824
+ async function openBrowser(url) {
12825
+ const command = 'darwin' === process.platform ? 'open' : 'win32' === process.platform ? 'rundll32' : 'xdg-open';
12826
+ const args = 'win32' === process.platform ? [
12827
+ 'url.dll,FileProtocolHandler',
12828
+ url
12829
+ ] : [
12830
+ url
12831
+ ];
12832
+ await new Promise((resolve, reject)=>{
12833
+ const child = (0, external_node_child_process_namespaceObject.spawn)(command, args, {
12834
+ stdio: 'ignore',
12835
+ shell: false
12836
+ });
12837
+ child.once('error', reject);
12838
+ child.once('spawn', ()=>{
12839
+ child.unref();
12840
+ resolve();
12841
+ });
12842
+ });
12910
12843
  }
12911
- async function runModelCommand(rawArgs, deps, io = {
12844
+ async function runSiwcCommand(args, io = {
12912
12845
  stdout: console.log,
12913
12846
  stderr: console.error
12914
- }) {
12915
- const [, action, ...restArgs] = rawArgs;
12916
- const mergedDeps = {
12917
- ...getDefaultDeps(io),
12918
- ...deps
12919
- };
12920
- if (!action || '--help' === action || '-h' === action) {
12921
- io.stdout(MODEL_COMMAND_USAGE);
12847
+ }, credentialsPath = defaultCredentialsPath()) {
12848
+ if (!args.length || args.includes('--help') || args.includes('-h')) {
12849
+ io.stdout(USAGE);
12922
12850
  return 0;
12923
12851
  }
12924
- if ('verify' === action) return runModelVerifyCommand(restArgs, mergedDeps, io);
12925
- if ('eval' === action) {
12926
- io.stderr('midscene model eval is not implemented yet. It is reserved for future model evaluation suites.');
12852
+ const [action, ...options] = args;
12853
+ if ('login' !== action && 'refresh' !== action) {
12854
+ io.stderr(`Unknown SIWC command: ${action}\n${USAGE}`);
12927
12855
  return 1;
12928
12856
  }
12929
- io.stderr(`Unknown midscene model command: ${action}\n\n${MODEL_COMMAND_USAGE}`);
12930
- return 1;
12857
+ const portOptions = {
12858
+ port: 0,
12859
+ noOpen: false
12860
+ };
12861
+ const seen = new Set();
12862
+ for(let index = 0; index < options.length; index++){
12863
+ const option = options[index];
12864
+ if ('login' !== action || seen.has(option) || ![
12865
+ '--port',
12866
+ '--no-open'
12867
+ ].includes(option)) {
12868
+ io.stderr(`Invalid midscene model siwc arguments.\n${USAGE}`);
12869
+ return 1;
12870
+ }
12871
+ seen.add(option);
12872
+ if ('--no-open' === option) portOptions.noOpen = true;
12873
+ else {
12874
+ const value = options[++index];
12875
+ if (!value || !/^\d+$/.test(value) || Number(value) > 65535) {
12876
+ io.stderr('--port must be an integer between 0 and 65535.');
12877
+ return 1;
12878
+ }
12879
+ portOptions.port = Number(value);
12880
+ }
12881
+ }
12882
+ const unlock = await lockCredentials(credentialsPath).catch((error)=>{
12883
+ io.stderr(error.message);
12884
+ });
12885
+ if (!unlock) return 1;
12886
+ const controller = new AbortController();
12887
+ const cancel = ()=>controller.abort();
12888
+ const timer = setTimeout(cancel, 300000);
12889
+ process.once('SIGINT', cancel);
12890
+ process.once('SIGTERM', cancel);
12891
+ try {
12892
+ io.stderr(source_default().cyan.bold(`šŸ”„ Starting OpenAI SIWC ${action}...`));
12893
+ const saved = await readRegistration(credentialsPath);
12894
+ const credentials = await (async ()=>{
12895
+ if ('refresh' === action) return refreshOpenAICredentials(requireCredentials(saved), controller.signal);
12896
+ const registration = saved ?? {
12897
+ ext_agent_host_id: `urn:uuid:${(0, external_node_crypto_.randomUUID)()}`
12898
+ };
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.');
12911
+ }
12912
+ }
12913
+ });
12914
+ if (registration.subject && result.subject !== registration.subject) throw new Error('Login identity does not match the saved account.');
12915
+ return result;
12916
+ })();
12917
+ await saveRegistration(credentialsPath, credentials);
12918
+ io.stderr('\nšŸ”‘ your Access token:');
12919
+ io.stdout(credentials.access_token);
12920
+ const expiresAt = new Intl.DateTimeFormat('en-GB', {
12921
+ dateStyle: 'medium',
12922
+ timeStyle: 'long'
12923
+ }).format(credentials.expires_at);
12924
+ const remainingMinutes = Math.ceil((credentials.expires_at - Date.now()) / 60000);
12925
+ const expiresIn = new Intl.RelativeTimeFormat('en', {
12926
+ numeric: 'always'
12927
+ }).format(60 === remainingMinutes ? 1 : remainingMinutes, 60 === remainingMinutes ? 'hour' : 'minute');
12928
+ io.stderr(`\nExpires at: ${expiresAt} (${expiresIn})`);
12929
+ 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
+ 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.`));
12933
+ return 1;
12934
+ } finally{
12935
+ clearTimeout(timer);
12936
+ process.removeListener('SIGINT', cancel);
12937
+ process.removeListener('SIGTERM', cancel);
12938
+ await unlock();
12939
+ }
12931
12940
  }
12941
+ const runModelCommand = (rawArgs, deps, io = {
12942
+ stdout: console.log,
12943
+ stderr: console.error
12944
+ })=>{
12945
+ const [, action, ...restArgs] = rawArgs;
12946
+ if ('siwc' === action) return runSiwcCommand(restArgs, io);
12947
+ const withSiwcUsage = (message)=>message.replace(' midscene model verify\n', ' midscene model verify\n midscene model siwc <login|refresh> [options]\n');
12948
+ return (0, model_command_namespaceObject.runModelCommand)(rawArgs, {
12949
+ loadDotenv: ()=>(0, model_command_namespaceObject.loadDotenvConfig)({
12950
+ dotenvDebug: true,
12951
+ dotenvOverride: true,
12952
+ log: io.stdout
12953
+ }),
12954
+ ...deps
12955
+ }, {
12956
+ stdout: (message)=>io.stdout(withSiwcUsage(message)),
12957
+ stderr: (message)=>io.stderr(withSiwcUsage(message))
12958
+ }, 'midscene');
12959
+ };
12932
12960
  const test_runner_namespaceObject = require("@midscene/core/internal/test-runner");
12933
12961
  const probeWorkflowFormat = (file)=>{
12934
12962
  try {
@@ -13036,7 +13064,7 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
13036
13064
  ] : [],
13037
13065
  ...config.files
13038
13066
  ]);
13039
- loadDotenvConfig({
13067
+ (0, model_command_namespaceObject.loadDotenvConfig)({
13040
13068
  dotenvDebug: config.dotenvDebug,
13041
13069
  dotenvOverride: config.dotenvOverride,
13042
13070
  log: console.log
@@ -13067,10 +13095,6 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
13067
13095
  "use strict";
13068
13096
  module.exports = require("assert");
13069
13097
  },
13070
- crypto (module) {
13071
- "use strict";
13072
- module.exports = require("crypto");
13073
- },
13074
13098
  fs (module) {
13075
13099
  "use strict";
13076
13100
  module.exports = require("fs");
@@ -13079,6 +13103,10 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
13079
13103
  "use strict";
13080
13104
  module.exports = require("lodash.merge");
13081
13105
  },
13106
+ "node:crypto" (module) {
13107
+ "use strict";
13108
+ module.exports = require("node:crypto");
13109
+ },
13082
13110
  "node:fs" (module) {
13083
13111
  "use strict";
13084
13112
  module.exports = require("node:fs");
@@ -16384,10 +16412,6 @@ Native cases/steps Test projects use the midscene-test command.`).wrap(yargs().t
16384
16412
  YError: e1
16385
16413
  };
16386
16414
  module.exports = ue;
16387
- },
16388
- "../../node_modules/.pnpm/dotenv@16.4.5/node_modules/dotenv/package.json" (module) {
16389
- "use strict";
16390
- module.exports = JSON.parse('{"name":"dotenv","version":"16.4.5","description":"Loads environment variables from .env file","main":"lib/main.js","types":"lib/main.d.ts","exports":{".":{"types":"./lib/main.d.ts","require":"./lib/main.js","default":"./lib/main.js"},"./config":"./config.js","./config.js":"./config.js","./lib/env-options":"./lib/env-options.js","./lib/env-options.js":"./lib/env-options.js","./lib/cli-options":"./lib/cli-options.js","./lib/cli-options.js":"./lib/cli-options.js","./package.json":"./package.json"},"scripts":{"dts-check":"tsc --project tests/types/tsconfig.json","lint":"standard","lint-readme":"standard-markdown","pretest":"npm run lint && npm run dts-check","test":"tap tests/*.js --100 -Rspec","test:coverage":"tap --coverage-report=lcov","prerelease":"npm test","release":"standard-version"},"repository":{"type":"git","url":"git://github.com/motdotla/dotenv.git"},"funding":"https://dotenvx.com","keywords":["dotenv","env",".env","environment","variables","config","settings"],"readmeFilename":"README.md","license":"BSD-2-Clause","devDependencies":{"@definitelytyped/dtslint":"^0.0.133","@types/node":"^18.11.3","decache":"^4.6.1","sinon":"^14.0.1","standard":"^17.0.0","standard-markdown":"^7.1.0","standard-version":"^9.5.0","tap":"^16.3.0","tar":"^6.1.11","typescript":"^4.8.4"},"engines":{"node":">=12"},"browser":{"fs":false}}');
16391
16415
  }
16392
16416
  };
16393
16417
  var __webpack_module_cache__ = {};