@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.
package/dist/qp.js CHANGED
@@ -147,6 +147,139 @@ var init_image_kind = __esm({
147
147
  }
148
148
  });
149
149
 
150
+ // src/browser/headless.ts
151
+ async function launchHeadlessChromium(options = {}) {
152
+ const opts = { ...options, headless: true };
153
+ try {
154
+ return await import_playwright.chromium.launch(opts);
155
+ } catch (bundledError) {
156
+ try {
157
+ return await import_playwright.chromium.launch({ ...opts, channel: "chrome" });
158
+ } catch {
159
+ const detail = bundledError instanceof Error ? bundledError.message : String(bundledError);
160
+ throw new Error(`${NO_BROWSER_MESSAGE}
161
+
162
+ Playwright reported: ${detail}`);
163
+ }
164
+ }
165
+ }
166
+ var import_playwright, NO_BROWSER_MESSAGE;
167
+ var init_headless = __esm({
168
+ "src/browser/headless.ts"() {
169
+ "use strict";
170
+ import_playwright = require("playwright");
171
+ 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";
172
+ }
173
+ });
174
+
175
+ // src/lib/json-repair.ts
176
+ function repairUnicodeEscapes(content) {
177
+ return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
178
+ const code = Number.parseInt(hex, 16);
179
+ return code <= 1114111 ? String.fromCodePoint(code) : match;
180
+ }).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
181
+ }
182
+ function salvageTruncatedJson(content) {
183
+ const stack = [];
184
+ const cuts = [];
185
+ let inString = false;
186
+ let escaped = false;
187
+ const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
188
+ for (let i = 0; i < content.length; i++) {
189
+ const ch = content[i];
190
+ if (inString) {
191
+ if (escaped) escaped = false;
192
+ else if (ch === "\\") escaped = true;
193
+ else if (ch === '"') {
194
+ inString = false;
195
+ record(i + 1, false);
196
+ }
197
+ continue;
198
+ }
199
+ if (ch === '"') {
200
+ inString = true;
201
+ } else if (ch === "{" || ch === "[") {
202
+ stack.push(ch);
203
+ } else if (ch === "}" || ch === "]") {
204
+ stack.pop();
205
+ record(i + 1, true);
206
+ } else if (ch === ",") {
207
+ record(i, content[i - 1] === "}" || content[i - 1] === "]");
208
+ }
209
+ }
210
+ const tryCut = (cut) => {
211
+ const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
212
+ if (head.endsWith(":")) return null;
213
+ let closed = head;
214
+ for (let d = cut.open.length - 1; d >= 0; d--) {
215
+ closed += CLOSERS[cut.open[d]];
216
+ }
217
+ try {
218
+ JSON.parse(closed);
219
+ return closed;
220
+ } catch {
221
+ return null;
222
+ }
223
+ };
224
+ const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
225
+ for (const containerOnly of [true, false]) {
226
+ for (let n = 0; n < attempts; n++) {
227
+ const cut = cuts[cuts.length - 1 - n];
228
+ if (containerOnly && !cut.closesContainer) continue;
229
+ const salvaged = tryCut(cut);
230
+ if (salvaged !== null) return salvaged;
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+ function extractJsonCandidates(content) {
236
+ let text = content.trim();
237
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
238
+ if (fence?.[1]?.trim()) text = fence[1].trim();
239
+ const start = text.search(/[{[]/);
240
+ if (start === -1) return [];
241
+ const closer = text[start] === "{" ? "}" : "]";
242
+ const end = text.lastIndexOf(closer);
243
+ const candidates = [text.slice(start)];
244
+ if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
245
+ return candidates;
246
+ }
247
+ function parseAIJson(content) {
248
+ const candidates = extractJsonCandidates(content);
249
+ if (candidates.length === 0) return null;
250
+ for (const text of candidates) {
251
+ try {
252
+ return { value: JSON.parse(text), repaired: false };
253
+ } catch {
254
+ }
255
+ }
256
+ for (const text of candidates) {
257
+ try {
258
+ return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
259
+ } catch {
260
+ }
261
+ }
262
+ for (const text of candidates) {
263
+ for (const variant of [repairUnicodeEscapes(text), text]) {
264
+ const salvaged = salvageTruncatedJson(variant);
265
+ if (salvaged === null) continue;
266
+ try {
267
+ return { value: JSON.parse(salvaged), repaired: true };
268
+ } catch {
269
+ }
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+ var MAX_SALVAGE_ATTEMPTS, CLOSERS;
275
+ var init_json_repair = __esm({
276
+ "src/lib/json-repair.ts"() {
277
+ "use strict";
278
+ MAX_SALVAGE_ATTEMPTS = 500;
279
+ CLOSERS = { "{": "}", "[": "]" };
280
+ }
281
+ });
282
+
150
283
  // src/lib/utils.ts
151
284
  function getLanguageName(lang) {
152
285
  if (!lang || lang === "en") return null;
@@ -468,24 +601,26 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", op
468
601
  }
469
602
  }
470
603
  function parseAIPlanResponse(content) {
471
- try {
472
- let processedContent = content;
473
- const codeBlockMatch = processedContent.match(/```(?:json)?\s*([\s\S]*?)```/);
474
- if (codeBlockMatch?.[1]) {
475
- processedContent = codeBlockMatch[1].trim();
476
- }
477
- const jsonMatch = processedContent.match(/\{[\s\S]*\}/);
478
- if (!jsonMatch) {
479
- return { success: false, error: "No valid JSON found in AI response" };
480
- }
481
- const parsed = JSON.parse(jsonMatch[0]);
482
- return { success: true, plan: parsed };
483
- } catch (err) {
484
- return {
485
- success: false,
486
- error: `Failed to parse AI response: ${err instanceof Error ? err.message : String(err)}`
487
- };
604
+ const parsed = parseAIJson(content);
605
+ if (!parsed) {
606
+ return { success: false, error: "No valid JSON found in AI response" };
488
607
  }
608
+ if (!Array.isArray(parsed.value.steps) || parsed.value.steps.length === 0) {
609
+ return { success: false, error: "AI response has no steps" };
610
+ }
611
+ return { success: true, plan: { ...parsed.value, steps: parsed.value.steps } };
612
+ }
613
+ async function callAIForPlan(systemPrompt, userPrompt, tier = "free", opts = {}) {
614
+ let lastParseError = "No valid JSON found in AI response";
615
+ for (let attempt = 1; attempt <= 2; attempt++) {
616
+ const aiResult = await callAI(systemPrompt, userPrompt, tier, opts);
617
+ if (!aiResult.success) return aiResult;
618
+ const parsed = parseAIPlanResponse(aiResult.content);
619
+ if (parsed.success) return { success: true, plan: parsed.plan, limits: aiResult.limits };
620
+ lastParseError = parsed.error;
621
+ console.error(`Plan parse failed (attempt ${attempt}/2):`, parsed.error);
622
+ }
623
+ return { success: false, error: lastParseError, isParseError: true };
489
624
  }
490
625
  function normalizeUrl(input) {
491
626
  let url = input.trim();
@@ -496,6 +631,7 @@ var PLAN_MAX_TOKENS, SERVER_BACKOFF_MS, CLI_BACKOFF_MS, SPARK_AI_BASE, SPARK_AI_
496
631
  var init_api_client = __esm({
497
632
  "src/lib/api-client.ts"() {
498
633
  "use strict";
634
+ init_json_repair();
499
635
  init_utils();
500
636
  PLAN_MAX_TOKENS = 1500;
501
637
  SERVER_BACKOFF_MS = [2e3, 4e3];
@@ -666,7 +802,7 @@ var VERSION, CONFIG_FILE, SIZE_PRESETS, RATE_FACTOR, NO_CLIP_OUTRO, DEFAULT_CONF
666
802
  var init_config = __esm({
667
803
  "src/lib/config.ts"() {
668
804
  "use strict";
669
- VERSION = "1.2.5";
805
+ VERSION = "1.2.6";
670
806
  CONFIG_FILE = "quickpeek.config.json";
671
807
  SIZE_PRESETS = {
672
808
  wide: { width: 1920, height: 1080, zoom: 1 },
@@ -977,7 +1113,13 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
977
1113
  return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
978
1114
  }
979
1115
  const browserTask = task2("Launching browser");
980
- const browser = await import_playwright4.chromium.launch({ headless: true });
1116
+ let browser;
1117
+ try {
1118
+ browser = await launchHeadlessChromium();
1119
+ } catch (err) {
1120
+ browserTask.succeed("Could not launch a browser");
1121
+ throw err;
1122
+ }
981
1123
  try {
982
1124
  const context = await browser.newContext({
983
1125
  ...storageStatePath ? { storageState: storageStatePath } : {},
@@ -1153,11 +1295,11 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
1153
1295
  });
1154
1296
  }
1155
1297
  }
1156
- var import_playwright4, SETTLE_POLL_MS, SETTLE_STABLE_POLLS, SETTLE_CAP_MS, PLACEHOLDER_POLL_MS, PLACEHOLDER_CAP_MS, PLACEHOLDER_STATIC_POLLS, PLACEHOLDER_CLEAR_POLLS, PLACEHOLDER_SELECTOR, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
1298
+ var SETTLE_POLL_MS, SETTLE_STABLE_POLLS, SETTLE_CAP_MS, PLACEHOLDER_POLL_MS, PLACEHOLDER_CAP_MS, PLACEHOLDER_STATIC_POLLS, PLACEHOLDER_CLEAR_POLLS, PLACEHOLDER_SELECTOR, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
1157
1299
  var init_crawl = __esm({
1158
1300
  "src/lib/crawl.ts"() {
1159
1301
  "use strict";
1160
- import_playwright4 = require("playwright");
1302
+ init_headless();
1161
1303
  init_scraping();
1162
1304
  SETTLE_POLL_MS = 400;
1163
1305
  SETTLE_STABLE_POLLS = 2;
@@ -2635,114 +2777,6 @@ var init_ffmpeg2 = __esm({
2635
2777
  }
2636
2778
  });
2637
2779
 
2638
- // src/lib/json-repair.ts
2639
- function repairUnicodeEscapes(content) {
2640
- return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
2641
- const code = Number.parseInt(hex, 16);
2642
- return code <= 1114111 ? String.fromCodePoint(code) : match;
2643
- }).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
2644
- }
2645
- function salvageTruncatedJson(content) {
2646
- const stack = [];
2647
- const cuts = [];
2648
- let inString = false;
2649
- let escaped = false;
2650
- const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
2651
- for (let i = 0; i < content.length; i++) {
2652
- const ch = content[i];
2653
- if (inString) {
2654
- if (escaped) escaped = false;
2655
- else if (ch === "\\") escaped = true;
2656
- else if (ch === '"') {
2657
- inString = false;
2658
- record(i + 1, false);
2659
- }
2660
- continue;
2661
- }
2662
- if (ch === '"') {
2663
- inString = true;
2664
- } else if (ch === "{" || ch === "[") {
2665
- stack.push(ch);
2666
- } else if (ch === "}" || ch === "]") {
2667
- stack.pop();
2668
- record(i + 1, true);
2669
- } else if (ch === ",") {
2670
- record(i, content[i - 1] === "}" || content[i - 1] === "]");
2671
- }
2672
- }
2673
- const tryCut = (cut) => {
2674
- const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
2675
- if (head.endsWith(":")) return null;
2676
- let closed = head;
2677
- for (let d = cut.open.length - 1; d >= 0; d--) {
2678
- closed += CLOSERS[cut.open[d]];
2679
- }
2680
- try {
2681
- JSON.parse(closed);
2682
- return closed;
2683
- } catch {
2684
- return null;
2685
- }
2686
- };
2687
- const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
2688
- for (const containerOnly of [true, false]) {
2689
- for (let n = 0; n < attempts; n++) {
2690
- const cut = cuts[cuts.length - 1 - n];
2691
- if (containerOnly && !cut.closesContainer) continue;
2692
- const salvaged = tryCut(cut);
2693
- if (salvaged !== null) return salvaged;
2694
- }
2695
- }
2696
- return null;
2697
- }
2698
- function extractJsonCandidates(content) {
2699
- let text = content.trim();
2700
- const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
2701
- if (fence?.[1]?.trim()) text = fence[1].trim();
2702
- const start = text.search(/[{[]/);
2703
- if (start === -1) return [];
2704
- const closer = text[start] === "{" ? "}" : "]";
2705
- const end = text.lastIndexOf(closer);
2706
- const candidates = [text.slice(start)];
2707
- if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
2708
- return candidates;
2709
- }
2710
- function parseAIJson(content) {
2711
- const candidates = extractJsonCandidates(content);
2712
- if (candidates.length === 0) return null;
2713
- for (const text of candidates) {
2714
- try {
2715
- return { value: JSON.parse(text), repaired: false };
2716
- } catch {
2717
- }
2718
- }
2719
- for (const text of candidates) {
2720
- try {
2721
- return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
2722
- } catch {
2723
- }
2724
- }
2725
- for (const text of candidates) {
2726
- for (const variant of [repairUnicodeEscapes(text), text]) {
2727
- const salvaged = salvageTruncatedJson(variant);
2728
- if (salvaged === null) continue;
2729
- try {
2730
- return { value: JSON.parse(salvaged), repaired: true };
2731
- } catch {
2732
- }
2733
- }
2734
- }
2735
- return null;
2736
- }
2737
- var MAX_SALVAGE_ATTEMPTS, CLOSERS;
2738
- var init_json_repair = __esm({
2739
- "src/lib/json-repair.ts"() {
2740
- "use strict";
2741
- MAX_SALVAGE_ATTEMPTS = 500;
2742
- CLOSERS = { "{": "}", "[": "]" };
2743
- }
2744
- });
2745
-
2746
2780
  // src/lib/prompts.ts
2747
2781
  function buildSystemPrompt(maxSteps, profile) {
2748
2782
  let scope;
@@ -4115,6 +4149,7 @@ __export(lib_exports, {
4115
4149
  buildUserPrompt: () => buildUserPrompt,
4116
4150
  buildWatermarkFilters: () => buildWatermarkFilters,
4117
4151
  callAI: () => callAI,
4152
+ callAIForPlan: () => callAIForPlan,
4118
4153
  callAIViaRelay: () => callAIViaRelay,
4119
4154
  canReuseVoiceover: () => canReuseVoiceover,
4120
4155
  captionCanvas: () => captionCanvas,
@@ -5037,7 +5072,7 @@ async function listAuthStates() {
5037
5072
 
5038
5073
  // src/browser/launch.ts
5039
5074
  var import_promises2 = require("fs/promises");
5040
- var import_playwright2 = require("playwright");
5075
+ var import_playwright3 = require("playwright");
5041
5076
  init_lifecycle();
5042
5077
 
5043
5078
  // src/browser/brand-watermark.ts
@@ -5131,11 +5166,14 @@ function clampOpacity(value) {
5131
5166
  return Math.min(1, Math.max(0.35, value));
5132
5167
  }
5133
5168
 
5169
+ // src/browser/launch.ts
5170
+ init_headless();
5171
+
5134
5172
  // src/browser/wsl.ts
5135
5173
  var import_node_child_process = require("child_process");
5136
5174
  var import_node_fs3 = require("fs");
5137
5175
  var import_node_http = require("http");
5138
- var import_playwright = require("playwright");
5176
+ var import_playwright2 = require("playwright");
5139
5177
  function isWSL() {
5140
5178
  if (process.platform !== "linux") return false;
5141
5179
  try {
@@ -5254,7 +5292,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
5254
5292
  const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
5255
5293
  try {
5256
5294
  await waitForChrome(windowsHost, proxyPort);
5257
- const browser = await import_playwright.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
5295
+ const browser = await import_playwright2.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
5258
5296
  return {
5259
5297
  browser,
5260
5298
  cleanup: async () => {
@@ -5311,7 +5349,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
5311
5349
  acceptDownloads: true
5312
5350
  });
5313
5351
  } catch {
5314
- context = await import_playwright2.chromium.launchPersistentContext(userDataDir, {
5352
+ context = await import_playwright3.chromium.launchPersistentContext(userDataDir, {
5315
5353
  headless: false,
5316
5354
  args: persistentArgs,
5317
5355
  viewport,
@@ -5322,7 +5360,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
5322
5360
  });
5323
5361
  }
5324
5362
  } else {
5325
- context = await import_playwright2.chromium.launchPersistentContext(userDataDir, {
5363
+ context = await import_playwright3.chromium.launchPersistentContext(userDataDir, {
5326
5364
  headless: false,
5327
5365
  args: persistentArgs,
5328
5366
  viewport,
@@ -5340,12 +5378,12 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
5340
5378
  ignoreDefaultArgs: ["--enable-automation"]
5341
5379
  };
5342
5380
  try {
5343
- browser = await import_playwright2.chromium.launch({ ...headedOpts, channel: "chrome" });
5381
+ browser = await import_playwright3.chromium.launch({ ...headedOpts, channel: "chrome" });
5344
5382
  } catch {
5345
- browser = await import_playwright2.chromium.launch(headedOpts);
5383
+ browser = await import_playwright3.chromium.launch(headedOpts);
5346
5384
  }
5347
5385
  } else {
5348
- browser = await import_playwright2.chromium.launch({ headless: true });
5386
+ browser = await launchHeadlessChromium();
5349
5387
  }
5350
5388
  context = await browser.newContext({
5351
5389
  viewport,
@@ -5382,9 +5420,9 @@ async function launchSignInBrowser() {
5382
5420
  ignoreDefaultArgs: ["--enable-automation"]
5383
5421
  };
5384
5422
  try {
5385
- return await import_playwright2.chromium.launch({ ...opts, channel: "chrome" });
5423
+ return await import_playwright3.chromium.launch({ ...opts, channel: "chrome" });
5386
5424
  } catch {
5387
- return await import_playwright2.chromium.launch(opts);
5425
+ return await import_playwright3.chromium.launch(opts);
5388
5426
  }
5389
5427
  }
5390
5428
  async function cleanupBrowserSession(session) {
@@ -5453,7 +5491,7 @@ async function cancelSignIn() {
5453
5491
  var import_node_fs4 = require("fs");
5454
5492
  var import_node_os2 = require("os");
5455
5493
  var import_node_path2 = require("path");
5456
- var import_playwright3 = require("playwright");
5494
+ var import_playwright4 = require("playwright");
5457
5495
  var BROWSERS = [
5458
5496
  {
5459
5497
  key: "chrome",
@@ -5563,7 +5601,7 @@ async function readState(profileDir, channel, store2) {
5563
5601
  ];
5564
5602
  if (store2) args.push(`--password-store=${store2}`);
5565
5603
  try {
5566
- const context = await import_playwright3.chromium.launchPersistentContext(profileDir, { headless: true, channel, args });
5604
+ const context = await import_playwright4.chromium.launchPersistentContext(profileDir, { headless: true, channel, args });
5567
5605
  try {
5568
5606
  return await context.storageState();
5569
5607
  } finally {
@@ -6359,35 +6397,23 @@ async function setupUserEmail() {
6359
6397
  console.log(import_chalk5.default.white(" needs an address to meter. No card, and no verification"));
6360
6398
  console.log(import_chalk5.default.white(" needed for your first 5 videos."));
6361
6399
  console.log(import_chalk5.default.dim(" After that, signing in gets you 10 videos a day."));
6362
- const browserEmail = await browserLogin();
6363
- if (browserEmail) {
6364
- console.log();
6365
- console.log(import_chalk5.default.green(` \u2713 Signed in as ${browserEmail}`));
6366
- console.log();
6367
- return browserEmail;
6368
- }
6369
6400
  console.log();
6370
- console.log(import_chalk5.default.dim(" Or enter your email (Enter to skip):"));
6401
+ console.log(import_chalk5.default.dim(" Type your email, or 'b' to sign in through a browser."));
6371
6402
  const email = await prompt(import_chalk5.default.white(" Email: "));
6372
- if (!email) {
6403
+ if (!email) return null;
6404
+ if (email.toLowerCase() === "b" || email.toLowerCase() === "browser") {
6405
+ const browserEmail = await browserLogin();
6406
+ if (!browserEmail) return null;
6373
6407
  console.log();
6374
- console.log(import_chalk5.default.yellow(" No email given, so the AI planner cannot be reached."));
6375
- console.log(import_chalk5.default.dim(" Nothing has been recorded. Run qp again when you are ready."));
6376
- console.log(import_chalk5.default.dim(" To record with no account at all, use the MCP server:"));
6377
- console.log(import_chalk5.default.dim(" https://quickpeek.co/mcp"));
6408
+ console.log(import_chalk5.default.green(` \u2713 Signed in as ${browserEmail}`));
6378
6409
  console.log();
6379
- process.exit(1);
6410
+ return browserEmail;
6380
6411
  }
6381
6412
  if (!email.includes("@") || !email.includes(".")) {
6382
6413
  console.log();
6383
6414
  console.log(import_chalk5.default.yellow(" \u26A0 That doesn't look like a valid email address."));
6384
6415
  const proceed = await prompt(import_chalk5.default.white(" Continue anyway? ") + import_chalk5.default.dim("[y/N] "));
6385
- if (proceed.toLowerCase() !== "y") {
6386
- console.log();
6387
- console.log(import_chalk5.default.dim(" Nothing has been recorded. Run qp again when you are ready."));
6388
- console.log();
6389
- process.exit(1);
6390
- }
6416
+ if (proceed.toLowerCase() !== "y") return null;
6391
6417
  }
6392
6418
  console.log();
6393
6419
  console.log(import_chalk5.default.green(" \u2713 Email registered"));
@@ -10548,10 +10574,11 @@ async function handleRegistrationGate() {
10548
10574
  console.log(import_chalk15.default.dim(" \u2022 10 videos a day instead of 5 in total"));
10549
10575
  console.log(import_chalk15.default.dim(" \u2022 Keep your video history"));
10550
10576
  console.log(import_chalk15.default.dim(" \u2022 Upgrade to Pro anytime"));
10551
- let email = await browserLogin();
10552
- if (!email) {
10553
- console.log();
10554
- email = await promptInput("Email: ");
10577
+ console.log();
10578
+ console.log(import_chalk15.default.dim(" Type your email, or 'b' to sign in through a browser."));
10579
+ let email = await promptInput("Email: ");
10580
+ if (email.toLowerCase() === "b" || email.toLowerCase() === "browser") {
10581
+ email = await browserLogin() ?? "";
10555
10582
  }
10556
10583
  if (!email || !email.includes("@")) {
10557
10584
  console.log(import_chalk15.default.red(" Invalid email. Run qp again to try again."));
@@ -10626,7 +10653,7 @@ async function runAuthPhase(input) {
10626
10653
  console.log();
10627
10654
  const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
10628
10655
  if (input2.toLowerCase() === "exit") {
10629
- gracefulExit();
10656
+ gracefulExit(1);
10630
10657
  return null;
10631
10658
  }
10632
10659
  if (input2?.includes("@")) {
@@ -10635,7 +10662,7 @@ async function runAuthPhase(input) {
10635
10662
  const result = await registerFreeUser(newEmail);
10636
10663
  if (!result.success) {
10637
10664
  registerTask.fail(result.error || "Registration failed");
10638
- gracefulExit();
10665
+ gracefulExit(1);
10639
10666
  return null;
10640
10667
  }
10641
10668
  if (result.alreadyVerified) {
@@ -10650,7 +10677,7 @@ async function runAuthPhase(input) {
10650
10677
  console.log(import_chalk16.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
10651
10678
  console.log(import_chalk16.default.dim(" Click the link in the email, then run qp again."));
10652
10679
  console.log();
10653
- gracefulExit();
10680
+ gracefulExit(1);
10654
10681
  return null;
10655
10682
  }
10656
10683
  } else {
@@ -10666,7 +10693,7 @@ async function runAuthPhase(input) {
10666
10693
  console.log();
10667
10694
  console.log(import_chalk16.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
10668
10695
  console.log();
10669
- gracefulExit();
10696
+ gracefulExit(1);
10670
10697
  return null;
10671
10698
  }
10672
10699
  }
@@ -10674,7 +10701,7 @@ async function runAuthPhase(input) {
10674
10701
  } else if (localVideoCount >= FREE_VIDEO_LIMIT) {
10675
10702
  const registration = await handleRegistrationGate();
10676
10703
  if (!registration) {
10677
- gracefulExit();
10704
+ gracefulExit(1);
10678
10705
  return null;
10679
10706
  }
10680
10707
  userEmail = registration.email;
@@ -10684,7 +10711,7 @@ async function runAuthPhase(input) {
10684
10711
  localVideoCount
10685
10712
  });
10686
10713
  if (!registration.verified) {
10687
- gracefulExit();
10714
+ gracefulExit(1);
10688
10715
  return null;
10689
10716
  }
10690
10717
  await displayUsageStatus(userEmail, userTier, localVideoCount);
@@ -10693,7 +10720,16 @@ async function runAuthPhase(input) {
10693
10720
  console.log();
10694
10721
  console.log(import_chalk16.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
10695
10722
  console.log(import_chalk16.default.dim(` ${remaining2 - 1} more after this \u2022 Sign up for 10 a day`));
10696
- userEmail = await setupUserEmail();
10723
+ const enteredEmail = await setupUserEmail();
10724
+ if (!enteredEmail) {
10725
+ console.log();
10726
+ console.log(import_chalk16.default.yellow(" No email, so no video was recorded."));
10727
+ console.log(import_chalk16.default.white(" Next: run `qp login` to sign in, then run qp again."));
10728
+ console.log();
10729
+ gracefulExit(1);
10730
+ return null;
10731
+ }
10732
+ userEmail = enteredEmail;
10697
10733
  let registeredVerified = false;
10698
10734
  try {
10699
10735
  const reg = await registerFreeUser(userEmail);
@@ -3,7 +3,7 @@ import { Page } from 'playwright';
3
3
  /**
4
4
  * Configuration types and defaults for QuickPeek
5
5
  */
6
- declare const VERSION = "1.2.5";
6
+ declare const VERSION = "1.2.6";
7
7
  declare const CONFIG_FILE = "quickpeek.config.json";
8
8
  type UserTier = 'free' | 'pro' | 'elite';
9
9
  /** The one dimension knob users see: wide (16:9) or short (9:16). */
@@ -280,6 +280,8 @@ interface AIError {
280
280
  host?: string;
281
281
  /** Raw response body (truncated), for the error log. */
282
282
  detail?: string;
283
+ /** The call succeeded but its output was not a usable plan. */
284
+ isParseError?: boolean;
283
285
  }
284
286
  type AIResult = AIResponse | AIError;
285
287
  /** Called before each backoff so the CLI can explain a long pause. */
@@ -338,7 +340,17 @@ declare function callAI(systemPrompt: string, userPrompt: string, tier?: UserTie
338
340
  */
339
341
  declare function callAIViaRelay(systemPrompt: string, userPrompt: string, email: string, tier?: UserTier, opts?: ChatOptions): Promise<AIResult>;
340
342
  /**
341
- * Parse AI-generated plan JSON from response text
343
+ * Parse AI-generated plan JSON from response text.
344
+ *
345
+ * Delegates to parseAIJson, the same lenient path the CLI's plan step uses.
346
+ * This used to do its own `\{[\s\S]*\}` extraction, which fails on exactly the
347
+ * two things a model does under a token ceiling: stopping mid-array (the
348
+ * greedy match then ends at an inner brace and JSON.parse throws), and
349
+ * spending the whole budget on prose so no braces are emitted at all — the
350
+ * "No valid JSON found in AI response" the web wizard kept showing, where
351
+ * hitting Retry worked only because the next sample happened to be terser.
352
+ * The repair path recovers every complete step from a truncated plan, so a
353
+ * clipped response yields a usable plan instead of an error and a re-crawl.
342
354
  */
343
355
  declare function parseAIPlanResponse(content: string): {
344
356
  success: true;
@@ -357,6 +369,36 @@ declare function parseAIPlanResponse(content: string): {
357
369
  success: false;
358
370
  error: string;
359
371
  };
372
+ /**
373
+ * Ask for a plan and parse it, resampling once if the answer is unusable.
374
+ *
375
+ * parseAIJson recovers a truncated plan, but it cannot recover the other
376
+ * failure mode: a model that spends its whole completion budget reasoning in
377
+ * prose and is cut off before writing a single brace. That is sampling luck,
378
+ * not a broken request — which is why the wizard's Retry button "fixed" it,
379
+ * and why the fix is to resample here rather than to hand the user a red box.
380
+ * Retrying in the route is strictly cheaper than the user retrying: their
381
+ * Retry re-runs the page crawl as well.
382
+ *
383
+ * Exactly one extra attempt. A model that produces prose twice is a prompt or
384
+ * backend problem, and a third round trip only spends the caller's serverless
385
+ * budget on the same answer.
386
+ */
387
+ declare function callAIForPlan(systemPrompt: string, userPrompt: string, tier?: UserTier, opts?: ChatOptions): Promise<{
388
+ success: true;
389
+ plan: {
390
+ title?: string;
391
+ description?: string;
392
+ steps: Array<{
393
+ id?: number;
394
+ action?: string;
395
+ caption?: string;
396
+ target?: string;
397
+ value?: string;
398
+ }>;
399
+ };
400
+ limits: RateLimitInfo;
401
+ } | AIError>;
360
402
  /**
361
403
  * Normalize a URL by adding https:// and stripping www prefix
362
404
  */
@@ -644,4 +686,4 @@ declare function hasVoice(lang: string): boolean;
644
686
  */
645
687
  declare function voiceFor(lang: string, gender?: VoiceGender): string | null;
646
688
 
647
- export { getStatus as $, type AIError as A, type VideoSize as B, type CaptionStyle as C, DEFAULT_CAPTIONS as D, EXCLUDED_LINK_PATTERNS as E, type VoiceGender as F, applyProfile as G, type HighlightMode as H, INTERACTIVE_SELECTORS as I, ariaLabelSelector as J, buildSystemPrompt as K, buildUserPrompt as L, callAI as M, NO_CLIP_OUTRO as N, callAIViaRelay as O, PLAN_MAX_TOKENS as P, crawlPage as Q, RATE_FACTOR as R, SERVER_BACKOFF_MS as S, type Tier as T, type UserTier as U, type VoiceRate as V, deStock as W, defaultCaptionsFor as X, getErrorMessage as Y, getLanguageName as Z, getOrRefreshRelayToken as _, type CaptionPosition as a, getStatusCached as a0, getTierByEmail as a1, gotoSettled as a2, hasTrialRemaining as a3, hasVoice as a4, idSelector as a5, internationalVoiceFor as a6, isCanceling as a7, isExcludedLink as a8, isPaid as a9, isVerified as aa, normalizeUrl as ab, parseAIPlanResponse as ac, parsePartial as ad, pricingUrl as ae, rgbaToHex as af, runPooled as ag, shouldSkipLink as ah, showPaywall as ai, stripLongDashes as aj, toTitleCase as ak, voiceFor as al, waitForPlaceholdersGone as am, getTier as an, type Config as b, type AIResponse as c, type AIResult as d, CLI_BACKOFF_MS as e, CONFIG_FILE as f, type CaptionPreset as g, type CaptionWordStyle as h, type ChatOptions as i, DEFAULT_CONFIG as j, type ElementInfo as k, type ElementType as l, INTERNATIONAL_VOICE as m, type Plan as n, RELAY_BASE as o, type RateLimitInfo as p, type RetryNotice as q, SIZE_PRESETS as r, type SparkStatus as s, type SparkSubscription as t, type SparkTrial as u, type Step as v, type SuggestedAction as w, VERSION as x, VIDEO_PROFILES as y, type VideoProfileName as z };
689
+ export { getOrRefreshRelayToken as $, type AIError as A, type VideoSize as B, type CaptionStyle as C, DEFAULT_CAPTIONS as D, EXCLUDED_LINK_PATTERNS as E, type VoiceGender as F, applyProfile as G, type HighlightMode as H, INTERACTIVE_SELECTORS as I, ariaLabelSelector as J, buildSystemPrompt as K, buildUserPrompt as L, callAI as M, NO_CLIP_OUTRO as N, callAIForPlan as O, PLAN_MAX_TOKENS as P, callAIViaRelay as Q, RATE_FACTOR as R, SERVER_BACKOFF_MS as S, type Tier as T, type UserTier as U, type VoiceRate as V, crawlPage as W, deStock as X, defaultCaptionsFor as Y, getErrorMessage as Z, getLanguageName as _, type CaptionPosition as a, getStatus as a0, getStatusCached as a1, getTierByEmail as a2, gotoSettled as a3, hasTrialRemaining as a4, hasVoice as a5, idSelector as a6, internationalVoiceFor as a7, isCanceling as a8, isExcludedLink as a9, isPaid as aa, isVerified as ab, normalizeUrl as ac, parseAIPlanResponse as ad, parsePartial as ae, pricingUrl as af, rgbaToHex as ag, runPooled as ah, shouldSkipLink as ai, showPaywall as aj, stripLongDashes as ak, toTitleCase as al, voiceFor as am, waitForPlaceholdersGone as an, getTier as ao, type Config as b, type AIResponse as c, type AIResult as d, CLI_BACKOFF_MS as e, CONFIG_FILE as f, type CaptionPreset as g, type CaptionWordStyle as h, type ChatOptions as i, DEFAULT_CONFIG as j, type ElementInfo as k, type ElementType as l, INTERNATIONAL_VOICE as m, type Plan as n, RELAY_BASE as o, type RateLimitInfo as p, type RetryNotice as q, SIZE_PRESETS as r, type SparkStatus as s, type SparkSubscription as t, type SparkTrial as u, type Step as v, type SuggestedAction as w, VERSION as x, VIDEO_PROFILES as y, type VideoProfileName as z };
package/dist/web.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { n as Plan } from './voices-ufga0oOn.mjs';
2
- export { A as AIError, c as AIResponse, d as AIResult, i as ChatOptions, E as EXCLUDED_LINK_PATTERNS, k as ElementInfo, l as ElementType, H as HighlightMode, I as INTERACTIVE_SELECTORS, m as INTERNATIONAL_VOICE, P as PLAN_MAX_TOKENS, p as RateLimitInfo, s as SparkStatus, t as SparkSubscription, u as SparkTrial, v as Step, w as SuggestedAction, T as Tier, K as buildSystemPrompt, L as buildUserPrompt, M as callAI, Q as crawlPage, W as deStock, Y as getErrorMessage, Z as getLanguageName, $ as getStatus, a0 as getStatusCached, an as getTier, a1 as getTierByEmail, a3 as hasTrialRemaining, a4 as hasVoice, a6 as internationalVoiceFor, a7 as isCanceling, a8 as isExcludedLink, a9 as isPaid, aa as isVerified, ab as normalizeUrl, ac as parseAIPlanResponse, ad as parsePartial, ae as pricingUrl, af as rgbaToHex, ag as runPooled, ah as shouldSkipLink, ai as showPaywall, aj as stripLongDashes, ak as toTitleCase, al as voiceFor } from './voices-ufga0oOn.mjs';
1
+ import { n as Plan } from './voices-5AFt1GDp.mjs';
2
+ export { A as AIError, c as AIResponse, d as AIResult, i as ChatOptions, E as EXCLUDED_LINK_PATTERNS, k as ElementInfo, l as ElementType, H as HighlightMode, I as INTERACTIVE_SELECTORS, m as INTERNATIONAL_VOICE, P as PLAN_MAX_TOKENS, p as RateLimitInfo, s as SparkStatus, t as SparkSubscription, u as SparkTrial, v as Step, w as SuggestedAction, T as Tier, K as buildSystemPrompt, L as buildUserPrompt, M as callAI, O as callAIForPlan, W as crawlPage, X as deStock, Z as getErrorMessage, _ as getLanguageName, a0 as getStatus, a1 as getStatusCached, ao as getTier, a2 as getTierByEmail, a4 as hasTrialRemaining, a5 as hasVoice, a7 as internationalVoiceFor, a8 as isCanceling, a9 as isExcludedLink, aa as isPaid, ab as isVerified, ac as normalizeUrl, ad as parseAIPlanResponse, ae as parsePartial, af as pricingUrl, ag as rgbaToHex, ah as runPooled, ai as shouldSkipLink, aj as showPaywall, ak as stripLongDashes, al as toTitleCase, am as voiceFor } from './voices-5AFt1GDp.mjs';
3
3
  export { MALE_VOICES, MULTILINGUAL_VOICES, WIDE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
4
4
  import 'playwright';
5
5