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.
@@ -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: '8.0.3' };
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: '8.0.5' };
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 endpoint = new URL('scan', SUBTO_API_BASE_SLASH).toString();
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
- const p = cp.spawnSync('pbcopy');
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
- const p = cp.spawnSync('clip'); p.stdin && p.stdin.end(String(text)); return true;
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
- const j = await res.json();
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
- const j = await res.json();
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 || '8.0.5');
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, DEBUG);
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
- const j = await r.json(); console.log(chalk.green('Upload queued:'), j.uploadId, 'scanId:', j.scanId, 'expiresAt:', new Date(j.expiresAt).toString());
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, DEBUG);
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
- const json = await res.json();
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, DEBUG);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subto",
3
- "version": "8.0.5",
3
+ "version": "9.0.0",
4
4
  "description": "Subto CLI — thin wrapper around the Subto.One API",
5
5
  "bin": {
6
6
  "subto": "bin/subto.js"