subto 8.0.5 → 9.0.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/package/index.js +1 -1
- package/index.js +30 -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.0' };
|
|
15
15
|
|
|
16
16
|
function configFilePath() { return CONFIG_PATH; }
|
|
17
17
|
|
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.0' };
|
|
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+');
|
|
@@ -398,12 +401,12 @@ async function printFullReport(data) {
|
|
|
398
401
|
function copyToClipboard(text){
|
|
399
402
|
try{
|
|
400
403
|
if (process.platform === 'darwin'){
|
|
401
|
-
|
|
402
|
-
p.stdin && p.stdin.end(String(text));
|
|
404
|
+
cp.spawnSync('pbcopy', { input: String(text) });
|
|
403
405
|
return true;
|
|
404
406
|
}
|
|
405
407
|
if (process.platform === 'win32'){
|
|
406
|
-
|
|
408
|
+
cp.spawnSync('clip', { input: String(text) });
|
|
409
|
+
return true;
|
|
407
410
|
}
|
|
408
411
|
}catch(e){}
|
|
409
412
|
return false;
|
|
@@ -429,6 +432,8 @@ async function callOpenAI(prompt){
|
|
|
429
432
|
const fetchFn = global.fetch;
|
|
430
433
|
if(typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
431
434
|
|
|
435
|
+
// Try OpenAI first if key is available
|
|
436
|
+
if (openaiKey) {
|
|
432
437
|
const model = process.env.AI_MODEL || process.env.OPENAI_MODEL || 'gpt-4o-mini';
|
|
433
438
|
const body = {
|
|
434
439
|
model,
|
|
@@ -444,7 +449,8 @@ async function callOpenAI(prompt){
|
|
|
444
449
|
// Avoid including response bodies in errors to prevent accidental leakage of sensitive data
|
|
445
450
|
throw new Error(`OpenAI API error ${res.status}: ${res.statusText || 'request failed'}`);
|
|
446
451
|
}
|
|
447
|
-
|
|
452
|
+
let j;
|
|
453
|
+
try { j = await res.json(); } catch (_) { throw new Error('OpenAI returned non-JSON response'); }
|
|
448
454
|
const msg = j && j.choices && j.choices[0] && (j.choices[0].message && j.choices[0].message.content || j.choices[0].text);
|
|
449
455
|
return String(msg || '').trim();
|
|
450
456
|
}
|
|
@@ -466,7 +472,8 @@ async function callOpenAI(prompt){
|
|
|
466
472
|
// Avoid including response bodies in errors to prevent accidental leakage of sensitive data
|
|
467
473
|
throw new Error(`OpenRouter API error ${res.status}: ${res.statusText || 'request failed'}`);
|
|
468
474
|
}
|
|
469
|
-
|
|
475
|
+
let j;
|
|
476
|
+
try { j = await res.json(); } catch (_) { throw new Error('OpenRouter returned non-JSON response'); }
|
|
470
477
|
// OpenRouter responses mimic OpenAI shape (choices[0].message.content)
|
|
471
478
|
const msg = j && j.choices && j.choices[0] && (j.choices[0].message && j.choices[0].message.content || j.choices[0].text);
|
|
472
479
|
return String(msg || '').trim();
|
|
@@ -570,7 +577,7 @@ async function startChatREPL(scanData){
|
|
|
570
577
|
|
|
571
578
|
async function run(argv) {
|
|
572
579
|
const program = new Command();
|
|
573
|
-
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '
|
|
580
|
+
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '9.0.0');
|
|
574
581
|
program.option('-v, --verbose', 'Show verbose HTTP logs');
|
|
575
582
|
program.option('--debug', 'Show debug HTTP headers and responses');
|
|
576
583
|
program.option('--chat', 'Start local AI assistant (no command required)');
|
|
@@ -888,7 +895,14 @@ async function run(argv) {
|
|
|
888
895
|
let recheckedAfterTerminal = false;
|
|
889
896
|
let data = null;
|
|
890
897
|
startSpinner();
|
|
898
|
+
const MAX_POLL_ITERATIONS = 900; // ~30 min at 2s interval
|
|
899
|
+
let pollIteration = 0;
|
|
891
900
|
while (true) {
|
|
901
|
+
if (++pollIteration > MAX_POLL_ITERATIONS) {
|
|
902
|
+
stopSpinner();
|
|
903
|
+
console.error(chalk.red('Polling timed out after ' + MAX_POLL_ITERATIONS + ' iterations. The scan may still be running on the server.'));
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
892
906
|
const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
|
|
893
907
|
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
894
908
|
|
|
@@ -1150,7 +1164,7 @@ async function run(argv) {
|
|
|
1150
1164
|
const endpoint = new URL('/api/v1/upload', base).toString();
|
|
1151
1165
|
const fetchFn = global.fetch; if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
1152
1166
|
const body = { files: samples, meta: { collected: collected.length, totalBytes } };
|
|
1153
|
-
const r = await fetchWithKey(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }, cfg.apiKey,
|
|
1167
|
+
const r = await fetchWithKey(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }, cfg.apiKey, Boolean(program.opts && program.opts().debug));
|
|
1154
1168
|
if (!r.ok) {
|
|
1155
1169
|
const txt = await r.text().catch(()=>null);
|
|
1156
1170
|
console.error(chalk.red('Upload failed:'), r.status, txt || r.statusText);
|
|
@@ -1162,13 +1176,14 @@ async function run(argv) {
|
|
|
1162
1176
|
} catch (e) { /* ignore */ }
|
|
1163
1177
|
process.exit(1);
|
|
1164
1178
|
}
|
|
1165
|
-
|
|
1179
|
+
let j; try { j = await r.json(); } catch (_) { console.error(chalk.red('Server returned non-JSON response')); process.exit(1); }
|
|
1180
|
+
console.log(chalk.green('Upload queued:'), j.uploadId, 'scanId:', j.scanId, 'expiresAt:', j.expiresAt ? new Date(j.expiresAt).toString() : 'N/A');
|
|
1166
1181
|
if (opts.wait) {
|
|
1167
1182
|
// Poll the scan resource until completed (reuse existing polling behavior)
|
|
1168
1183
|
const statusUrl = new URL(`scan/${j.scanId}`, SUBTO_API_BASE_SLASH).toString();
|
|
1169
1184
|
const sleep = ms => new Promise(r=>setTimeout(r,ms));
|
|
1170
1185
|
while (true) {
|
|
1171
|
-
const s = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey,
|
|
1186
|
+
const s = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, Boolean(program.opts && program.opts().debug));
|
|
1172
1187
|
if (!s.ok) { console.error(chalk.red('Failed to fetch scan status:', s.status)); break; }
|
|
1173
1188
|
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; }
|
|
1174
1189
|
console.log(chalk.dim('Scan status:'), data.status || 'queued', '— polling again in 4s'); await sleep(4000);
|
|
@@ -1202,7 +1217,7 @@ async function run(argv) {
|
|
|
1202
1217
|
const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
|
|
1203
1218
|
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
1204
1219
|
if (!r.ok) { throw new Error(`Failed to fetch scan ${scanId}: ${r.status}`); }
|
|
1205
|
-
scanData = await r.json();
|
|
1220
|
+
try { scanData = await r.json(); } catch (_) { throw new Error('Server returned non-JSON response for scan ' + scanId); }
|
|
1206
1221
|
}
|
|
1207
1222
|
|
|
1208
1223
|
if (!scanData) { console.error(chalk.red('No scan data available.')); return; }
|
|
@@ -1379,7 +1394,7 @@ async function run(argv) {
|
|
|
1379
1394
|
const endpoint = new URL(`video/${scanId}/debug`, serverBase).toString();
|
|
1380
1395
|
const res = await fetchFn(endpoint, { headers: { 'Accept': 'application/json' } });
|
|
1381
1396
|
if (!res.ok) throw new Error(`Server returned ${res.status}`);
|
|
1382
|
-
|
|
1397
|
+
let json; try { json = await res.json(); } catch (_) { throw new Error('Server returned non-JSON response'); }
|
|
1383
1398
|
console.log(chalk.bold('\nVideo diagnostic for:'), scanId);
|
|
1384
1399
|
if (json && json.scan) {
|
|
1385
1400
|
console.log(' URL:', json.scan.url || '(unknown)');
|
|
@@ -1417,9 +1432,9 @@ async function run(argv) {
|
|
|
1417
1432
|
const base = SUBTO_HOST_BASE; const fetchFn = global.fetch;
|
|
1418
1433
|
if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
1419
1434
|
const statusUrl = new URL(`scan/${answer}`, SUBTO_API_BASE_SLASH).toString();
|
|
1420
|
-
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey,
|
|
1435
|
+
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, false);
|
|
1421
1436
|
if (!r.ok) throw new Error(`Failed to fetch scan ${answer}: ${r.status}`);
|
|
1422
|
-
scanData = await r.json();
|
|
1437
|
+
try { scanData = await r.json(); } catch (_) { throw new Error('Server returned non-JSON response'); }
|
|
1423
1438
|
}
|
|
1424
1439
|
await startChatREPL(scanData);
|
|
1425
1440
|
return;
|