@midscene/cli 1.16.2 → 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/es/index.mjs +73 -170
- package/dist/es/index.mjs.map +1 -1
- package/dist/lib/framework/index.js +6 -2
- package/dist/lib/framework/index.js.map +1 -1
- package/dist/lib/index.js +78 -170
- package/dist/lib/index.js.map +1 -1
- package/package.json +10 -10
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,
|
|
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";
|
|
@@ -2801,7 +2801,7 @@ var __webpack_modules__ = {
|
|
|
2801
2801
|
},
|
|
2802
2802
|
"./src/index.ts" (__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
|
|
2803
2803
|
var package_namespaceObject = {
|
|
2804
|
-
rE: "1.16.
|
|
2804
|
+
rE: "1.16.5"
|
|
2805
2805
|
};
|
|
2806
2806
|
var brace_expansion = __webpack_require__("../../node_modules/.pnpm/brace-expansion@2.0.1/node_modules/brace-expansion/index.js");
|
|
2807
2807
|
const MAX_PATTERN_LENGTH = 65536;
|
|
@@ -8475,7 +8475,7 @@ Usage:
|
|
|
8475
8475
|
type: 'boolean',
|
|
8476
8476
|
description: 'Whether to dismiss the HarmonyOS keyboard after text input'
|
|
8477
8477
|
}
|
|
8478
|
-
}).version('version', 'Show version number', "1.16.
|
|
8478
|
+
}).version('version', 'Show version number', "1.16.5").help().epilogue(`For complete list of configuration options, please visit:
|
|
8479
8479
|
• Web options: https://midscenejs.com/automate-with-scripts-in-yaml#the-web-part
|
|
8480
8480
|
• Android options: https://midscenejs.com/automate-with-scripts-in-yaml#the-android-part
|
|
8481
8481
|
• HarmonyOS options: https://midscenejs.com/automate-with-scripts-in-yaml#the-harmony-part
|
|
@@ -12490,19 +12490,31 @@ defineYamlBatchTest(test, testOptions);
|
|
|
12490
12490
|
const success = printExecutionSummary(results, summaryPath);
|
|
12491
12491
|
return success ? exitCode : 1;
|
|
12492
12492
|
}
|
|
12493
|
+
function describeSiwcError(error) {
|
|
12494
|
+
return inspect(error, {
|
|
12495
|
+
depth: null,
|
|
12496
|
+
colors: false,
|
|
12497
|
+
maxArrayLength: null,
|
|
12498
|
+
maxStringLength: null
|
|
12499
|
+
});
|
|
12500
|
+
}
|
|
12493
12501
|
const defaultCredentialsPath = ()=>join(homedir(), '.midscene', 'siwc.json');
|
|
12494
12502
|
async function readCredentialsFile(path) {
|
|
12495
12503
|
const content = await readFile(path, 'utf8').catch((error)=>{
|
|
12496
12504
|
if ('ENOENT' === error.code) return;
|
|
12497
|
-
throw new
|
|
12505
|
+
throw new SiwcError('Unable to read the local SIWC credentials file.', {
|
|
12506
|
+
cause: error
|
|
12507
|
+
});
|
|
12498
12508
|
});
|
|
12499
12509
|
if (void 0 === content) return;
|
|
12500
12510
|
try {
|
|
12501
12511
|
const value = JSON.parse(content);
|
|
12502
|
-
if (!value || 'string' != typeof value.credentials?.ext_agent_host_id || !value.credentials.ext_agent_host_id) throw new
|
|
12512
|
+
if (!value || 'string' != typeof value.credentials?.ext_agent_host_id || !value.credentials.ext_agent_host_id) throw new SiwcError('Invalid SIWC credentials record.');
|
|
12503
12513
|
return value;
|
|
12504
|
-
} catch
|
|
12505
|
-
throw new
|
|
12514
|
+
} catch (error) {
|
|
12515
|
+
throw new SiwcError('Invalid local SIWC credentials file.', {
|
|
12516
|
+
cause: error
|
|
12517
|
+
});
|
|
12506
12518
|
}
|
|
12507
12519
|
}
|
|
12508
12520
|
async function readRegistration(path) {
|
|
@@ -12541,8 +12553,10 @@ defineYamlBatchTest(test, testOptions);
|
|
|
12541
12553
|
mode: 448
|
|
12542
12554
|
});
|
|
12543
12555
|
} catch (error) {
|
|
12544
|
-
if ('EEXIST' === error.code) throw new
|
|
12545
|
-
throw new
|
|
12556
|
+
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.`);
|
|
12557
|
+
throw new SiwcError('Unable to lock the local SIWC credentials file.', {
|
|
12558
|
+
cause: error
|
|
12559
|
+
});
|
|
12546
12560
|
}
|
|
12547
12561
|
return ()=>rm(lockPath, {
|
|
12548
12562
|
recursive: true,
|
|
@@ -12557,18 +12571,13 @@ defineYamlBatchTest(test, testOptions);
|
|
|
12557
12571
|
'access_token',
|
|
12558
12572
|
'refresh_token',
|
|
12559
12573
|
'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
|
|
12574
|
+
].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
12575
|
return value;
|
|
12562
12576
|
}
|
|
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
12577
|
async function loginWithOpenAI(options, dependencies) {
|
|
12567
12578
|
const hostId = options.hostId ?? `urn:uuid:${randomUUID()}`;
|
|
12568
12579
|
const existingClientId = options.clientId;
|
|
12569
|
-
const
|
|
12570
|
-
const nonce = randomBytes(32).toString('base64url');
|
|
12571
|
-
const verifier = randomBytes(32).toString('base64url');
|
|
12580
|
+
const context = {};
|
|
12572
12581
|
const signal = options.signal;
|
|
12573
12582
|
signal.throwIfAborted();
|
|
12574
12583
|
const server = createServer();
|
|
@@ -12582,17 +12591,24 @@ defineYamlBatchTest(test, testOptions);
|
|
|
12582
12591
|
response.setHeader('Referrer-Policy', 'no-referrer');
|
|
12583
12592
|
const url = new URL(request.url || '/', 'http://127.0.0.1');
|
|
12584
12593
|
if ('GET' !== request.method || '/auth/callback' !== url.pathname) return void response.writeHead(404).end('Not found');
|
|
12585
|
-
if (
|
|
12594
|
+
if (!context.attempt) return void response.writeHead(400).end('Authorization has not started.');
|
|
12586
12595
|
if (consumed.value) return void response.writeHead(409).end('Authorization already received.');
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12596
|
+
const parsed = (()=>{
|
|
12597
|
+
try {
|
|
12598
|
+
return parseSiwcCallback(new URL(request.url || '/', context.attempt.redirectUri).toString(), context.attempt);
|
|
12599
|
+
} catch (error) {
|
|
12600
|
+
response.writeHead(400).end('Invalid authorization callback.');
|
|
12601
|
+
if (url.searchParams.has('error')) {
|
|
12602
|
+
consumed.value = true;
|
|
12603
|
+
reject(error);
|
|
12604
|
+
}
|
|
12605
|
+
return;
|
|
12606
|
+
}
|
|
12607
|
+
})();
|
|
12608
|
+
if (!parsed) {
|
|
12609
|
+
if (!response.writableEnded) response.writeHead(400).end('Invalid login state. Use the original authorization link.');
|
|
12591
12610
|
return;
|
|
12592
12611
|
}
|
|
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
12612
|
consumed.value = true;
|
|
12597
12613
|
response.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
12598
12614
|
response.end(`<!doctype html>
|
|
@@ -12618,10 +12634,7 @@ defineYamlBatchTest(test, testOptions);
|
|
|
12618
12634
|
</main>
|
|
12619
12635
|
</body>
|
|
12620
12636
|
</html>`);
|
|
12621
|
-
resolve(
|
|
12622
|
-
code,
|
|
12623
|
-
clientId
|
|
12624
|
-
});
|
|
12637
|
+
resolve(parsed);
|
|
12625
12638
|
});
|
|
12626
12639
|
server.on('error', reject);
|
|
12627
12640
|
});
|
|
@@ -12640,132 +12653,22 @@ defineYamlBatchTest(test, testOptions);
|
|
|
12640
12653
|
});
|
|
12641
12654
|
signal.throwIfAborted();
|
|
12642
12655
|
const redirectUri = `http://127.0.0.1:${server.address().port}/auth/callback`;
|
|
12643
|
-
const
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
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'
|
|
12656
|
+
const { attempt, url } = await createSiwcAuthorization({
|
|
12657
|
+
hostId,
|
|
12658
|
+
clientId: existingClientId,
|
|
12659
|
+
redirectUri,
|
|
12660
|
+
agentName: 'Midscene'
|
|
12678
12661
|
});
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
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
|
-
};
|
|
12662
|
+
context.attempt = attempt;
|
|
12663
|
+
await options.onAuthorization(url);
|
|
12664
|
+
const result = await withSiwcStage('Authorization callback', ()=>callback);
|
|
12665
|
+
return await exchangeSiwcCode(attempt, result, signal, dependencies);
|
|
12714
12666
|
} finally{
|
|
12715
12667
|
signal.removeEventListener('abort', abort);
|
|
12716
12668
|
server.closeAllConnections();
|
|
12717
12669
|
await new Promise((resolve)=>server.close(()=>resolve()));
|
|
12718
12670
|
}
|
|
12719
12671
|
}
|
|
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
12672
|
const USAGE = `Usage:
|
|
12770
12673
|
midscene model siwc login [--no-open] [--port <port>]
|
|
12771
12674
|
midscene model siwc refresh
|
|
@@ -12838,7 +12741,7 @@ Options (login):
|
|
|
12838
12741
|
}
|
|
12839
12742
|
}
|
|
12840
12743
|
const unlock = await lockCredentials(credentialsPath).catch((error)=>{
|
|
12841
|
-
io.stderr(error
|
|
12744
|
+
io.stderr(describeSiwcError(error));
|
|
12842
12745
|
});
|
|
12843
12746
|
if (!unlock) return 1;
|
|
12844
12747
|
const controller = new AbortController();
|
|
@@ -12848,31 +12751,31 @@ Options (login):
|
|
|
12848
12751
|
process.once('SIGTERM', cancel);
|
|
12849
12752
|
try {
|
|
12850
12753
|
io.stderr(source_default().cyan.bold(`🔄 Starting OpenAI SIWC ${action}...`));
|
|
12851
|
-
const saved = await readRegistration(credentialsPath);
|
|
12754
|
+
const saved = await withSiwcStage('Read credentials', ()=>readRegistration(credentialsPath));
|
|
12852
12755
|
const credentials = await (async ()=>{
|
|
12853
|
-
if ('refresh' === action) return
|
|
12756
|
+
if ('refresh' === action) return withSiwcStage('Refresh', ()=>refreshSiwcCredentials(requireCredentials(saved), controller.signal));
|
|
12854
12757
|
const registration = saved ?? {
|
|
12855
12758
|
ext_agent_host_id: `urn:uuid:${randomUUID()}`
|
|
12856
12759
|
};
|
|
12857
|
-
if (!saved) await saveRegistration(credentialsPath, registration);
|
|
12858
|
-
const result = await loginWithOpenAI({
|
|
12859
|
-
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12760
|
+
if (!saved) await withSiwcStage('Save registration', ()=>saveRegistration(credentialsPath, registration));
|
|
12761
|
+
const result = await withSiwcStage('Login', ()=>loginWithOpenAI({
|
|
12762
|
+
port: portOptions.port,
|
|
12763
|
+
hostId: registration.ext_agent_host_id,
|
|
12764
|
+
clientId: registration.client_id,
|
|
12765
|
+
signal: controller.signal,
|
|
12766
|
+
onAuthorization: async (url)=>{
|
|
12767
|
+
io.stderr(`\n${source_default().bold('Continue with ChatGPT:')}\n${url}\n\n${source_default().gray('Waiting for authorization...')}`);
|
|
12768
|
+
if (!portOptions.noOpen) try {
|
|
12769
|
+
await openBrowser(url);
|
|
12770
|
+
} catch {
|
|
12771
|
+
io.stderr('Open the URL above in your browser to continue.');
|
|
12772
|
+
}
|
|
12869
12773
|
}
|
|
12870
|
-
}
|
|
12871
|
-
|
|
12872
|
-
if (registration.subject && result.subject !== registration.subject) throw new Error('Login identity does not match the saved account.');
|
|
12774
|
+
}));
|
|
12775
|
+
if (registration.subject && result.subject !== registration.subject) throw new SiwcError('Login identity does not match the saved account.');
|
|
12873
12776
|
return result;
|
|
12874
12777
|
})();
|
|
12875
|
-
await saveRegistration(credentialsPath, credentials);
|
|
12778
|
+
await withSiwcStage('Save credentials', ()=>saveRegistration(credentialsPath, credentials));
|
|
12876
12779
|
io.stderr('\n🔑 your Access token:');
|
|
12877
12780
|
io.stdout(credentials.access_token);
|
|
12878
12781
|
const expiresAt = new Intl.DateTimeFormat('en-GB', {
|
|
@@ -12886,8 +12789,8 @@ Options (login):
|
|
|
12886
12789
|
io.stderr(`\nExpires at: ${expiresAt} (${expiresIn})`);
|
|
12887
12790
|
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
12791
|
return 0;
|
|
12889
|
-
} catch
|
|
12890
|
-
io.stderr(source_default().red(controller.signal.aborted ? `OpenAI ${action} cancelled or timed out
|
|
12792
|
+
} catch (error) {
|
|
12793
|
+
io.stderr(source_default().red(controller.signal.aborted ? `OpenAI ${action} cancelled or timed out.\n${describeSiwcError(error)}` : `OpenAI ${action} failed.\n${describeSiwcError(error)}`));
|
|
12891
12794
|
return 1;
|
|
12892
12795
|
} finally{
|
|
12893
12796
|
clearTimeout(timer);
|