@spark-apps/quickpeek 1.2.5 → 1.2.6

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.
@@ -296,6 +296,139 @@ var init_image_kind = __esm({
296
296
  }
297
297
  });
298
298
 
299
+ // src/browser/headless.ts
300
+ import { chromium } from "playwright";
301
+ async function launchHeadlessChromium(options = {}) {
302
+ const opts = { ...options, headless: true };
303
+ try {
304
+ return await chromium.launch(opts);
305
+ } catch (bundledError) {
306
+ try {
307
+ return await chromium.launch({ ...opts, channel: "chrome" });
308
+ } catch {
309
+ const detail = bundledError instanceof Error ? bundledError.message : String(bundledError);
310
+ throw new Error(`${NO_BROWSER_MESSAGE}
311
+
312
+ Playwright reported: ${detail}`);
313
+ }
314
+ }
315
+ }
316
+ var NO_BROWSER_MESSAGE;
317
+ var init_headless = __esm({
318
+ "src/browser/headless.ts"() {
319
+ "use strict";
320
+ NO_BROWSER_MESSAGE = "QuickPeek needs a browser to record and could not find one.\n Run this once, then run QuickPeek again:\n npx playwright install chromium";
321
+ }
322
+ });
323
+
324
+ // src/lib/json-repair.ts
325
+ function repairUnicodeEscapes(content) {
326
+ return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
327
+ const code = Number.parseInt(hex, 16);
328
+ return code <= 1114111 ? String.fromCodePoint(code) : match;
329
+ }).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
330
+ }
331
+ function salvageTruncatedJson(content) {
332
+ const stack = [];
333
+ const cuts = [];
334
+ let inString = false;
335
+ let escaped = false;
336
+ const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
337
+ for (let i = 0; i < content.length; i++) {
338
+ const ch = content[i];
339
+ if (inString) {
340
+ if (escaped) escaped = false;
341
+ else if (ch === "\\") escaped = true;
342
+ else if (ch === '"') {
343
+ inString = false;
344
+ record(i + 1, false);
345
+ }
346
+ continue;
347
+ }
348
+ if (ch === '"') {
349
+ inString = true;
350
+ } else if (ch === "{" || ch === "[") {
351
+ stack.push(ch);
352
+ } else if (ch === "}" || ch === "]") {
353
+ stack.pop();
354
+ record(i + 1, true);
355
+ } else if (ch === ",") {
356
+ record(i, content[i - 1] === "}" || content[i - 1] === "]");
357
+ }
358
+ }
359
+ const tryCut = (cut) => {
360
+ const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
361
+ if (head.endsWith(":")) return null;
362
+ let closed = head;
363
+ for (let d = cut.open.length - 1; d >= 0; d--) {
364
+ closed += CLOSERS[cut.open[d]];
365
+ }
366
+ try {
367
+ JSON.parse(closed);
368
+ return closed;
369
+ } catch {
370
+ return null;
371
+ }
372
+ };
373
+ const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
374
+ for (const containerOnly of [true, false]) {
375
+ for (let n = 0; n < attempts; n++) {
376
+ const cut = cuts[cuts.length - 1 - n];
377
+ if (containerOnly && !cut.closesContainer) continue;
378
+ const salvaged = tryCut(cut);
379
+ if (salvaged !== null) return salvaged;
380
+ }
381
+ }
382
+ return null;
383
+ }
384
+ function extractJsonCandidates(content) {
385
+ let text = content.trim();
386
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
387
+ if (fence?.[1]?.trim()) text = fence[1].trim();
388
+ const start = text.search(/[{[]/);
389
+ if (start === -1) return [];
390
+ const closer = text[start] === "{" ? "}" : "]";
391
+ const end = text.lastIndexOf(closer);
392
+ const candidates = [text.slice(start)];
393
+ if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
394
+ return candidates;
395
+ }
396
+ function parseAIJson(content) {
397
+ const candidates = extractJsonCandidates(content);
398
+ if (candidates.length === 0) return null;
399
+ for (const text of candidates) {
400
+ try {
401
+ return { value: JSON.parse(text), repaired: false };
402
+ } catch {
403
+ }
404
+ }
405
+ for (const text of candidates) {
406
+ try {
407
+ return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
408
+ } catch {
409
+ }
410
+ }
411
+ for (const text of candidates) {
412
+ for (const variant of [repairUnicodeEscapes(text), text]) {
413
+ const salvaged = salvageTruncatedJson(variant);
414
+ if (salvaged === null) continue;
415
+ try {
416
+ return { value: JSON.parse(salvaged), repaired: true };
417
+ } catch {
418
+ }
419
+ }
420
+ }
421
+ return null;
422
+ }
423
+ var MAX_SALVAGE_ATTEMPTS, CLOSERS;
424
+ var init_json_repair = __esm({
425
+ "src/lib/json-repair.ts"() {
426
+ "use strict";
427
+ MAX_SALVAGE_ATTEMPTS = 500;
428
+ CLOSERS = { "{": "}", "[": "]" };
429
+ }
430
+ });
431
+
299
432
  // src/lib/api-client.ts
300
433
  function apiKey() {
301
434
  return process.env.SPARK_AI_KEY || "";
@@ -494,24 +627,26 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", op
494
627
  }
495
628
  }
496
629
  function parseAIPlanResponse(content) {
497
- try {
498
- let processedContent = content;
499
- const codeBlockMatch = processedContent.match(/```(?:json)?\s*([\s\S]*?)```/);
500
- if (codeBlockMatch?.[1]) {
501
- processedContent = codeBlockMatch[1].trim();
502
- }
503
- const jsonMatch = processedContent.match(/\{[\s\S]*\}/);
504
- if (!jsonMatch) {
505
- return { success: false, error: "No valid JSON found in AI response" };
506
- }
507
- const parsed = JSON.parse(jsonMatch[0]);
508
- return { success: true, plan: parsed };
509
- } catch (err) {
510
- return {
511
- success: false,
512
- error: `Failed to parse AI response: ${err instanceof Error ? err.message : String(err)}`
513
- };
630
+ const parsed = parseAIJson(content);
631
+ if (!parsed) {
632
+ return { success: false, error: "No valid JSON found in AI response" };
633
+ }
634
+ if (!Array.isArray(parsed.value.steps) || parsed.value.steps.length === 0) {
635
+ return { success: false, error: "AI response has no steps" };
514
636
  }
637
+ return { success: true, plan: { ...parsed.value, steps: parsed.value.steps } };
638
+ }
639
+ async function callAIForPlan(systemPrompt, userPrompt, tier = "free", opts = {}) {
640
+ let lastParseError = "No valid JSON found in AI response";
641
+ for (let attempt = 1; attempt <= 2; attempt++) {
642
+ const aiResult = await callAI(systemPrompt, userPrompt, tier, opts);
643
+ if (!aiResult.success) return aiResult;
644
+ const parsed = parseAIPlanResponse(aiResult.content);
645
+ if (parsed.success) return { success: true, plan: parsed.plan, limits: aiResult.limits };
646
+ lastParseError = parsed.error;
647
+ console.error(`Plan parse failed (attempt ${attempt}/2):`, parsed.error);
648
+ }
649
+ return { success: false, error: lastParseError, isParseError: true };
515
650
  }
516
651
  function normalizeUrl(input) {
517
652
  let url = input.trim();
@@ -522,6 +657,7 @@ var PLAN_MAX_TOKENS, SERVER_BACKOFF_MS, CLI_BACKOFF_MS, SPARK_AI_BASE, SPARK_AI_
522
657
  var init_api_client = __esm({
523
658
  "src/lib/api-client.ts"() {
524
659
  "use strict";
660
+ init_json_repair();
525
661
  init_utils();
526
662
  PLAN_MAX_TOKENS = 1500;
527
663
  SERVER_BACKOFF_MS = [2e3, 4e3];
@@ -692,7 +828,7 @@ var VERSION, CONFIG_FILE, SIZE_PRESETS, RATE_FACTOR, NO_CLIP_OUTRO, DEFAULT_CONF
692
828
  var init_config = __esm({
693
829
  "src/lib/config.ts"() {
694
830
  "use strict";
695
- VERSION = "1.2.5";
831
+ VERSION = "1.2.6";
696
832
  CONFIG_FILE = "quickpeek.config.json";
697
833
  SIZE_PRESETS = {
698
834
  wide: { width: 1920, height: 1080, zoom: 1 },
@@ -884,7 +1020,6 @@ var init_scraping = __esm({
884
1020
  });
885
1021
 
886
1022
  // src/lib/crawl.ts
887
- import { chromium as chromium4 } from "playwright";
888
1023
  async function waitForDomStable(page, cap = SETTLE_CAP_MS) {
889
1024
  const deadline = Date.now() + cap;
890
1025
  let previous = -1;
@@ -1004,7 +1139,13 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
1004
1139
  return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
1005
1140
  }
1006
1141
  const browserTask = task2("Launching browser");
1007
- const browser = await chromium4.launch({ headless: true });
1142
+ let browser;
1143
+ try {
1144
+ browser = await launchHeadlessChromium();
1145
+ } catch (err) {
1146
+ browserTask.succeed("Could not launch a browser");
1147
+ throw err;
1148
+ }
1008
1149
  try {
1009
1150
  const context = await browser.newContext({
1010
1151
  ...storageStatePath ? { storageState: storageStatePath } : {},
@@ -1184,6 +1325,7 @@ var SETTLE_POLL_MS, SETTLE_STABLE_POLLS, SETTLE_CAP_MS, PLACEHOLDER_POLL_MS, PLA
1184
1325
  var init_crawl = __esm({
1185
1326
  "src/lib/crawl.ts"() {
1186
1327
  "use strict";
1328
+ init_headless();
1187
1329
  init_scraping();
1188
1330
  SETTLE_POLL_MS = 400;
1189
1331
  SETTLE_STABLE_POLLS = 2;
@@ -2617,114 +2759,6 @@ var init_ffmpeg2 = __esm({
2617
2759
  }
2618
2760
  });
2619
2761
 
2620
- // src/lib/json-repair.ts
2621
- function repairUnicodeEscapes(content) {
2622
- return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
2623
- const code = Number.parseInt(hex, 16);
2624
- return code <= 1114111 ? String.fromCodePoint(code) : match;
2625
- }).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
2626
- }
2627
- function salvageTruncatedJson(content) {
2628
- const stack = [];
2629
- const cuts = [];
2630
- let inString = false;
2631
- let escaped = false;
2632
- const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
2633
- for (let i = 0; i < content.length; i++) {
2634
- const ch = content[i];
2635
- if (inString) {
2636
- if (escaped) escaped = false;
2637
- else if (ch === "\\") escaped = true;
2638
- else if (ch === '"') {
2639
- inString = false;
2640
- record(i + 1, false);
2641
- }
2642
- continue;
2643
- }
2644
- if (ch === '"') {
2645
- inString = true;
2646
- } else if (ch === "{" || ch === "[") {
2647
- stack.push(ch);
2648
- } else if (ch === "}" || ch === "]") {
2649
- stack.pop();
2650
- record(i + 1, true);
2651
- } else if (ch === ",") {
2652
- record(i, content[i - 1] === "}" || content[i - 1] === "]");
2653
- }
2654
- }
2655
- const tryCut = (cut) => {
2656
- const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
2657
- if (head.endsWith(":")) return null;
2658
- let closed = head;
2659
- for (let d = cut.open.length - 1; d >= 0; d--) {
2660
- closed += CLOSERS[cut.open[d]];
2661
- }
2662
- try {
2663
- JSON.parse(closed);
2664
- return closed;
2665
- } catch {
2666
- return null;
2667
- }
2668
- };
2669
- const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
2670
- for (const containerOnly of [true, false]) {
2671
- for (let n = 0; n < attempts; n++) {
2672
- const cut = cuts[cuts.length - 1 - n];
2673
- if (containerOnly && !cut.closesContainer) continue;
2674
- const salvaged = tryCut(cut);
2675
- if (salvaged !== null) return salvaged;
2676
- }
2677
- }
2678
- return null;
2679
- }
2680
- function extractJsonCandidates(content) {
2681
- let text = content.trim();
2682
- const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
2683
- if (fence?.[1]?.trim()) text = fence[1].trim();
2684
- const start = text.search(/[{[]/);
2685
- if (start === -1) return [];
2686
- const closer = text[start] === "{" ? "}" : "]";
2687
- const end = text.lastIndexOf(closer);
2688
- const candidates = [text.slice(start)];
2689
- if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
2690
- return candidates;
2691
- }
2692
- function parseAIJson(content) {
2693
- const candidates = extractJsonCandidates(content);
2694
- if (candidates.length === 0) return null;
2695
- for (const text of candidates) {
2696
- try {
2697
- return { value: JSON.parse(text), repaired: false };
2698
- } catch {
2699
- }
2700
- }
2701
- for (const text of candidates) {
2702
- try {
2703
- return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
2704
- } catch {
2705
- }
2706
- }
2707
- for (const text of candidates) {
2708
- for (const variant of [repairUnicodeEscapes(text), text]) {
2709
- const salvaged = salvageTruncatedJson(variant);
2710
- if (salvaged === null) continue;
2711
- try {
2712
- return { value: JSON.parse(salvaged), repaired: true };
2713
- } catch {
2714
- }
2715
- }
2716
- }
2717
- return null;
2718
- }
2719
- var MAX_SALVAGE_ATTEMPTS, CLOSERS;
2720
- var init_json_repair = __esm({
2721
- "src/lib/json-repair.ts"() {
2722
- "use strict";
2723
- MAX_SALVAGE_ATTEMPTS = 500;
2724
- CLOSERS = { "{": "}", "[": "]" };
2725
- }
2726
- });
2727
-
2728
2762
  // src/lib/prompts.ts
2729
2763
  function buildSystemPrompt(maxSteps, profile) {
2730
2764
  let scope;
@@ -4104,6 +4138,7 @@ __export(lib_exports, {
4104
4138
  buildUserPrompt: () => buildUserPrompt,
4105
4139
  buildWatermarkFilters: () => buildWatermarkFilters,
4106
4140
  callAI: () => callAI,
4141
+ callAIForPlan: () => callAIForPlan,
4107
4142
  callAIViaRelay: () => callAIViaRelay,
4108
4143
  canReuseVoiceover: () => canReuseVoiceover,
4109
4144
  captionCanvas: () => captionCanvas,
@@ -5445,7 +5480,7 @@ function resolveStartUrl(url) {
5445
5480
  // src/browser/launch.ts
5446
5481
  init_lifecycle();
5447
5482
  import { mkdir as mkdir2 } from "fs/promises";
5448
- import { chromium as chromium2 } from "playwright";
5483
+ import { chromium as chromium3 } from "playwright";
5449
5484
 
5450
5485
  // src/browser/brand-watermark.ts
5451
5486
  init_image_kind();
@@ -5538,11 +5573,14 @@ function clampOpacity(value) {
5538
5573
  return Math.min(1, Math.max(0.35, value));
5539
5574
  }
5540
5575
 
5576
+ // src/browser/launch.ts
5577
+ init_headless();
5578
+
5541
5579
  // src/browser/wsl.ts
5542
5580
  import { execSync, spawn as spawn2 } from "child_process";
5543
5581
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
5544
5582
  import { request } from "http";
5545
- import { chromium } from "playwright";
5583
+ import { chromium as chromium2 } from "playwright";
5546
5584
  function isWSL() {
5547
5585
  if (process.platform !== "linux") return false;
5548
5586
  try {
@@ -5661,7 +5699,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
5661
5699
  const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
5662
5700
  try {
5663
5701
  await waitForChrome(windowsHost, proxyPort);
5664
- const browser = await chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
5702
+ const browser = await chromium2.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
5665
5703
  return {
5666
5704
  browser,
5667
5705
  cleanup: async () => {
@@ -5718,7 +5756,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
5718
5756
  acceptDownloads: true
5719
5757
  });
5720
5758
  } catch {
5721
- context = await chromium2.launchPersistentContext(userDataDir, {
5759
+ context = await chromium3.launchPersistentContext(userDataDir, {
5722
5760
  headless: false,
5723
5761
  args: persistentArgs,
5724
5762
  viewport,
@@ -5729,7 +5767,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
5729
5767
  });
5730
5768
  }
5731
5769
  } else {
5732
- context = await chromium2.launchPersistentContext(userDataDir, {
5770
+ context = await chromium3.launchPersistentContext(userDataDir, {
5733
5771
  headless: false,
5734
5772
  args: persistentArgs,
5735
5773
  viewport,
@@ -5747,12 +5785,12 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
5747
5785
  ignoreDefaultArgs: ["--enable-automation"]
5748
5786
  };
5749
5787
  try {
5750
- browser = await chromium2.launch({ ...headedOpts, channel: "chrome" });
5788
+ browser = await chromium3.launch({ ...headedOpts, channel: "chrome" });
5751
5789
  } catch {
5752
- browser = await chromium2.launch(headedOpts);
5790
+ browser = await chromium3.launch(headedOpts);
5753
5791
  }
5754
5792
  } else {
5755
- browser = await chromium2.launch({ headless: true });
5793
+ browser = await launchHeadlessChromium();
5756
5794
  }
5757
5795
  context = await browser.newContext({
5758
5796
  viewport,
@@ -5789,9 +5827,9 @@ async function launchSignInBrowser() {
5789
5827
  ignoreDefaultArgs: ["--enable-automation"]
5790
5828
  };
5791
5829
  try {
5792
- return await chromium2.launch({ ...opts, channel: "chrome" });
5830
+ return await chromium3.launch({ ...opts, channel: "chrome" });
5793
5831
  } catch {
5794
- return await chromium2.launch(opts);
5832
+ return await chromium3.launch(opts);
5795
5833
  }
5796
5834
  }
5797
5835
  async function cleanupBrowserSession(session) {
@@ -5863,7 +5901,7 @@ async function cancelSignIn() {
5863
5901
  import { copyFileSync, existsSync as existsSync4, mkdirSync, mkdtempSync, rmSync as rmSync2 } from "fs";
5864
5902
  import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
5865
5903
  import { dirname, join as join3 } from "path";
5866
- import { chromium as chromium3 } from "playwright";
5904
+ import { chromium as chromium4 } from "playwright";
5867
5905
  var BROWSERS = [
5868
5906
  {
5869
5907
  key: "chrome",
@@ -5973,7 +6011,7 @@ async function readState(profileDir, channel, store2) {
5973
6011
  ];
5974
6012
  if (store2) args.push(`--password-store=${store2}`);
5975
6013
  try {
5976
- const context = await chromium3.launchPersistentContext(profileDir, { headless: true, channel, args });
6014
+ const context = await chromium4.launchPersistentContext(profileDir, { headless: true, channel, args });
5977
6015
  try {
5978
6016
  return await context.storageState();
5979
6017
  } finally {
@@ -6590,35 +6628,23 @@ async function setupUserEmail() {
6590
6628
  console.log(chalk4.white(" needs an address to meter. No card, and no verification"));
6591
6629
  console.log(chalk4.white(" needed for your first 5 videos."));
6592
6630
  console.log(chalk4.dim(" After that, signing in gets you 10 videos a day."));
6593
- const browserEmail = await browserLogin();
6594
- if (browserEmail) {
6595
- console.log();
6596
- console.log(chalk4.green(` \u2713 Signed in as ${browserEmail}`));
6597
- console.log();
6598
- return browserEmail;
6599
- }
6600
6631
  console.log();
6601
- console.log(chalk4.dim(" Or enter your email (Enter to skip):"));
6632
+ console.log(chalk4.dim(" Type your email, or 'b' to sign in through a browser."));
6602
6633
  const email = await prompt(chalk4.white(" Email: "));
6603
- if (!email) {
6634
+ if (!email) return null;
6635
+ if (email.toLowerCase() === "b" || email.toLowerCase() === "browser") {
6636
+ const browserEmail = await browserLogin();
6637
+ if (!browserEmail) return null;
6604
6638
  console.log();
6605
- console.log(chalk4.yellow(" No email given, so the AI planner cannot be reached."));
6606
- console.log(chalk4.dim(" Nothing has been recorded. Run qp again when you are ready."));
6607
- console.log(chalk4.dim(" To record with no account at all, use the MCP server:"));
6608
- console.log(chalk4.dim(" https://quickpeek.co/mcp"));
6639
+ console.log(chalk4.green(` \u2713 Signed in as ${browserEmail}`));
6609
6640
  console.log();
6610
- process.exit(1);
6641
+ return browserEmail;
6611
6642
  }
6612
6643
  if (!email.includes("@") || !email.includes(".")) {
6613
6644
  console.log();
6614
6645
  console.log(chalk4.yellow(" \u26A0 That doesn't look like a valid email address."));
6615
6646
  const proceed = await prompt(chalk4.white(" Continue anyway? ") + chalk4.dim("[y/N] "));
6616
- if (proceed.toLowerCase() !== "y") {
6617
- console.log();
6618
- console.log(chalk4.dim(" Nothing has been recorded. Run qp again when you are ready."));
6619
- console.log();
6620
- process.exit(1);
6621
- }
6647
+ if (proceed.toLowerCase() !== "y") return null;
6622
6648
  }
6623
6649
  console.log();
6624
6650
  console.log(chalk4.green(" \u2713 Email registered"));
@@ -7132,10 +7158,11 @@ async function handleRegistrationGate() {
7132
7158
  console.log(chalk8.dim(" \u2022 10 videos a day instead of 5 in total"));
7133
7159
  console.log(chalk8.dim(" \u2022 Keep your video history"));
7134
7160
  console.log(chalk8.dim(" \u2022 Upgrade to Pro anytime"));
7135
- let email = await browserLogin();
7136
- if (!email) {
7137
- console.log();
7138
- email = await promptInput("Email: ");
7161
+ console.log();
7162
+ console.log(chalk8.dim(" Type your email, or 'b' to sign in through a browser."));
7163
+ let email = await promptInput("Email: ");
7164
+ if (email.toLowerCase() === "b" || email.toLowerCase() === "browser") {
7165
+ email = await browserLogin() ?? "";
7139
7166
  }
7140
7167
  if (!email || !email.includes("@")) {
7141
7168
  console.log(chalk8.red(" Invalid email. Run qp again to try again."));
@@ -7210,7 +7237,7 @@ async function runAuthPhase(input) {
7210
7237
  console.log();
7211
7238
  const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
7212
7239
  if (input2.toLowerCase() === "exit") {
7213
- gracefulExit();
7240
+ gracefulExit(1);
7214
7241
  return null;
7215
7242
  }
7216
7243
  if (input2?.includes("@")) {
@@ -7219,7 +7246,7 @@ async function runAuthPhase(input) {
7219
7246
  const result = await registerFreeUser(newEmail);
7220
7247
  if (!result.success) {
7221
7248
  registerTask.fail(result.error || "Registration failed");
7222
- gracefulExit();
7249
+ gracefulExit(1);
7223
7250
  return null;
7224
7251
  }
7225
7252
  if (result.alreadyVerified) {
@@ -7234,7 +7261,7 @@ async function runAuthPhase(input) {
7234
7261
  console.log(chalk9.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
7235
7262
  console.log(chalk9.dim(" Click the link in the email, then run qp again."));
7236
7263
  console.log();
7237
- gracefulExit();
7264
+ gracefulExit(1);
7238
7265
  return null;
7239
7266
  }
7240
7267
  } else {
@@ -7250,7 +7277,7 @@ async function runAuthPhase(input) {
7250
7277
  console.log();
7251
7278
  console.log(chalk9.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
7252
7279
  console.log();
7253
- gracefulExit();
7280
+ gracefulExit(1);
7254
7281
  return null;
7255
7282
  }
7256
7283
  }
@@ -7258,7 +7285,7 @@ async function runAuthPhase(input) {
7258
7285
  } else if (localVideoCount >= FREE_VIDEO_LIMIT) {
7259
7286
  const registration = await handleRegistrationGate();
7260
7287
  if (!registration) {
7261
- gracefulExit();
7288
+ gracefulExit(1);
7262
7289
  return null;
7263
7290
  }
7264
7291
  userEmail = registration.email;
@@ -7268,7 +7295,7 @@ async function runAuthPhase(input) {
7268
7295
  localVideoCount
7269
7296
  });
7270
7297
  if (!registration.verified) {
7271
- gracefulExit();
7298
+ gracefulExit(1);
7272
7299
  return null;
7273
7300
  }
7274
7301
  await displayUsageStatus(userEmail, userTier, localVideoCount);
@@ -7277,7 +7304,16 @@ async function runAuthPhase(input) {
7277
7304
  console.log();
7278
7305
  console.log(chalk9.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
7279
7306
  console.log(chalk9.dim(` ${remaining2 - 1} more after this \u2022 Sign up for 10 a day`));
7280
- userEmail = await setupUserEmail();
7307
+ const enteredEmail = await setupUserEmail();
7308
+ if (!enteredEmail) {
7309
+ console.log();
7310
+ console.log(chalk9.yellow(" No email, so no video was recorded."));
7311
+ console.log(chalk9.white(" Next: run `qp login` to sign in, then run qp again."));
7312
+ console.log();
7313
+ gracefulExit(1);
7314
+ return null;
7315
+ }
7316
+ userEmail = enteredEmail;
7281
7317
  let registeredVerified = false;
7282
7318
  try {
7283
7319
  const reg = await registerFreeUser(userEmail);