subto 8.0.6 → 9.0.1
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/package/index.js +84 -2
- package/index.js +106 -15
- package/package.json +1 -1
package/dist/package/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const chalk = (_chalk && _chalk.default) ? _chalk.default : _chalk;
|
|
|
11
11
|
const CONFIG_DIR = path.join(os.homedir(), '.subto');
|
|
12
12
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
13
13
|
const DEFAULT_API_BASE = 'https://subto.one';
|
|
14
|
-
const CLIENT_META = { name: 'subto-cli', version: '
|
|
14
|
+
const CLIENT_META = { name: 'subto-cli', version: '9.0.1' };
|
|
15
15
|
|
|
16
16
|
function configFilePath() { return CONFIG_PATH; }
|
|
17
17
|
|
|
@@ -122,9 +122,52 @@ function printScanSummary(obj) {
|
|
|
122
122
|
if (keys.length) console.log(chalk.dim(' Additional keys: ' + keys.join(', ')));
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
async function fetchWithKey(url, opts, apiKey) {
|
|
126
|
+
const fetchFn = global.fetch || (function(){ try{ const u = require('undici'); if (u && typeof u.fetch === 'function') { global.fetch = u.fetch; return global.fetch; } } catch(e){} return null; })();
|
|
127
|
+
if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Install Node 18+ or run `npm install undici`.');
|
|
128
|
+
const baseHeaders = (opts && opts.headers) ? Object.assign({}, opts.headers) : {};
|
|
129
|
+
const headers = Object.assign({}, baseHeaders, { 'Authorization': `Bearer ${apiKey}`, 'x-api-key': String(apiKey) });
|
|
130
|
+
return fetchFn(url, Object.assign({}, opts || {}, { headers }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function fetchJsonWithKey(url, opts, apiKey) {
|
|
134
|
+
const res = await fetchWithKey(url, opts, apiKey);
|
|
135
|
+
const text = await res.text();
|
|
136
|
+
let data = null;
|
|
137
|
+
try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; }
|
|
138
|
+
return { status: res.status, headers: res.headers, body: data };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatCount(value) {
|
|
142
|
+
const numeric = Number(value);
|
|
143
|
+
if (!Number.isFinite(numeric)) return '0';
|
|
144
|
+
return numeric.toLocaleString('en-US');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatMemberSince(value) {
|
|
148
|
+
if (!value) return 'Unknown';
|
|
149
|
+
const parsed = new Date(value);
|
|
150
|
+
if (Number.isNaN(parsed.getTime())) return String(value);
|
|
151
|
+
return parsed.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function printAccountSummary(payload) {
|
|
155
|
+
const account = payload && payload.account ? payload.account : {};
|
|
156
|
+
const stats = account && account.stats ? account.stats : {};
|
|
157
|
+
const title = account.name || payload.accountId || payload.userId || 'Account';
|
|
158
|
+
|
|
159
|
+
console.log(chalk.bold('Account summary:'));
|
|
160
|
+
console.log(chalk.green(' Name:') + ' ' + title);
|
|
161
|
+
if (account.email) console.log(chalk.green(' Email:') + ' ' + account.email);
|
|
162
|
+
if (payload.accountId) console.log(chalk.green(' Account ID:') + ' ' + payload.accountId);
|
|
163
|
+
console.log(chalk.green(' API calls:') + ' ' + formatCount(stats.apiCalls));
|
|
164
|
+
console.log(chalk.green(' Scans:') + ' ' + formatCount(stats.scans));
|
|
165
|
+
console.log(chalk.green(' Member since:') + ' ' + formatMemberSince(account.createdAt));
|
|
166
|
+
}
|
|
167
|
+
|
|
125
168
|
async function run(argv) {
|
|
126
169
|
const program = new Command();
|
|
127
|
-
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '
|
|
170
|
+
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '9.0.1');
|
|
128
171
|
|
|
129
172
|
program.command('login').description('Store your API key in ~/.subto/config.json').action(async () => {
|
|
130
173
|
try {
|
|
@@ -143,6 +186,45 @@ async function run(argv) {
|
|
|
143
186
|
} catch (err) { if (err && err.message === 'Aborted') { console.log('\n' + chalk.yellow('Login aborted.')); process.exit(1); } throw err; }
|
|
144
187
|
});
|
|
145
188
|
|
|
189
|
+
program
|
|
190
|
+
.command('account')
|
|
191
|
+
.description('Show the current account name, API calls, and scans')
|
|
192
|
+
.option('--json', 'Output raw JSON')
|
|
193
|
+
.action(async (opts) => {
|
|
194
|
+
const cfg = await readConfig();
|
|
195
|
+
if (!cfg || !cfg.apiKey) {
|
|
196
|
+
console.error(chalk.red('Missing API key. Run:'), chalk.cyan('subto login'));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const endpoint = new URL('/api/v1/account/me', process.env.SUBTO_API_BASE_URL || DEFAULT_API_BASE).toString();
|
|
202
|
+
const resp = await fetchJsonWithKey(endpoint, { headers: { 'Accept': 'application/json' } }, cfg.apiKey);
|
|
203
|
+
|
|
204
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
205
|
+
console.error(chalk.red('Authentication failed. Check your API key with `subto login`.'));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
209
|
+
let msg = `Request failed with status ${resp.status}`;
|
|
210
|
+
if (resp.body && typeof resp.body === 'object' && resp.body.error) msg += ': ' + resp.body.error;
|
|
211
|
+
console.error(chalk.red(msg));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (opts.json) {
|
|
216
|
+
console.log(JSON.stringify(resp.body, null, 2));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
printAccountSummary(resp.body);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const msg = err && err.message ? err.message : String(err);
|
|
223
|
+
console.error(chalk.red('Network error:'), msg);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
146
228
|
program
|
|
147
229
|
.command('scan <url>')
|
|
148
230
|
.description('Request a scan for <url> via the Subto API')
|
package/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const chalk = (_chalk && _chalk.default) ? _chalk.default : _chalk;
|
|
|
11
11
|
const CONFIG_DIR = path.join(os.homedir(), '.subto');
|
|
12
12
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
13
13
|
const DEFAULT_API_BASE = 'https://subto.one/api/v1';
|
|
14
|
-
const CLIENT_META = { name: 'subto-cli', version: '
|
|
14
|
+
const CLIENT_META = { name: 'subto-cli', version: '9.0.1' };
|
|
15
15
|
const cp = require('child_process');
|
|
16
16
|
|
|
17
17
|
// Normalize SUBTO API base so callers can set either
|
|
@@ -217,7 +217,10 @@ async function _maskHeaders(obj){
|
|
|
217
217
|
|
|
218
218
|
async function postScan(url, apiKey, debug=false) {
|
|
219
219
|
// Use normalized API base
|
|
220
|
-
const
|
|
220
|
+
const endpointObj = new URL('scan', SUBTO_API_BASE_SLASH);
|
|
221
|
+
// API key is sent via headers only — never in query strings to prevent
|
|
222
|
+
// leakage via server logs, proxy logs, and Referer headers.
|
|
223
|
+
const endpoint = endpointObj.toString();
|
|
221
224
|
const body = { url, source: 'cli', client: CLIENT_META };
|
|
222
225
|
const fetchFn = global.fetch;
|
|
223
226
|
if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
@@ -293,6 +296,41 @@ function printScanSummary(obj) {
|
|
|
293
296
|
if (keys.length) console.log(chalk.dim(' Additional keys: ' + keys.join(', ')));
|
|
294
297
|
}
|
|
295
298
|
|
|
299
|
+
async function fetchJsonWithKey(url, opts, apiKey, debug=false) {
|
|
300
|
+
const res = await fetchWithKey(url, opts, apiKey, debug);
|
|
301
|
+
const text = typeof res._cached_text === 'string' ? res._cached_text : await res.text();
|
|
302
|
+
let data = null;
|
|
303
|
+
try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; }
|
|
304
|
+
return { status: res.status, headers: res.headers, body: data };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function formatCount(value) {
|
|
308
|
+
const numeric = Number(value);
|
|
309
|
+
if (!Number.isFinite(numeric)) return '0';
|
|
310
|
+
return numeric.toLocaleString('en-US');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function formatMemberSince(value) {
|
|
314
|
+
if (!value) return 'Unknown';
|
|
315
|
+
const parsed = new Date(value);
|
|
316
|
+
if (Number.isNaN(parsed.getTime())) return String(value);
|
|
317
|
+
return parsed.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function printAccountSummary(payload) {
|
|
321
|
+
const account = payload && payload.account ? payload.account : {};
|
|
322
|
+
const stats = account && account.stats ? account.stats : {};
|
|
323
|
+
const title = account.name || payload.accountId || payload.userId || 'Account';
|
|
324
|
+
|
|
325
|
+
console.log(chalk.bold('Account summary:'));
|
|
326
|
+
console.log(chalk.green(' Name:') + ' ' + title);
|
|
327
|
+
if (account.email) console.log(chalk.green(' Email:') + ' ' + account.email);
|
|
328
|
+
if (payload.accountId) console.log(chalk.green(' Account ID:') + ' ' + payload.accountId);
|
|
329
|
+
console.log(chalk.green(' API calls:') + ' ' + formatCount(stats.apiCalls));
|
|
330
|
+
console.log(chalk.green(' Scans:') + ' ' + formatCount(stats.scans));
|
|
331
|
+
console.log(chalk.green(' Member since:') + ' ' + formatMemberSince(account.createdAt));
|
|
332
|
+
}
|
|
333
|
+
|
|
296
334
|
async function printFullReport(data) {
|
|
297
335
|
const scan = (data && data.results) ? data.results : data;
|
|
298
336
|
if (!scan || typeof scan !== 'object') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
@@ -398,12 +436,12 @@ async function printFullReport(data) {
|
|
|
398
436
|
function copyToClipboard(text){
|
|
399
437
|
try{
|
|
400
438
|
if (process.platform === 'darwin'){
|
|
401
|
-
|
|
402
|
-
p.stdin && p.stdin.end(String(text));
|
|
439
|
+
cp.spawnSync('pbcopy', { input: String(text) });
|
|
403
440
|
return true;
|
|
404
441
|
}
|
|
405
442
|
if (process.platform === 'win32'){
|
|
406
|
-
|
|
443
|
+
cp.spawnSync('clip', { input: String(text) });
|
|
444
|
+
return true;
|
|
407
445
|
}
|
|
408
446
|
}catch(e){}
|
|
409
447
|
return false;
|
|
@@ -429,6 +467,8 @@ async function callOpenAI(prompt){
|
|
|
429
467
|
const fetchFn = global.fetch;
|
|
430
468
|
if(typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
431
469
|
|
|
470
|
+
// Try OpenAI first if key is available
|
|
471
|
+
if (openaiKey) {
|
|
432
472
|
const model = process.env.AI_MODEL || process.env.OPENAI_MODEL || 'gpt-4o-mini';
|
|
433
473
|
const body = {
|
|
434
474
|
model,
|
|
@@ -444,9 +484,11 @@ async function callOpenAI(prompt){
|
|
|
444
484
|
// Avoid including response bodies in errors to prevent accidental leakage of sensitive data
|
|
445
485
|
throw new Error(`OpenAI API error ${res.status}: ${res.statusText || 'request failed'}`);
|
|
446
486
|
}
|
|
447
|
-
|
|
487
|
+
let j;
|
|
488
|
+
try { j = await res.json(); } catch (_) { throw new Error('OpenAI returned non-JSON response'); }
|
|
448
489
|
const msg = j && j.choices && j.choices[0] && (j.choices[0].message && j.choices[0].message.content || j.choices[0].text);
|
|
449
490
|
return String(msg || '').trim();
|
|
491
|
+
}
|
|
450
492
|
|
|
451
493
|
if (openrouterKey) {
|
|
452
494
|
// Use OpenRouter chat completions endpoint
|
|
@@ -465,7 +507,8 @@ async function callOpenAI(prompt){
|
|
|
465
507
|
// Avoid including response bodies in errors to prevent accidental leakage of sensitive data
|
|
466
508
|
throw new Error(`OpenRouter API error ${res.status}: ${res.statusText || 'request failed'}`);
|
|
467
509
|
}
|
|
468
|
-
|
|
510
|
+
let j;
|
|
511
|
+
try { j = await res.json(); } catch (_) { throw new Error('OpenRouter returned non-JSON response'); }
|
|
469
512
|
// OpenRouter responses mimic OpenAI shape (choices[0].message.content)
|
|
470
513
|
const msg = j && j.choices && j.choices[0] && (j.choices[0].message && j.choices[0].message.content || j.choices[0].text);
|
|
471
514
|
return String(msg || '').trim();
|
|
@@ -569,7 +612,7 @@ async function startChatREPL(scanData){
|
|
|
569
612
|
|
|
570
613
|
async function run(argv) {
|
|
571
614
|
const program = new Command();
|
|
572
|
-
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '
|
|
615
|
+
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '9.0.1');
|
|
573
616
|
program.option('-v, --verbose', 'Show verbose HTTP logs');
|
|
574
617
|
program.option('--debug', 'Show debug HTTP headers and responses');
|
|
575
618
|
program.option('--chat', 'Start local AI assistant (no command required)');
|
|
@@ -595,6 +638,46 @@ async function run(argv) {
|
|
|
595
638
|
} catch (err) { if (err && err.message === 'Aborted') { console.log('\n' + chalk.yellow('Login aborted.')); process.exit(1); } throw err; }
|
|
596
639
|
});
|
|
597
640
|
|
|
641
|
+
program
|
|
642
|
+
.command('account')
|
|
643
|
+
.description('Show the current account name, API calls, and scans')
|
|
644
|
+
.option('--json', 'Output raw JSON')
|
|
645
|
+
.action(async (opts) => {
|
|
646
|
+
const cfg = await readConfig();
|
|
647
|
+
if (!cfg || !cfg.apiKey) {
|
|
648
|
+
console.error(chalk.red('Missing API key. Run:'), chalk.cyan('subto login'));
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
const DEBUG = (program && typeof program.opts === 'function') ? Boolean(program.opts().debug) : false;
|
|
654
|
+
const endpoint = new URL('account/me', SUBTO_API_BASE_SLASH).toString();
|
|
655
|
+
const resp = await fetchJsonWithKey(endpoint, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
656
|
+
|
|
657
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
658
|
+
console.error(chalk.red('Authentication failed. Check your API key with `subto login`.'));
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
662
|
+
let msg = `Request failed with status ${resp.status}`;
|
|
663
|
+
if (resp.body && typeof resp.body === 'object' && resp.body.error) msg += ': ' + resp.body.error;
|
|
664
|
+
console.error(chalk.red(msg));
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (opts.json) {
|
|
669
|
+
console.log(JSON.stringify(resp.body, null, 2));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
printAccountSummary(resp.body);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
const msg = err && err.message ? err.message : String(err);
|
|
676
|
+
console.error(chalk.red('Network error:'), msg);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
598
681
|
program
|
|
599
682
|
.command('scan <url>')
|
|
600
683
|
.description('Request a scan for <url> via the Subto API')
|
|
@@ -887,7 +970,14 @@ async function run(argv) {
|
|
|
887
970
|
let recheckedAfterTerminal = false;
|
|
888
971
|
let data = null;
|
|
889
972
|
startSpinner();
|
|
973
|
+
const MAX_POLL_ITERATIONS = 900; // ~30 min at 2s interval
|
|
974
|
+
let pollIteration = 0;
|
|
890
975
|
while (true) {
|
|
976
|
+
if (++pollIteration > MAX_POLL_ITERATIONS) {
|
|
977
|
+
stopSpinner();
|
|
978
|
+
console.error(chalk.red('Polling timed out after ' + MAX_POLL_ITERATIONS + ' iterations. The scan may still be running on the server.'));
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
891
981
|
const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
|
|
892
982
|
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
893
983
|
|
|
@@ -1149,7 +1239,7 @@ async function run(argv) {
|
|
|
1149
1239
|
const endpoint = new URL('/api/v1/upload', base).toString();
|
|
1150
1240
|
const fetchFn = global.fetch; if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
1151
1241
|
const body = { files: samples, meta: { collected: collected.length, totalBytes } };
|
|
1152
|
-
const r = await fetchWithKey(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }, cfg.apiKey,
|
|
1242
|
+
const r = await fetchWithKey(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }, cfg.apiKey, Boolean(program.opts && program.opts().debug));
|
|
1153
1243
|
if (!r.ok) {
|
|
1154
1244
|
const txt = await r.text().catch(()=>null);
|
|
1155
1245
|
console.error(chalk.red('Upload failed:'), r.status, txt || r.statusText);
|
|
@@ -1161,13 +1251,14 @@ async function run(argv) {
|
|
|
1161
1251
|
} catch (e) { /* ignore */ }
|
|
1162
1252
|
process.exit(1);
|
|
1163
1253
|
}
|
|
1164
|
-
|
|
1254
|
+
let j; try { j = await r.json(); } catch (_) { console.error(chalk.red('Server returned non-JSON response')); process.exit(1); }
|
|
1255
|
+
console.log(chalk.green('Upload queued:'), j.uploadId, 'scanId:', j.scanId, 'expiresAt:', j.expiresAt ? new Date(j.expiresAt).toString() : 'N/A');
|
|
1165
1256
|
if (opts.wait) {
|
|
1166
1257
|
// Poll the scan resource until completed (reuse existing polling behavior)
|
|
1167
1258
|
const statusUrl = new URL(`scan/${j.scanId}`, SUBTO_API_BASE_SLASH).toString();
|
|
1168
1259
|
const sleep = ms => new Promise(r=>setTimeout(r,ms));
|
|
1169
1260
|
while (true) {
|
|
1170
|
-
const s = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey,
|
|
1261
|
+
const s = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, Boolean(program.opts && program.opts().debug));
|
|
1171
1262
|
if (!s.ok) { console.error(chalk.red('Failed to fetch scan status:', s.status)); break; }
|
|
1172
1263
|
const data = await s.json(); if (data.status && ['completed','done','finished','success'].includes(String(data.status).toLowerCase())) { console.log(chalk.green('Scan complete.')); await printFullReport(data); break; }
|
|
1173
1264
|
console.log(chalk.dim('Scan status:'), data.status || 'queued', '— polling again in 4s'); await sleep(4000);
|
|
@@ -1201,7 +1292,7 @@ async function run(argv) {
|
|
|
1201
1292
|
const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
|
|
1202
1293
|
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
1203
1294
|
if (!r.ok) { throw new Error(`Failed to fetch scan ${scanId}: ${r.status}`); }
|
|
1204
|
-
scanData = await r.json();
|
|
1295
|
+
try { scanData = await r.json(); } catch (_) { throw new Error('Server returned non-JSON response for scan ' + scanId); }
|
|
1205
1296
|
}
|
|
1206
1297
|
|
|
1207
1298
|
if (!scanData) { console.error(chalk.red('No scan data available.')); return; }
|
|
@@ -1378,7 +1469,7 @@ async function run(argv) {
|
|
|
1378
1469
|
const endpoint = new URL(`video/${scanId}/debug`, serverBase).toString();
|
|
1379
1470
|
const res = await fetchFn(endpoint, { headers: { 'Accept': 'application/json' } });
|
|
1380
1471
|
if (!res.ok) throw new Error(`Server returned ${res.status}`);
|
|
1381
|
-
|
|
1472
|
+
let json; try { json = await res.json(); } catch (_) { throw new Error('Server returned non-JSON response'); }
|
|
1382
1473
|
console.log(chalk.bold('\nVideo diagnostic for:'), scanId);
|
|
1383
1474
|
if (json && json.scan) {
|
|
1384
1475
|
console.log(' URL:', json.scan.url || '(unknown)');
|
|
@@ -1416,9 +1507,9 @@ async function run(argv) {
|
|
|
1416
1507
|
const base = SUBTO_HOST_BASE; const fetchFn = global.fetch;
|
|
1417
1508
|
if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
1418
1509
|
const statusUrl = new URL(`scan/${answer}`, SUBTO_API_BASE_SLASH).toString();
|
|
1419
|
-
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey,
|
|
1510
|
+
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, false);
|
|
1420
1511
|
if (!r.ok) throw new Error(`Failed to fetch scan ${answer}: ${r.status}`);
|
|
1421
|
-
scanData = await r.json();
|
|
1512
|
+
try { scanData = await r.json(); } catch (_) { throw new Error('Server returned non-JSON response'); }
|
|
1422
1513
|
}
|
|
1423
1514
|
await startChatREPL(scanData);
|
|
1424
1515
|
return;
|