@spark-apps/quickpeek 1.2.1 → 1.2.2

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
@@ -128,6 +128,20 @@ var init_utils = __esm({
128
128
  function apiKey() {
129
129
  return process.env.SPARK_AI_KEY || "";
130
130
  }
131
+ function hostOf2(url) {
132
+ try {
133
+ return new URL(url).host;
134
+ } catch {
135
+ return "AI service";
136
+ }
137
+ }
138
+ function firstLine(text) {
139
+ return truncate((text ?? "").trim().split("\n")[0] ?? "", 200);
140
+ }
141
+ function truncate(text, max) {
142
+ const trimmed = (text ?? "").trim();
143
+ return trimmed.length > max ? `${trimmed.slice(0, max)}\u2026` : trimmed;
144
+ }
131
145
  async function getOrRefreshDirectToken() {
132
146
  const now = Date.now();
133
147
  if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
@@ -179,8 +193,10 @@ async function getOrRefreshRelayToken(email) {
179
193
  throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
180
194
  }
181
195
  }
182
- async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
183
- let lastRateLimitError = null;
196
+ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier, opts = {}) {
197
+ const { onRetry, backoffMs = SERVER_BACKOFF_MS } = opts;
198
+ let lastRetriableError = null;
199
+ const host = hostOf2(chatUrl);
184
200
  for (let attempt = 1; attempt <= 3; attempt++) {
185
201
  try {
186
202
  const response = await fetch(chatUrl, {
@@ -196,36 +212,53 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
196
212
  ]
197
213
  })
198
214
  });
215
+ const bodyText = await response.text().catch(() => "");
199
216
  let data = {};
200
- const contentType = response.headers.get("content-type") ?? "";
201
- if (contentType.includes("application/json")) {
202
- data = await response.json();
217
+ try {
218
+ data = bodyText ? JSON.parse(bodyText) : {};
219
+ } catch {
203
220
  }
204
221
  if (!response.ok) {
205
- const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
206
- if (response.status === 429 && attempt < 3) {
207
- const retryAfter = response.headers.get("Retry-After");
208
- const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
209
- lastRateLimitError = {
222
+ const upstream = data.error || firstLine(bodyText) || response.statusText || "no error body";
223
+ const error = `${host} ${response.status}: ${upstream}`;
224
+ const detail = truncate(bodyText, 800);
225
+ const isRetriable = response.status === 429 || response.status >= 500;
226
+ if (isRetriable && attempt < 3) {
227
+ const retryAfter2 = response.headers.get("Retry-After");
228
+ const delayMs = retryAfter2 ? parseInt(retryAfter2, 10) * 1e3 : response.status >= 500 ? backoffMs[attempt - 1] ?? backoffMs[backoffMs.length - 1] ?? 2e3 : 1e3 * 2 ** (attempt - 1);
229
+ onRetry?.({ attempt, status: response.status, delayMs, host });
230
+ lastRetriableError = {
210
231
  success: false,
211
232
  error,
212
- isRateLimit: true,
213
- ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
233
+ isRateLimit: response.status === 429,
234
+ status: response.status,
235
+ host,
236
+ ...detail ? { detail } : {},
237
+ ...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
214
238
  };
215
239
  await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
216
240
  continue;
217
241
  }
242
+ const retryAfter = response.headers.get("Retry-After");
243
+ const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
218
244
  return {
219
245
  success: false,
220
246
  error,
221
247
  isRateLimit: response.status === 429,
222
- isAuthError: response.status === 401 || response.status === 403
248
+ isAuthError: response.status === 401 || response.status === 403,
249
+ status: response.status,
250
+ host,
251
+ ...detail ? { detail } : {},
252
+ ...Number.isFinite(retryAfterSeconds) ? { retryAfterSeconds } : {}
223
253
  };
224
254
  }
225
255
  if (!data.success || !data.response) {
226
256
  return {
227
257
  success: false,
228
- error: data.error || "No response from Spark AI"
258
+ error: `${host} 200 but no content: ${data.error || firstLine(bodyText) || "empty response"}`,
259
+ status: response.status,
260
+ host,
261
+ ...bodyText ? { detail: truncate(bodyText, 800) } : {}
229
262
  };
230
263
  }
231
264
  return {
@@ -239,14 +272,16 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
239
272
  }
240
273
  };
241
274
  } catch (err) {
242
- if (attempt === 3) throw err;
275
+ if (attempt === 3) {
276
+ return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
277
+ }
243
278
  const delayMs = 1e3 * 2 ** (attempt - 1);
244
279
  await new Promise((resolve9) => setTimeout(resolve9, delayMs));
245
280
  }
246
281
  }
247
- return lastRateLimitError || { success: false, error: "Request failed after retries" };
282
+ return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
248
283
  }
249
- async function callAI(systemPrompt, userPrompt, tier = "free") {
284
+ async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
250
285
  if (!apiKey()) {
251
286
  return {
252
287
  success: false,
@@ -256,12 +291,12 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
256
291
  }
257
292
  try {
258
293
  const token = await getOrRefreshDirectToken();
259
- return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
294
+ return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier, opts);
260
295
  } catch (err) {
261
296
  return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
262
297
  }
263
298
  }
264
- async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
299
+ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", opts = {}) {
265
300
  if (!email) {
266
301
  return {
267
302
  success: false,
@@ -271,11 +306,11 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
271
306
  }
272
307
  try {
273
308
  let token = await getOrRefreshRelayToken(email);
274
- let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
309
+ let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
275
310
  if (!result.success && result.isAuthError) {
276
311
  cachedRelayToken = null;
277
312
  token = await getOrRefreshRelayToken(email);
278
- result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
313
+ result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
279
314
  }
280
315
  return result;
281
316
  } catch (err) {
@@ -307,11 +342,13 @@ function normalizeUrl(input) {
307
342
  url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
308
343
  return `https://${url}`;
309
344
  }
310
- var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
345
+ var SERVER_BACKOFF_MS, CLI_BACKOFF_MS, SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
311
346
  var init_api_client = __esm({
312
347
  "src/lib/api-client.ts"() {
313
348
  "use strict";
314
349
  init_utils();
350
+ SERVER_BACKOFF_MS = [2e3, 4e3];
351
+ CLI_BACKOFF_MS = [1e4, 3e4];
315
352
  SPARK_AI_BASE = "https://sparkbrain.app";
316
353
  SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
317
354
  SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
@@ -342,7 +379,7 @@ async function getStatusCached(email) {
342
379
  function getTier(d) {
343
380
  if (!d.subscription || !d.registered) return "free";
344
381
  if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
345
- return TIERS[d.subscription.price_id] ?? "free";
382
+ return TIERS[d.subscription.price_id] ?? "pro";
346
383
  }
347
384
  async function getTierByEmail(email) {
348
385
  try {
@@ -359,17 +396,18 @@ async function isVerified(email) {
359
396
  }
360
397
  }
361
398
  function pricingUrl(opts) {
362
- const u = new URL(`${BASE}/pricing/${APP_ID}`);
399
+ const u = new URL(`${BASE}/${OWNER_SLUG}/${APP_ID}`);
363
400
  if (opts?.email) u.searchParams.set("email", opts.email);
364
401
  if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
365
402
  return u.toString();
366
403
  }
367
- var APP_ID, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
404
+ var APP_ID, OWNER_SLUG, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
368
405
  var init_SparkStripe_feature_gating = __esm({
369
406
  "src/lib/SparkStripe-feature-gating.ts"() {
370
407
  "use strict";
371
408
  APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
372
- BASE = "https://sparkstripe.com";
409
+ OWNER_SLUG = process.env.SPARK_OWNER_SLUG ?? "spark";
410
+ BASE = "https://sparkpay.dev";
373
411
  TIERS = {
374
412
  price_1TADnPEf9opkuWid9NtqOYON: "pro",
375
413
  price_1TADnPEf9opkuWidgbvvX9ma: "pro",
@@ -571,6 +609,9 @@ var init_scraping = __esm({
571
609
  });
572
610
 
573
611
  // src/lib/crawl.ts
612
+ function idSelector(id) {
613
+ return /^[A-Za-z_-][\w-]*$/.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
614
+ }
574
615
  function getActionForType(type) {
575
616
  switch (type) {
576
617
  case "button":
@@ -596,7 +637,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
596
637
  return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
597
638
  }
598
639
  const browserTask = task2("Launching browser");
599
- const browser = await import_playwright.chromium.launch({ headless: true });
640
+ const browser = await import_playwright3.chromium.launch({ headless: true });
600
641
  const context = await browser.newContext({
601
642
  ...storageStatePath ? { storageState: storageStatePath } : {},
602
643
  // Crawl at the size we will record at, or the measurements below describe
@@ -700,9 +741,9 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
700
741
  } else if (el.isFile || el.type === "file") {
701
742
  selector = 'input[type="file"]';
702
743
  } else if (el.type === "range") {
703
- selector = el.id ? `#${el.id}` : 'input[type="range"]';
744
+ selector = el.id ? idSelector(el.id) : 'input[type="range"]';
704
745
  } else if (el.type === "checkbox") {
705
- selector = el.id ? `#${el.id}` : 'input[type="checkbox"]';
746
+ selector = el.id ? idSelector(el.id) : 'input[type="checkbox"]';
706
747
  } else if (el.type === "button") {
707
748
  if (btnTexts[el.text] === 1) {
708
749
  selector = `text="${el.text}"`;
@@ -712,7 +753,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
712
753
  } else if (el.type === "link") {
713
754
  selector = `text="${el.text}"`;
714
755
  } else if (el.id) {
715
- selector = `#${el.id}`;
756
+ selector = idSelector(el.id);
716
757
  } else if (el.name) {
717
758
  selector = `[name="${el.name}"]`;
718
759
  } else if (el.placeholder) {
@@ -747,11 +788,11 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
747
788
  }
748
789
  return { title, elements, scrollable };
749
790
  }
750
- var import_playwright, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
791
+ var import_playwright3, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
751
792
  var init_crawl = __esm({
752
793
  "src/lib/crawl.ts"() {
753
794
  "use strict";
754
- import_playwright = require("playwright");
795
+ import_playwright3 = require("playwright");
755
796
  init_scraping();
756
797
  noopTask = () => ({ succeed: () => {
757
798
  } });
@@ -813,9 +854,9 @@ function convertArgToWSL(arg2) {
813
854
  function spawnFFmpeg(cmd2, args) {
814
855
  if (isWindowsRuntime) {
815
856
  const wslArgs = args.map(convertArgToWSL);
816
- return (0, import_node_child_process.spawn)("wsl", ["-e", cmd2, ...wslArgs]);
857
+ return (0, import_node_child_process2.spawn)("wsl", ["-e", cmd2, ...wslArgs]);
817
858
  }
818
- return (0, import_node_child_process.spawn)(cmd2, args);
859
+ return (0, import_node_child_process2.spawn)(cmd2, args);
819
860
  }
820
861
  function runFFmpeg(args, errorLabel) {
821
862
  return new Promise((resolve9, reject) => {
@@ -831,11 +872,11 @@ function runFFmpeg(args, errorLabel) {
831
872
  proc.on("error", reject);
832
873
  });
833
874
  }
834
- var import_node_child_process, isWindowsRuntime;
875
+ var import_node_child_process2, isWindowsRuntime;
835
876
  var init_spawn = __esm({
836
877
  "src/lib/ffmpeg/spawn.ts"() {
837
878
  "use strict";
838
- import_node_child_process = require("child_process");
879
+ import_node_child_process2 = require("child_process");
839
880
  init_utils();
840
881
  isWindowsRuntime = process.platform === "win32";
841
882
  }
@@ -1016,8 +1057,39 @@ function getMediaDuration(filePath) {
1016
1057
  proc.on("error", reject);
1017
1058
  });
1018
1059
  }
1060
+ function getAudioCodec(filePath) {
1061
+ return new Promise((resolve9) => {
1062
+ const args = [
1063
+ "-v",
1064
+ "error",
1065
+ "-select_streams",
1066
+ "a:0",
1067
+ "-show_entries",
1068
+ "stream=codec_name",
1069
+ "-of",
1070
+ "default=noprint_wrappers=1:nokey=1",
1071
+ filePath
1072
+ ];
1073
+ const proc = spawnFFmpeg("ffprobe", args);
1074
+ let stdout = "";
1075
+ proc.stdout.on("data", (data) => {
1076
+ stdout += data;
1077
+ });
1078
+ proc.on("close", (code) => resolve9(code === 0 ? stdout.trim() : ""));
1079
+ proc.on("error", () => resolve9(""));
1080
+ });
1081
+ }
1082
+ async function isHonestWav(filePath) {
1083
+ const codec = await getAudioCodec(filePath);
1084
+ return codec.startsWith("pcm_");
1085
+ }
1086
+ function buildConcatArgs(listPath, output) {
1087
+ const isWav = output.toLowerCase().endsWith(".wav");
1088
+ const codec = isWav ? ["-c:a", "pcm_s16le"] : ["-c", "copy"];
1089
+ return ["-y", "-f", "concat", "-safe", "0", "-i", listPath, ...codec, output];
1090
+ }
1019
1091
  function concatMedia(listPath, output) {
1020
- return runFFmpeg(["-y", "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", output], "ffmpeg concat failed");
1092
+ return runFFmpeg(buildConcatArgs(listPath, output), "ffmpeg concat failed");
1021
1093
  }
1022
1094
  function generateSilence(outputPath, durationMs) {
1023
1095
  return runFFmpeg(
@@ -1266,6 +1338,114 @@ var init_ffmpeg2 = __esm({
1266
1338
  }
1267
1339
  });
1268
1340
 
1341
+ // src/lib/json-repair.ts
1342
+ function repairUnicodeEscapes(content) {
1343
+ return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
1344
+ const code = Number.parseInt(hex, 16);
1345
+ return code <= 1114111 ? String.fromCodePoint(code) : match;
1346
+ }).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
1347
+ }
1348
+ function salvageTruncatedJson(content) {
1349
+ const stack = [];
1350
+ const cuts = [];
1351
+ let inString = false;
1352
+ let escaped = false;
1353
+ const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
1354
+ for (let i = 0; i < content.length; i++) {
1355
+ const ch = content[i];
1356
+ if (inString) {
1357
+ if (escaped) escaped = false;
1358
+ else if (ch === "\\") escaped = true;
1359
+ else if (ch === '"') {
1360
+ inString = false;
1361
+ record(i + 1, false);
1362
+ }
1363
+ continue;
1364
+ }
1365
+ if (ch === '"') {
1366
+ inString = true;
1367
+ } else if (ch === "{" || ch === "[") {
1368
+ stack.push(ch);
1369
+ } else if (ch === "}" || ch === "]") {
1370
+ stack.pop();
1371
+ record(i + 1, true);
1372
+ } else if (ch === ",") {
1373
+ record(i, content[i - 1] === "}" || content[i - 1] === "]");
1374
+ }
1375
+ }
1376
+ const tryCut = (cut) => {
1377
+ const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
1378
+ if (head.endsWith(":")) return null;
1379
+ let closed = head;
1380
+ for (let d = cut.open.length - 1; d >= 0; d--) {
1381
+ closed += CLOSERS[cut.open[d]];
1382
+ }
1383
+ try {
1384
+ JSON.parse(closed);
1385
+ return closed;
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ };
1390
+ const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
1391
+ for (const containerOnly of [true, false]) {
1392
+ for (let n = 0; n < attempts; n++) {
1393
+ const cut = cuts[cuts.length - 1 - n];
1394
+ if (containerOnly && !cut.closesContainer) continue;
1395
+ const salvaged = tryCut(cut);
1396
+ if (salvaged !== null) return salvaged;
1397
+ }
1398
+ }
1399
+ return null;
1400
+ }
1401
+ function extractJsonCandidates(content) {
1402
+ let text = content.trim();
1403
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
1404
+ if (fence?.[1]?.trim()) text = fence[1].trim();
1405
+ const start = text.search(/[{[]/);
1406
+ if (start === -1) return [];
1407
+ const closer = text[start] === "{" ? "}" : "]";
1408
+ const end = text.lastIndexOf(closer);
1409
+ const candidates = [text.slice(start)];
1410
+ if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
1411
+ return candidates;
1412
+ }
1413
+ function parseAIJson(content) {
1414
+ const candidates = extractJsonCandidates(content);
1415
+ if (candidates.length === 0) return null;
1416
+ for (const text of candidates) {
1417
+ try {
1418
+ return { value: JSON.parse(text), repaired: false };
1419
+ } catch {
1420
+ }
1421
+ }
1422
+ for (const text of candidates) {
1423
+ try {
1424
+ return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
1425
+ } catch {
1426
+ }
1427
+ }
1428
+ for (const text of candidates) {
1429
+ for (const variant of [repairUnicodeEscapes(text), text]) {
1430
+ const salvaged = salvageTruncatedJson(variant);
1431
+ if (salvaged === null) continue;
1432
+ try {
1433
+ return { value: JSON.parse(salvaged), repaired: true };
1434
+ } catch {
1435
+ }
1436
+ }
1437
+ }
1438
+ return null;
1439
+ }
1440
+ var MAX_SALVAGE_ATTEMPTS, CLOSERS;
1441
+ var init_json_repair = __esm({
1442
+ "src/lib/json-repair.ts"() {
1443
+ "use strict";
1444
+ MAX_SALVAGE_ATTEMPTS = 500;
1445
+ CLOSERS = { "{": "}", "[": "]" };
1446
+ }
1447
+ });
1448
+
1269
1449
  // src/lib/prompts.ts
1270
1450
  function buildSystemPrompt(maxSteps) {
1271
1451
  let scope;
@@ -1467,7 +1647,7 @@ var init_styles = __esm({
1467
1647
  async function registerFreeUser(email) {
1468
1648
  const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
1469
1649
  try {
1470
- const response = await fetch(`https://sparkstripe.com/api/register-free`, {
1650
+ const response = await fetch(`https://sparkpay.dev/api/register-free`, {
1471
1651
  method: "POST",
1472
1652
  headers: { "Content-Type": "application/json" },
1473
1653
  body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
@@ -1534,7 +1714,7 @@ async function getMediaStepDuration(step, baseDir) {
1534
1714
  return 0;
1535
1715
  }
1536
1716
  const mediaPath = (0, import_node_path2.isAbsolute)(step.value) ? step.value : (0, import_node_path2.resolve)(baseDir, step.value);
1537
- if (!(0, import_node_fs2.existsSync)(mediaPath)) {
1717
+ if (!(0, import_node_fs3.existsSync)(mediaPath)) {
1538
1718
  return 0;
1539
1719
  }
1540
1720
  try {
@@ -1556,8 +1736,8 @@ async function getMediaStepDuration(step, baseDir) {
1556
1736
  async function loadVoiceoverMeta(outputDir) {
1557
1737
  const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
1558
1738
  try {
1559
- if (!(0, import_node_fs2.existsSync)(metaPath)) return null;
1560
- const content = await (0, import_promises2.readFile)(metaPath, "utf-8");
1739
+ if (!(0, import_node_fs3.existsSync)(metaPath)) return null;
1740
+ const content = await (0, import_promises3.readFile)(metaPath, "utf-8");
1561
1741
  return JSON.parse(content);
1562
1742
  } catch {
1563
1743
  return null;
@@ -1565,7 +1745,7 @@ async function loadVoiceoverMeta(outputDir) {
1565
1745
  }
1566
1746
  async function saveVoiceoverMeta(outputDir, meta) {
1567
1747
  const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
1568
- await (0, import_promises2.writeFile)(metaPath, JSON.stringify(meta, null, 2));
1748
+ await (0, import_promises3.writeFile)(metaPath, JSON.stringify(meta, null, 2));
1569
1749
  }
1570
1750
  async function concatenateAudio(inputs, output, workDir) {
1571
1751
  const formatPath = (f) => {
@@ -1574,17 +1754,17 @@ async function concatenateAudio(inputs, output, workDir) {
1574
1754
  };
1575
1755
  const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
1576
1756
  const listPath = (0, import_node_path2.join)(workDir, "concat.txt");
1577
- await (0, import_promises2.writeFile)(listPath, listContent);
1757
+ await (0, import_promises3.writeFile)(listPath, listContent);
1578
1758
  await concatMedia(listPath, output);
1579
- await (0, import_promises2.unlink)(listPath).catch(() => {
1759
+ await (0, import_promises3.unlink)(listPath).catch(() => {
1580
1760
  });
1581
1761
  }
1582
- var import_node_fs2, import_promises2, import_node_path2, isWindows;
1762
+ var import_node_fs3, import_promises3, import_node_path2, isWindows;
1583
1763
  var init_helpers = __esm({
1584
1764
  "src/lib/tts/helpers.ts"() {
1585
1765
  "use strict";
1586
- import_node_fs2 = require("fs");
1587
- import_promises2 = require("fs/promises");
1766
+ import_node_fs3 = require("fs");
1767
+ import_promises3 = require("fs/promises");
1588
1768
  import_node_path2 = require("path");
1589
1769
  init_ffmpeg2();
1590
1770
  init_utils();
@@ -1633,9 +1813,9 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1633
1813
  const audioPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}.mp3`);
1634
1814
  const originalPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-padding.mp3`);
1635
1815
  const silencePath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-silence.mp3`);
1636
- if ((0, import_node_fs3.existsSync)(audioPath)) {
1816
+ if ((0, import_node_fs4.existsSync)(audioPath)) {
1637
1817
  segments.push(audioPath);
1638
- if ((0, import_node_fs3.existsSync)(originalPaddingPath)) {
1818
+ if ((0, import_node_fs4.existsSync)(originalPaddingPath)) {
1639
1819
  segments.push(originalPaddingPath);
1640
1820
  }
1641
1821
  if (recordingDuration > voiceDuration) {
@@ -1643,7 +1823,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1643
1823
  await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
1644
1824
  segments.push(alignPaddingPath);
1645
1825
  }
1646
- } else if ((0, import_node_fs3.existsSync)(silencePath)) {
1826
+ } else if ((0, import_node_fs4.existsSync)(silencePath)) {
1647
1827
  if (targetDuration !== voiceDuration) {
1648
1828
  await generateSilence(silencePath, targetDuration);
1649
1829
  }
@@ -1667,11 +1847,11 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1667
1847
  }
1668
1848
  return alignedStepAudio;
1669
1849
  }
1670
- var import_node_fs3, import_node_path3;
1850
+ var import_node_fs4, import_node_path3;
1671
1851
  var init_timing = __esm({
1672
1852
  "src/lib/tts/timing.ts"() {
1673
1853
  "use strict";
1674
- import_node_fs3 = require("fs");
1854
+ import_node_fs4 = require("fs");
1675
1855
  import_node_path3 = require("path");
1676
1856
  init_ffmpeg2();
1677
1857
  init_helpers();
@@ -1736,6 +1916,19 @@ var init_voices = __esm({
1736
1916
 
1737
1917
  // src/lib/tts/synthesize.ts
1738
1918
  async function synthesizeSpeech(text, outputPath, voice) {
1919
+ let pythonMsg = "";
1920
+ for (let attempt = 0; attempt < PYTHON_ATTEMPTS; attempt++) {
1921
+ try {
1922
+ await synthesizeWithPython(text, outputPath, voice);
1923
+ return;
1924
+ } catch (err) {
1925
+ pythonMsg = err instanceof Error ? err.message : String(err);
1926
+ if (attempt < PYTHON_ATTEMPTS - 1) await new Promise((r) => setTimeout(r, 500));
1927
+ }
1928
+ }
1929
+ await synthesizeWithNode(text, outputPath, voice, pythonMsg);
1930
+ }
1931
+ async function synthesizeWithPython(text, outputPath, voice) {
1739
1932
  const tempScriptPath = (0, import_node_path5.join)((0, import_node_path5.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
1740
1933
  const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(tempScriptPath)) : tempScriptPath;
1741
1934
  const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(outputPath)) : outputPath;
@@ -1753,44 +1946,49 @@ async def main():
1753
1946
  asyncio.run(main())
1754
1947
  `;
1755
1948
  try {
1756
- await (0, import_promises3.writeFile)(tempScriptPath, pythonScript);
1949
+ await (0, import_promises4.writeFile)(tempScriptPath, pythonScript);
1757
1950
  const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
1758
1951
  await execAsync(pythonCmd, { timeout: 3e4 });
1759
- await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
1760
- }));
1761
- return;
1762
- } catch (pythonErr) {
1763
- await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
1764
- }));
1765
- const pythonMsg = pythonErr instanceof Error ? pythonErr.message : String(pythonErr);
1766
- const maxRetries = 2;
1767
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1768
- try {
1769
- const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
1770
- const tts = new MsEdgeTTS();
1771
- await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
1772
- await tts.toFile(outputPath, text);
1773
- return;
1774
- } catch (nodeErr) {
1775
- if (attempt === maxRetries) {
1776
- const nodeMsg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
1777
- throw new Error(`TTS failed (Python: ${pythonMsg.slice(0, 100)}, Node: ${nodeMsg.slice(0, 100)})`);
1778
- }
1779
- await new Promise((r) => setTimeout(r, 500));
1952
+ } finally {
1953
+ await (0, import_promises4.unlink)(tempScriptPath).catch(() => {
1954
+ });
1955
+ }
1956
+ }
1957
+ async function synthesizeWithNode(text, outputPath, voice, pythonMsg, maxRetries = 2) {
1958
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1959
+ try {
1960
+ const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
1961
+ const tts = new MsEdgeTTS();
1962
+ await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
1963
+ const { audioStream } = tts.toStream(text);
1964
+ await (0, import_promises5.pipeline)(audioStream, (0, import_node_fs5.createWriteStream)(outputPath));
1965
+ const { size } = await (0, import_promises4.stat)(outputPath);
1966
+ if (size === 0) throw new Error("No audio data received");
1967
+ return;
1968
+ } catch (nodeErr) {
1969
+ await (0, import_promises4.unlink)(outputPath).catch(() => {
1970
+ });
1971
+ if (attempt === maxRetries) {
1972
+ const nodeMsg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
1973
+ throw new Error(`TTS failed (Python: ${pythonMsg.slice(0, 100)}, Node: ${nodeMsg.slice(0, 100)})`);
1780
1974
  }
1975
+ await new Promise((r) => setTimeout(r, 500));
1781
1976
  }
1782
1977
  }
1783
1978
  }
1784
- var import_node_child_process2, import_promises3, import_node_path5, import_node_util, execAsync;
1979
+ var import_node_child_process3, import_node_fs5, import_promises4, import_node_path5, import_promises5, import_node_util, execAsync, PYTHON_ATTEMPTS;
1785
1980
  var init_synthesize = __esm({
1786
1981
  "src/lib/tts/synthesize.ts"() {
1787
1982
  "use strict";
1788
- import_node_child_process2 = require("child_process");
1789
- import_promises3 = require("fs/promises");
1983
+ import_node_child_process3 = require("child_process");
1984
+ import_node_fs5 = require("fs");
1985
+ import_promises4 = require("fs/promises");
1790
1986
  import_node_path5 = require("path");
1987
+ import_promises5 = require("stream/promises");
1791
1988
  import_node_util = require("util");
1792
1989
  init_helpers();
1793
- execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
1990
+ execAsync = (0, import_node_util.promisify)(import_node_child_process3.exec);
1991
+ PYTHON_ATTEMPTS = 2;
1794
1992
  }
1795
1993
  });
1796
1994
 
@@ -1827,7 +2025,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1827
2025
  return { voiceoverPath: "", stepAudio: stepAudio2 };
1828
2026
  }
1829
2027
  const audioDir = (0, import_node_path6.join)(workDir, "audio");
1830
- await (0, import_promises4.mkdir)(audioDir, { recursive: true });
2028
+ await (0, import_promises6.mkdir)(audioDir, { recursive: true });
1831
2029
  const segments = [];
1832
2030
  const stepAudio = /* @__PURE__ */ new Map();
1833
2031
  const ttsTask = task2("Generating voiceover");
@@ -1878,7 +2076,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1878
2076
  return { voiceoverPath: "", stepAudio };
1879
2077
  }
1880
2078
  const concatTask = task2("Combining audio");
1881
- await (0, import_promises4.mkdir)(outputDir, { recursive: true });
2079
+ await (0, import_promises6.mkdir)(outputDir, { recursive: true });
1882
2080
  const voiceoverPath = (0, import_node_path6.join)(outputDir, "voiceover.wav");
1883
2081
  await concatenateAudio(segments, voiceoverPath, workDir);
1884
2082
  concatTask.succeed("Done");
@@ -1892,11 +2090,11 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1892
2090
  await saveVoiceoverMeta(outputDir, meta);
1893
2091
  return { voiceoverPath, stepAudio };
1894
2092
  }
1895
- var import_promises4, import_node_path6;
2093
+ var import_promises6, import_node_path6;
1896
2094
  var init_generate = __esm({
1897
2095
  "src/lib/tts/generate.ts"() {
1898
2096
  "use strict";
1899
- import_promises4 = require("fs/promises");
2097
+ import_promises6 = require("fs/promises");
1900
2098
  import_node_path6 = require("path");
1901
2099
  init_ffmpeg2();
1902
2100
  init_voices();
@@ -1943,6 +2141,7 @@ var init_types3 = __esm({
1943
2141
  // src/lib/index.ts
1944
2142
  var lib_exports = {};
1945
2143
  __export(lib_exports, {
2144
+ CLI_BACKOFF_MS: () => CLI_BACKOFF_MS,
1946
2145
  CONFIG_FILE: () => CONFIG_FILE,
1947
2146
  CURSOR_CSS: () => CURSOR_CSS,
1948
2147
  CURSOR_PNG: () => CURSOR_PNG,
@@ -1953,9 +2152,11 @@ __export(lib_exports, {
1953
2152
  FEATURES: () => FEATURES,
1954
2153
  INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
1955
2154
  LANGUAGE_VOICES: () => import_video_kit5.WIDE_VOICES,
2155
+ SERVER_BACKOFF_MS: () => SERVER_BACKOFF_MS,
1956
2156
  VERSION: () => VERSION,
1957
2157
  alignVoiceoverToRecording: () => alignVoiceoverToRecording,
1958
2158
  applyTransitions: () => applyTransitions,
2159
+ buildConcatArgs: () => buildConcatArgs,
1959
2160
  buildKeepSegments: () => buildKeepSegments,
1960
2161
  buildStepTransitions: () => buildStepTransitions,
1961
2162
  buildSystemPrompt: () => buildSystemPrompt,
@@ -1969,9 +2170,11 @@ __export(lib_exports, {
1969
2170
  concatVideoWithOutro: () => concatVideoWithOutro,
1970
2171
  crawlPage: () => crawlPage,
1971
2172
  estimateWordTimings: () => estimateWordTimings,
2173
+ extractJsonCandidates: () => extractJsonCandidates,
1972
2174
  generateASSHeader: () => generateASSHeader,
1973
2175
  generateSilence: () => generateSilence,
1974
2176
  generateVoiceover: () => generateVoiceover,
2177
+ getAudioCodec: () => getAudioCodec,
1975
2178
  getErrorMessage: () => getErrorMessage,
1976
2179
  getLanguageName: () => getLanguageName,
1977
2180
  getMediaDuration: () => getMediaDuration,
@@ -1982,20 +2185,25 @@ __export(lib_exports, {
1982
2185
  hasAccess: () => hasAccess,
1983
2186
  hasTrialRemaining: () => hasTrialRemaining,
1984
2187
  hexToASS: () => import_video_kit.hexToAss,
2188
+ idSelector: () => idSelector,
1985
2189
  isCanceling: () => isCanceling,
1986
2190
  isExcludedLink: () => isExcludedLink,
2191
+ isHonestWav: () => isHonestWav,
1987
2192
  isPaid: () => isPaid,
1988
2193
  isVerified: () => isVerified,
1989
2194
  loadExistingVoiceover: () => loadExistingVoiceover,
1990
2195
  loadVoiceoverMeta: () => loadVoiceoverMeta,
1991
2196
  mapTimestampsAfterCompression: () => mapTimestampsAfterCompression,
1992
2197
  normalizeUrl: () => normalizeUrl,
2198
+ parseAIJson: () => parseAIJson,
1993
2199
  parseAIPlanResponse: () => parseAIPlanResponse,
1994
2200
  parsePartial: () => parsePartial,
1995
2201
  pricingUrl: () => pricingUrl,
1996
2202
  registerFreeUser: () => registerFreeUser,
2203
+ repairUnicodeEscapes: () => repairUnicodeEscapes,
1997
2204
  resolveVoice: () => resolveVoice,
1998
2205
  rgbaToHex: () => rgbaToHex,
2206
+ salvageTruncatedJson: () => salvageTruncatedJson,
1999
2207
  shouldSkipLink: () => shouldSkipLink,
2000
2208
  shouldWatermark: () => shouldWatermark,
2001
2209
  showPaywall: () => showPaywall,
@@ -2014,6 +2222,7 @@ var init_lib = __esm({
2014
2222
  init_crawl();
2015
2223
  init_cursor();
2016
2224
  init_ffmpeg2();
2225
+ init_json_repair();
2017
2226
  init_prompts();
2018
2227
  init_SparkStripe_feature_gating();
2019
2228
  init_scraping();
@@ -2032,16 +2241,14 @@ var import_dotenv = __toESM(require("dotenv"));
2032
2241
  var import_chalk21 = __toESM(require("chalk"));
2033
2242
 
2034
2243
  // src/commands/auth-cmd.ts
2035
- var import_node_readline2 = require("readline");
2036
2244
  var import_chalk3 = __toESM(require("chalk"));
2037
- var import_playwright2 = require("playwright");
2038
2245
 
2039
2246
  // src/browser/auth-state.ts
2040
2247
  var import_node_fs = require("fs");
2041
2248
  var import_promises = require("fs/promises");
2042
2249
  var import_node_os = require("os");
2043
2250
  var import_node_path = require("path");
2044
- var AUTH_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".quickpeek", "auth");
2251
+ var AUTH_DIR = process.env.QUICKPEEK_AUTH_DIR || (0, import_node_path.join)((0, import_node_os.homedir)(), ".quickpeek", "auth");
2045
2252
  function hostOf(url) {
2046
2253
  try {
2047
2254
  return new URL(url).host;
@@ -2053,156 +2260,468 @@ function authStatePath(url) {
2053
2260
  const host = hostOf(url);
2054
2261
  return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.json`) : null;
2055
2262
  }
2263
+ function authMetaPath(url) {
2264
+ const host = hostOf(url);
2265
+ return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.meta.json`) : null;
2266
+ }
2056
2267
  function findAuthState(url) {
2057
2268
  const path = authStatePath(url);
2058
2269
  return path && (0, import_node_fs.existsSync)(path) ? path : void 0;
2059
2270
  }
2060
- async function saveAuthState(url, state) {
2271
+ async function saveAuthState(url, state, startUrl) {
2061
2272
  const path = authStatePath(url);
2062
2273
  if (!path) throw new Error(`Not a valid URL: ${url}`);
2063
2274
  await (0, import_promises.mkdir)(AUTH_DIR, { recursive: true });
2064
2275
  await (0, import_promises.writeFile)(path, JSON.stringify(state, null, 2));
2065
2276
  await (0, import_promises.chmod)(path, 384);
2277
+ if (startUrl) {
2278
+ await (0, import_promises.writeFile)(authMetaPath(url), JSON.stringify({ startUrl, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
2279
+ }
2066
2280
  return path;
2067
2281
  }
2282
+ function findStartUrl(url) {
2283
+ const path = authMetaPath(url);
2284
+ if (!path || !(0, import_node_fs.existsSync)(path)) return void 0;
2285
+ try {
2286
+ const startUrl = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8")).startUrl;
2287
+ return typeof startUrl === "string" && startUrl.startsWith("http") ? startUrl : void 0;
2288
+ } catch {
2289
+ return void 0;
2290
+ }
2291
+ }
2292
+ function resolveStartUrl(url) {
2293
+ try {
2294
+ const parsed = new URL(url);
2295
+ const bareOrigin = (parsed.pathname === "/" || parsed.pathname === "") && !parsed.search && !parsed.hash;
2296
+ if (!bareOrigin || !findAuthState(url)) return url;
2297
+ return findStartUrl(url) ?? url;
2298
+ } catch {
2299
+ return url;
2300
+ }
2301
+ }
2068
2302
  async function clearAuthState(url) {
2069
2303
  if (url) {
2070
2304
  const path = authStatePath(url);
2071
2305
  if (!path || !(0, import_node_fs.existsSync)(path)) return [];
2072
2306
  await (0, import_promises.unlink)(path);
2307
+ const meta = authMetaPath(url);
2308
+ if (meta && (0, import_node_fs.existsSync)(meta)) await (0, import_promises.unlink)(meta);
2073
2309
  return [hostOf(url)];
2074
2310
  }
2075
2311
  if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
2076
2312
  const files = (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json"));
2077
2313
  await Promise.all(files.map((f) => (0, import_promises.unlink)((0, import_node_path.join)(AUTH_DIR, f))));
2078
- return files.map((f) => f.slice(0, -".json".length));
2314
+ return files.filter((f) => !f.endsWith(".meta.json")).map((f) => f.slice(0, -".json".length));
2079
2315
  }
2080
2316
  async function listAuthStates() {
2081
2317
  if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
2082
- return (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length));
2318
+ return (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json") && !f.endsWith(".meta.json")).map((f) => f.slice(0, -".json".length));
2083
2319
  }
2084
2320
 
2085
- // src/help.ts
2086
- var import_chalk = __toESM(require("chalk"));
2087
- init_lib();
2088
-
2089
- // src/logo.ts
2090
- var import_figlet = __toESM(require("figlet"));
2091
- var import_gradient_string = __toESM(require("gradient-string"));
2092
- var DEFAULT_LOGO_CONFIG = {
2093
- text: "QuickPeek",
2094
- font: "Slant",
2095
- horizontalLayout: "default",
2096
- gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
2097
- suffix: " \u{1F440}"
2098
- };
2099
- function renderLogo(config = {}) {
2100
- const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
2101
- const ascii = import_figlet.default.textSync(resolved.text, {
2102
- font: resolved.font,
2103
- horizontalLayout: resolved.horizontalLayout
2104
- });
2105
- const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
2106
- return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
2107
- }
2321
+ // src/browser/launch.ts
2322
+ var import_promises2 = require("fs/promises");
2323
+ var import_playwright2 = require("playwright");
2108
2324
 
2109
- // src/utils/config-loader.ts
2110
- var import_node_fs4 = require("fs");
2111
- var import_promises5 = require("fs/promises");
2112
- var import_node_os2 = require("os");
2113
- var import_node_path7 = require("path");
2114
- init_lib();
2115
- async function loadConfig(_url, langOverride) {
2116
- const configPath = (0, import_node_path7.join)(process.cwd(), CONFIG_FILE);
2117
- if ((0, import_node_fs4.existsSync)(configPath)) {
2118
- try {
2119
- const content = await (0, import_promises5.readFile)(configPath, "utf-8");
2120
- const userConfig = JSON.parse(content);
2121
- const config = {
2122
- lang: langOverride || userConfig.lang || "en",
2123
- plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
2124
- video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
2125
- audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
2126
- music: userConfig.music,
2127
- timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
2128
- output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
2129
- };
2130
- return { config, created: false };
2131
- } catch {
2132
- }
2325
+ // src/browser/wsl.ts
2326
+ var import_node_child_process = require("child_process");
2327
+ var import_node_fs2 = require("fs");
2328
+ var import_node_http = require("http");
2329
+ var import_playwright = require("playwright");
2330
+ function isWSL() {
2331
+ if (process.platform !== "linux") return false;
2332
+ try {
2333
+ const r = (0, import_node_child_process.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
2334
+ return r.includes("microsoft") || r.includes("wsl");
2335
+ } catch {
2336
+ return false;
2133
2337
  }
2134
- const fullDefaultConfig = {
2135
- lang: "en",
2136
- plan: { maxSteps: 8 },
2137
- video: {
2138
- width: 1080,
2139
- height: 1920,
2140
- fps: 30,
2141
- quality: 0.5,
2142
- preset: "fast",
2143
- cursor: true,
2144
- highlight: false,
2145
- fadeIn: 500,
2146
- // ms
2147
- fadeOut: 500,
2148
- // ms
2149
- contrast: 0.25
2150
- },
2151
- audio: {
2152
- // Voice is derived from lang setting via LANGUAGE_VOICES mapping
2153
- // To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
2154
- speed: 1
2155
- },
2156
- music: {
2157
- path: "",
2158
- volume: 0.3
2159
- },
2160
- output: { dir: "./demo" }
2161
- };
2162
- await (0, import_promises5.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
2163
- return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
2164
2338
  }
2165
- var WORK_DIR = ".quickpeek";
2166
- async function prepareWorkDir() {
2167
- const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
2168
- if ((0, import_node_fs4.existsSync)(workDir)) {
2169
- await (0, import_promises5.rm)(workDir, { recursive: true });
2170
- }
2171
- await (0, import_promises5.mkdir)(workDir, { recursive: true });
2172
- return workDir;
2339
+ function tryEnableWSLInterop() {
2340
+ return (0, import_node_fs2.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
2173
2341
  }
2174
- var USER_DATA_DIR = (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".quickpeek");
2175
- var USER_DATA_FILE = (0, import_node_path7.join)(USER_DATA_DIR, "user.json");
2176
- async function loadUserData() {
2342
+ function wslToWindowsPath(wslPath) {
2343
+ if (!wslPath) return wslPath;
2344
+ const m = wslPath.match(/^\/mnt\/([a-z])\/(.*)/);
2345
+ if (!m) return wslPath;
2346
+ return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
2347
+ }
2348
+ function findExe(whereExeQuery, ...hardcoded) {
2349
+ for (const p of hardcoded) if ((0, import_node_fs2.existsSync)(p)) return p;
2177
2350
  try {
2178
- if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
2179
- return JSON.parse(await (0, import_promises5.readFile)(USER_DATA_FILE, "utf-8"));
2180
- }
2351
+ const line = (0, import_node_child_process.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
2352
+ if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
2181
2353
  } catch {
2182
2354
  }
2183
- return {};
2355
+ return null;
2184
2356
  }
2185
- async function saveUserData(userData) {
2186
- if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
2187
- await (0, import_promises5.mkdir)(USER_DATA_DIR, { recursive: true });
2357
+ var findWindowsChrome = () => findExe(
2358
+ "chrome.exe",
2359
+ "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
2360
+ "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
2361
+ );
2362
+ var findWindowsNode = () => findExe("node.exe");
2363
+ function getWindowsHostIP() {
2364
+ try {
2365
+ const m = (0, import_node_child_process.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
2366
+ /default via (\d+\.\d+\.\d+\.\d+)/
2367
+ );
2368
+ if (m?.[1]) return m[1];
2369
+ } catch {
2188
2370
  }
2189
- const existing = await loadUserData();
2190
- const merged = { ...existing, ...userData };
2191
- await (0, import_promises5.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
2192
- }
2193
- var DEFAULT_CAPTIONS_CSS = `.caption {
2194
- font-family: Arial, sans-serif;
2195
- -webkit-text-stroke: 3px #000000;
2196
- }
2197
- .caption .regular {
2198
- font-size: 56px;
2199
- color: #FFFFFF;
2371
+ try {
2372
+ const m = (0, import_node_fs2.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
2373
+ if (m?.[1]) return m[1];
2374
+ } catch {
2375
+ }
2376
+ return "127.0.0.1";
2200
2377
  }
2201
- .caption .highlight {
2202
- font-size: 64px;
2203
- color: #FFFF00;
2378
+ function startRelay(nodePath, windowsHost, targetPort) {
2379
+ const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
2380
+ return new Promise((resolve9, reject) => {
2381
+ const proc = (0, import_node_child_process.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
2382
+ const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
2383
+ let buf = "";
2384
+ proc.stdout.on("data", (d) => {
2385
+ buf += d.toString();
2386
+ const m = buf.match(/PORT:(\d+)/);
2387
+ if (m) {
2388
+ clearTimeout(timer);
2389
+ resolve9({ proc, port: +m[1] });
2390
+ }
2391
+ });
2392
+ proc.on("error", (e) => {
2393
+ clearTimeout(timer);
2394
+ reject(e);
2395
+ });
2396
+ });
2204
2397
  }
2205
- `;
2398
+ function waitForChrome(host, port) {
2399
+ return new Promise((resolve9, reject) => {
2400
+ const deadline = Date.now() + 2e4;
2401
+ const poll = () => {
2402
+ if (Date.now() > deadline) {
2403
+ reject(new Error("Chrome debug port did not open within 20 s"));
2404
+ return;
2405
+ }
2406
+ const req = (0, import_node_http.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
2407
+ res.resume();
2408
+ resolve9();
2409
+ });
2410
+ req.on("error", () => setTimeout(poll, 600));
2411
+ req.setTimeout(1e3, () => {
2412
+ req.destroy();
2413
+ setTimeout(poll, 600);
2414
+ });
2415
+ req.end();
2416
+ };
2417
+ poll();
2418
+ });
2419
+ }
2420
+ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
2421
+ const debugPort = options.debugPort ?? 9222;
2422
+ const chromePath = findWindowsChrome();
2423
+ if (!chromePath) throw new Error("Windows Chrome not found. Install Google Chrome for Windows.");
2424
+ const nodePath = findWindowsNode();
2425
+ if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
2426
+ const winUserData = wslToWindowsPath(userDataDir);
2427
+ const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
2428
+ const chromeProc = (0, import_node_child_process.spawn)(
2429
+ chromePath,
2430
+ [
2431
+ `--remote-debugging-port=${debugPort}`,
2432
+ `--user-data-dir=${winUserData}`,
2433
+ ...winExtPath ? [`--disable-extensions-except=${winExtPath}`, `--load-extension=${winExtPath}`] : [],
2434
+ "--no-first-run",
2435
+ "--no-default-browser-check",
2436
+ "--disable-background-networking",
2437
+ "--disable-sync",
2438
+ "--disable-features=TranslateUI",
2439
+ options.startMaximized ? "--start-maximized" : "--start-minimized"
2440
+ ],
2441
+ { stdio: "ignore", detached: true }
2442
+ );
2443
+ chromeProc.unref();
2444
+ const windowsHost = getWindowsHostIP();
2445
+ const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
2446
+ try {
2447
+ await waitForChrome(windowsHost, proxyPort);
2448
+ const browser = await import_playwright.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
2449
+ return {
2450
+ browser,
2451
+ cleanup: async () => {
2452
+ try {
2453
+ await browser.close();
2454
+ } catch {
2455
+ }
2456
+ relay.kill();
2457
+ }
2458
+ };
2459
+ } catch (err) {
2460
+ relay.kill();
2461
+ throw err;
2462
+ }
2463
+ }
2464
+
2465
+ // src/browser/launch.ts
2466
+ function recordingViewport(config) {
2467
+ const zoom = config.video.zoom && config.video.zoom > 0 ? config.video.zoom : 1;
2468
+ return {
2469
+ width: Math.round(config.video.width / zoom),
2470
+ height: Math.round(config.video.height / zoom)
2471
+ };
2472
+ }
2473
+ async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
2474
+ const extensionPath = config.browser?.extension;
2475
+ const zoom = config.video.zoom && config.video.zoom > 0 ? config.video.zoom : 1;
2476
+ const viewport = recordingViewport(config);
2477
+ const scaleOpts = zoom === 1 ? {} : { deviceScaleFactor: zoom };
2478
+ const videoOpts = recordVideo ? { recordVideo } : {};
2479
+ const cspOpts = { bypassCSP: true };
2480
+ const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
2481
+ let context;
2482
+ let browser = null;
2483
+ let chromeSession = null;
2484
+ if (extensionPath) {
2485
+ await (0, import_promises2.mkdir)(userDataDir, { recursive: true });
2486
+ const persistentArgs = [
2487
+ `--disable-extensions-except=${extensionPath}`,
2488
+ `--load-extension=${extensionPath}`,
2489
+ "--window-position=-32000,-32000",
2490
+ "--window-size=1,1"
2491
+ ];
2492
+ const useWindowsChrome = isWSL() && tryEnableWSLInterop();
2493
+ if (useWindowsChrome) {
2494
+ try {
2495
+ chromeSession = await launchWindowsChrome(extensionPath, userDataDir, { viewport, ...scaleOpts, ...videoOpts });
2496
+ const contexts = chromeSession.browser.contexts();
2497
+ context = contexts[0] || await chromeSession.browser.newContext({
2498
+ viewport,
2499
+ ...scaleOpts,
2500
+ ...videoOpts,
2501
+ ...cspOpts,
2502
+ acceptDownloads: true
2503
+ });
2504
+ } catch {
2505
+ context = await import_playwright2.chromium.launchPersistentContext(userDataDir, {
2506
+ headless: false,
2507
+ args: persistentArgs,
2508
+ viewport,
2509
+ ...scaleOpts,
2510
+ ...videoOpts,
2511
+ ...cspOpts,
2512
+ acceptDownloads: true
2513
+ });
2514
+ }
2515
+ } else {
2516
+ context = await import_playwright2.chromium.launchPersistentContext(userDataDir, {
2517
+ headless: false,
2518
+ args: persistentArgs,
2519
+ viewport,
2520
+ ...scaleOpts,
2521
+ ...videoOpts,
2522
+ ...cspOpts,
2523
+ acceptDownloads: true
2524
+ });
2525
+ }
2526
+ } else {
2527
+ if (process.env.QP_HEADED === "1") {
2528
+ const headedOpts = {
2529
+ headless: false,
2530
+ args: ["--disable-blink-features=AutomationControlled"],
2531
+ ignoreDefaultArgs: ["--enable-automation"]
2532
+ };
2533
+ try {
2534
+ browser = await import_playwright2.chromium.launch({ ...headedOpts, channel: "chrome" });
2535
+ } catch {
2536
+ browser = await import_playwright2.chromium.launch(headedOpts);
2537
+ }
2538
+ } else {
2539
+ browser = await import_playwright2.chromium.launch({ headless: true });
2540
+ }
2541
+ context = await browser.newContext({
2542
+ viewport,
2543
+ ...scaleOpts,
2544
+ ...videoOpts,
2545
+ ...authOpts,
2546
+ ...cspOpts,
2547
+ acceptDownloads: true
2548
+ });
2549
+ }
2550
+ await hideDevOverlays(context);
2551
+ return { context, browser, chromeSession };
2552
+ }
2553
+ async function hideDevOverlays(context) {
2554
+ await context.addInitScript(() => {
2555
+ const css = "nextjs-portal,vite-error-overlay,vite-plugin-checker-error-overlay{display:none!important;pointer-events:none!important}";
2556
+ const add = () => {
2557
+ const style = document.createElement("style");
2558
+ style.textContent = css;
2559
+ document.head?.appendChild(style);
2560
+ };
2561
+ if (document.head) add();
2562
+ else document.addEventListener("DOMContentLoaded", add, { once: true });
2563
+ }).catch(() => {
2564
+ });
2565
+ }
2566
+ async function launchSignInBrowser() {
2567
+ const opts = {
2568
+ headless: false,
2569
+ args: ["--disable-blink-features=AutomationControlled"],
2570
+ ignoreDefaultArgs: ["--enable-automation"]
2571
+ };
2572
+ try {
2573
+ return await import_playwright2.chromium.launch({ ...opts, channel: "chrome" });
2574
+ } catch {
2575
+ return await import_playwright2.chromium.launch(opts);
2576
+ }
2577
+ }
2578
+ async function cleanupBrowserSession(session) {
2579
+ if (session.chromeSession) {
2580
+ await session.chromeSession.cleanup();
2581
+ } else {
2582
+ await session.context.close();
2583
+ if (session.browser) await session.browser.close();
2584
+ }
2585
+ }
2586
+
2587
+ // src/browser/auth-session.ts
2588
+ function sameSite(a, b) {
2589
+ if (a === b) return true;
2590
+ const tail = (h) => h.split(".").slice(-2).join(".");
2591
+ return a.includes(".") && b.includes(".") && tail(a) === tail(b);
2592
+ }
2593
+ function captureStartUrl(context, targetUrl) {
2594
+ const targetHost = new URL(targetUrl).host;
2595
+ const pages = context.pages();
2596
+ for (let i = pages.length - 1; i >= 0; i--) {
2597
+ const pageUrl = pages[i].url();
2598
+ if (!pageUrl.startsWith("http")) continue;
2599
+ if (sameSite(new URL(pageUrl).host, targetHost)) return pageUrl;
2600
+ }
2601
+ return void 0;
2602
+ }
2603
+
2604
+ // src/help.ts
2605
+ var import_chalk = __toESM(require("chalk"));
2606
+ init_lib();
2607
+
2608
+ // src/logo.ts
2609
+ var import_figlet = __toESM(require("figlet"));
2610
+ var import_gradient_string = __toESM(require("gradient-string"));
2611
+ var DEFAULT_LOGO_CONFIG = {
2612
+ text: "QuickPeek",
2613
+ font: "Slant",
2614
+ horizontalLayout: "default",
2615
+ gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
2616
+ suffix: " \u{1F440}"
2617
+ };
2618
+ function renderLogo(config = {}) {
2619
+ const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
2620
+ const ascii = import_figlet.default.textSync(resolved.text, {
2621
+ font: resolved.font,
2622
+ horizontalLayout: resolved.horizontalLayout
2623
+ });
2624
+ const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
2625
+ return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
2626
+ }
2627
+
2628
+ // src/utils/config-loader.ts
2629
+ var import_node_fs6 = require("fs");
2630
+ var import_promises7 = require("fs/promises");
2631
+ var import_node_os2 = require("os");
2632
+ var import_node_path7 = require("path");
2633
+ init_lib();
2634
+ async function loadConfig(_url, langOverride) {
2635
+ const configPath = (0, import_node_path7.join)(process.cwd(), CONFIG_FILE);
2636
+ if ((0, import_node_fs6.existsSync)(configPath)) {
2637
+ try {
2638
+ const content = await (0, import_promises7.readFile)(configPath, "utf-8");
2639
+ const userConfig = JSON.parse(content);
2640
+ const config = {
2641
+ lang: langOverride || userConfig.lang || "en",
2642
+ plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
2643
+ video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
2644
+ audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
2645
+ music: userConfig.music,
2646
+ timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
2647
+ output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
2648
+ };
2649
+ return { config, created: false };
2650
+ } catch {
2651
+ }
2652
+ }
2653
+ const fullDefaultConfig = {
2654
+ lang: "en",
2655
+ plan: { maxSteps: 8 },
2656
+ video: {
2657
+ width: 1080,
2658
+ height: 1920,
2659
+ fps: 30,
2660
+ quality: 0.5,
2661
+ preset: "fast",
2662
+ cursor: true,
2663
+ highlight: false,
2664
+ fadeIn: 500,
2665
+ // ms
2666
+ fadeOut: 500,
2667
+ // ms
2668
+ contrast: 0.25
2669
+ },
2670
+ audio: {
2671
+ // Voice is derived from lang setting via LANGUAGE_VOICES mapping
2672
+ // To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
2673
+ speed: 1
2674
+ },
2675
+ music: {
2676
+ path: "",
2677
+ volume: 0.3
2678
+ },
2679
+ output: { dir: "./demo" }
2680
+ };
2681
+ await (0, import_promises7.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
2682
+ return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
2683
+ }
2684
+ var WORK_DIR = ".quickpeek";
2685
+ async function prepareWorkDir() {
2686
+ const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
2687
+ if ((0, import_node_fs6.existsSync)(workDir)) {
2688
+ await (0, import_promises7.rm)(workDir, { recursive: true });
2689
+ }
2690
+ await (0, import_promises7.mkdir)(workDir, { recursive: true });
2691
+ return workDir;
2692
+ }
2693
+ var USER_DATA_DIR = (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".quickpeek");
2694
+ var USER_DATA_FILE = (0, import_node_path7.join)(USER_DATA_DIR, "user.json");
2695
+ async function loadUserData() {
2696
+ try {
2697
+ if ((0, import_node_fs6.existsSync)(USER_DATA_FILE)) {
2698
+ return JSON.parse(await (0, import_promises7.readFile)(USER_DATA_FILE, "utf-8"));
2699
+ }
2700
+ } catch {
2701
+ }
2702
+ return {};
2703
+ }
2704
+ async function saveUserData(userData) {
2705
+ if (!(0, import_node_fs6.existsSync)(USER_DATA_DIR)) {
2706
+ await (0, import_promises7.mkdir)(USER_DATA_DIR, { recursive: true });
2707
+ }
2708
+ const existing = await loadUserData();
2709
+ const merged = { ...existing, ...userData };
2710
+ await (0, import_promises7.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
2711
+ }
2712
+ var DEFAULT_CAPTIONS_CSS = `.caption {
2713
+ font-family: Arial, sans-serif;
2714
+ -webkit-text-stroke: 3px #000000;
2715
+ }
2716
+ .caption .regular {
2717
+ font-size: 56px;
2718
+ color: #FFFFFF;
2719
+ }
2720
+ .caption .highlight {
2721
+ font-size: 64px;
2722
+ color: #FFFF00;
2723
+ }
2724
+ `;
2206
2725
  function parseCaptionCSS(css) {
2207
2726
  const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
2208
2727
  const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
@@ -2250,12 +2769,12 @@ function parseCaptionCSS(css) {
2250
2769
  }
2251
2770
  async function loadCaptions() {
2252
2771
  const captionsPath = (0, import_node_path7.join)(process.cwd(), "captions.css");
2253
- if (!(0, import_node_fs4.existsSync)(captionsPath)) {
2254
- await (0, import_promises5.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
2772
+ if (!(0, import_node_fs6.existsSync)(captionsPath)) {
2773
+ await (0, import_promises7.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
2255
2774
  return DEFAULT_CAPTIONS;
2256
2775
  }
2257
2776
  try {
2258
- const css = await (0, import_promises5.readFile)(captionsPath, "utf-8");
2777
+ const css = await (0, import_promises7.readFile)(captionsPath, "utf-8");
2259
2778
  return parseCaptionCSS(css);
2260
2779
  } catch {
2261
2780
  return DEFAULT_CAPTIONS;
@@ -2266,8 +2785,8 @@ async function savePlan(plan, planPath) {
2266
2785
  ...plan,
2267
2786
  steps: plan.steps.map(({ duration, ...step }) => step)
2268
2787
  };
2269
- await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(planPath), { recursive: true });
2270
- await (0, import_promises5.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
2788
+ await (0, import_promises7.mkdir)((0, import_node_path7.dirname)(planPath), { recursive: true });
2789
+ await (0, import_promises7.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
2271
2790
  }
2272
2791
 
2273
2792
  // src/help.ts
@@ -2346,6 +2865,7 @@ async function showHelp() {
2346
2865
  [`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
2347
2866
  [`${cmd("qp logout")}`, `qp logout`, "Sign out"],
2348
2867
  [`${cmd("qp auth")} ${arg("<url>")}`, `qp auth <url>`, "Save a signed-in session for a site you demo"],
2868
+ [`${cmd("qp auth list")}`, `qp auth list`, "Show which sites have a saved session"],
2349
2869
  [`${cmd("qp auth clear")} ${arg("[url]")}`, `qp auth clear [url]`, "Forget saved sessions"],
2350
2870
  [`${cmd("qp init")}`, `qp init`, "Create config file"],
2351
2871
  [`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
@@ -2400,6 +2920,9 @@ var autoYes = false;
2400
2920
  function setAutoYes(value) {
2401
2921
  autoYes = value;
2402
2922
  }
2923
+ function isAutoYes() {
2924
+ return autoYes;
2925
+ }
2403
2926
  function task(text) {
2404
2927
  return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
2405
2928
  }
@@ -2430,11 +2953,20 @@ function promptInput(prompt2) {
2430
2953
  });
2431
2954
  });
2432
2955
  }
2433
- var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
2434
- function formatClickablePath(filePath) {
2435
- const absolutePath = (0, import_node_path8.resolve)(filePath);
2436
- if (_isWSL || process.platform !== "win32") {
2437
- return absolutePath;
2956
+ function waitForEnter(prompt2 = `${INDENT}Press Enter when done...`) {
2957
+ return new Promise((resolve9) => {
2958
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
2959
+ rl.question(prompt2, () => {
2960
+ rl.close();
2961
+ resolve9();
2962
+ });
2963
+ });
2964
+ }
2965
+ var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
2966
+ function formatClickablePath(filePath) {
2967
+ const absolutePath = (0, import_node_path8.resolve)(filePath);
2968
+ if (_isWSL || process.platform !== "win32") {
2969
+ return absolutePath;
2438
2970
  }
2439
2971
  let formatted = absolutePath;
2440
2972
  formatted = formatted.replace(/\//g, "\\");
@@ -2450,15 +2982,6 @@ function formatDuration(ms) {
2450
2982
  }
2451
2983
 
2452
2984
  // src/commands/auth-cmd.ts
2453
- function waitForEnter(prompt2) {
2454
- return new Promise((resolve9) => {
2455
- const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
2456
- rl.question(prompt2, () => {
2457
- rl.close();
2458
- resolve9();
2459
- });
2460
- });
2461
- }
2462
2985
  async function authCommand(url) {
2463
2986
  console.log();
2464
2987
  showLogo();
@@ -2466,9 +2989,10 @@ async function authCommand(url) {
2466
2989
  console.log(import_chalk3.default.red(` Not a valid URL: ${url}`));
2467
2990
  return;
2468
2991
  }
2469
- console.log(import_chalk3.default.white(" A browser will open. Sign in to the site, then come back here."));
2992
+ console.log(import_chalk3.default.white(" A browser will open. Sign in, then leave it on the page demos"));
2993
+ console.log(import_chalk3.default.white(" should start from (usually your dashboard) and come back here."));
2470
2994
  console.log();
2471
- const browser = await import_playwright2.chromium.launch({ headless: false });
2995
+ const browser = await launchSignInBrowser();
2472
2996
  const context = await browser.newContext();
2473
2997
  const page = await context.newPage();
2474
2998
  try {
@@ -2478,11 +3002,15 @@ async function authCommand(url) {
2478
3002
  await waitForEnter(import_chalk3.default.cyan(" Press Enter when you are signed in... "));
2479
3003
  try {
2480
3004
  const state = await context.storageState();
2481
- const path = await saveAuthState(url, state);
3005
+ const startUrl = captureStartUrl(context, url);
3006
+ const path = await saveAuthState(url, state, startUrl);
2482
3007
  console.log();
2483
3008
  console.log(import_chalk3.default.green(` \u2713 Session saved for ${new URL(url).host}`));
2484
3009
  console.log(import_chalk3.default.dim(` ${formatClickablePath(path)}`));
2485
3010
  console.log(import_chalk3.default.dim(" Crawls and recordings of this site now start signed in."));
3011
+ if (startUrl && startUrl !== url) {
3012
+ console.log(import_chalk3.default.dim(" Demos of the bare URL will start at ") + import_chalk3.default.cyan(startUrl));
3013
+ }
2486
3014
  console.log(import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan(`qp auth clear ${new URL(url).host}`) + import_chalk3.default.dim(" to forget it."));
2487
3015
  } finally {
2488
3016
  await browser.close().catch(() => {
@@ -2524,17 +3052,17 @@ var import_chalk11 = __toESM(require("chalk"));
2524
3052
  init_lib();
2525
3053
 
2526
3054
  // src/ai/client.ts
2527
- var import_node_readline3 = require("readline");
3055
+ var import_node_readline2 = require("readline");
2528
3056
  var import_chalk5 = __toESM(require("chalk"));
2529
3057
  init_lib();
2530
3058
 
2531
3059
  // src/utils/browser-auth.ts
2532
- var import_node_http = require("http");
3060
+ var import_node_http2 = require("http");
2533
3061
  var import_node_url = require("url");
2534
3062
  var import_chalk4 = __toESM(require("chalk"));
2535
3063
 
2536
3064
  // src/lib/browser-open.ts
2537
- var import_node_child_process3 = require("child_process");
3065
+ var import_node_child_process4 = require("child_process");
2538
3066
  function editorBaseUrl() {
2539
3067
  return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
2540
3068
  }
@@ -2542,7 +3070,7 @@ function openInBrowser(url) {
2542
3070
  const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
2543
3071
  const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
2544
3072
  return new Promise((resolve9) => {
2545
- (0, import_node_child_process3.exec)(command, () => resolve9());
3073
+ (0, import_node_child_process4.exec)(command, () => resolve9());
2546
3074
  });
2547
3075
  }
2548
3076
 
@@ -2558,7 +3086,7 @@ async function browserLogin() {
2558
3086
  server?.close();
2559
3087
  resolve9(email);
2560
3088
  };
2561
- server = (0, import_node_http.createServer)((req, res) => {
3089
+ server = (0, import_node_http2.createServer)((req, res) => {
2562
3090
  const url = new import_node_url.URL(req.url || "/", `http://localhost`);
2563
3091
  const path = url.pathname;
2564
3092
  if (path === "/callback" || path === "/") {
@@ -2612,13 +3140,132 @@ async function browserLogin() {
2612
3140
  });
2613
3141
  }
2614
3142
 
3143
+ // src/utils/logging.ts
3144
+ var import_promises8 = require("fs/promises");
3145
+ var import_node_path9 = require("path");
3146
+ var LOG_FILE_NAME = "quickpeek.log";
3147
+ var logFileInitialized = false;
3148
+ var logFilePath = LOG_FILE_NAME;
3149
+ async function writeLogEntry(entry) {
3150
+ try {
3151
+ if (!logFileInitialized) {
3152
+ await (0, import_promises8.writeFile)(logFilePath, entry);
3153
+ logFileInitialized = true;
3154
+ } else {
3155
+ await (0, import_promises8.appendFile)(logFilePath, entry);
3156
+ }
3157
+ } catch {
3158
+ }
3159
+ }
3160
+ async function setLogFilePath(outputDir) {
3161
+ logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
3162
+ const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
3163
+ ${"=".repeat(60)}
3164
+
3165
+ `;
3166
+ try {
3167
+ await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(logFilePath), { recursive: true });
3168
+ await (0, import_promises8.writeFile)(logFilePath, header2);
3169
+ logFileInitialized = true;
3170
+ } catch {
3171
+ }
3172
+ }
3173
+ async function logInfo(context, message) {
3174
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
3175
+ `);
3176
+ }
3177
+ async function logDebug(context, message) {
3178
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
3179
+ `);
3180
+ }
3181
+ async function logError(context, error) {
3182
+ let errorMsg;
3183
+ if (error instanceof Error) {
3184
+ errorMsg = `${error.message}
3185
+ ${error.stack}`;
3186
+ } else if (typeof error === "object" && error !== null) {
3187
+ errorMsg = JSON.stringify(error, null, 2);
3188
+ } else {
3189
+ errorMsg = String(error);
3190
+ }
3191
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
3192
+ ${errorMsg}
3193
+
3194
+ `);
3195
+ }
3196
+ async function logStep(stepNum, action, details) {
3197
+ const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
3198
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
3199
+ `);
3200
+ }
3201
+
3202
+ // src/ai/mcp-context.ts
3203
+ var import_node_async_hooks = require("async_hooks");
3204
+ var DelegatedGenerationError = class extends Error {
3205
+ tool;
3206
+ step;
3207
+ /** The system instructions the call was about to send. */
3208
+ system;
3209
+ /** The user-side prompt. */
3210
+ prompt;
3211
+ constructor(tool, step, system, prompt2) {
3212
+ super(
3213
+ `Generation for "${tool}" (${step}) belongs to the calling model, not Groq. The tool should hand back this prompt and accept the result as a parameter.`
3214
+ );
3215
+ this.name = "DelegatedGenerationError";
3216
+ this.tool = tool;
3217
+ this.step = step;
3218
+ this.system = system;
3219
+ this.prompt = prompt2;
3220
+ }
3221
+ };
3222
+ var mcpCall = new import_node_async_hooks.AsyncLocalStorage();
3223
+ function currentMcpTool() {
3224
+ return mcpCall.getStore()?.tool ?? null;
3225
+ }
3226
+ function suppliedAnswer(step) {
3227
+ return mcpCall.getStore()?.answers?.[step];
3228
+ }
3229
+ function recordDelegation(e) {
3230
+ const store = mcpCall.getStore();
3231
+ if (store && !store.pending.some((p) => p.step === e.step)) store.pending.push(e);
3232
+ }
3233
+
2615
3234
  // src/ai/client.ts
2616
3235
  var lastRateLimitInfo = null;
3236
+ function wrap(text, width) {
3237
+ const lines = [];
3238
+ for (const paragraph of text.split("\n")) {
3239
+ let current = "";
3240
+ for (const word of paragraph.split(/\s+/).filter(Boolean)) {
3241
+ if (current && current.length + word.length + 1 > width) {
3242
+ lines.push(current);
3243
+ current = word;
3244
+ } else {
3245
+ current = current ? `${current} ${word}` : word;
3246
+ }
3247
+ }
3248
+ lines.push(current);
3249
+ }
3250
+ return lines.filter((l) => l.length > 0);
3251
+ }
3252
+ function diagnose(status, host) {
3253
+ const where = host ?? "the AI service";
3254
+ if (status === void 0) return `Could not reach ${where}. Check your network, then retry.`;
3255
+ if (status === 401 || status === 403) return `${where} rejected the credentials. Run qp login and retry.`;
3256
+ if (status === 413) return `The prompt was too large for ${where}. Shorten the script or the page.`;
3257
+ if (status === 429) return `${where} is rate limiting. Wait for the quota to reset, then retry.`;
3258
+ if (status === 502 || status === 503 || status === 504) {
3259
+ return `${where} could not reach the AI backend (upstream outage). Retry in a few minutes.`;
3260
+ }
3261
+ if (status >= 500) return `${where} failed internally. Retry in a few minutes; if it persists, report this.`;
3262
+ return `${where} rejected the request. See the body above.`;
3263
+ }
2617
3264
  function getLastRateLimitInfo() {
2618
3265
  return lastRateLimitInfo;
2619
3266
  }
2620
3267
  function prompt(question) {
2621
- const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
3268
+ const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
2622
3269
  return new Promise((resolve9) => {
2623
3270
  rl.question(question, (answer) => {
2624
3271
  rl.close();
@@ -2661,15 +3308,33 @@ async function setupUserEmail() {
2661
3308
  console.log();
2662
3309
  return email;
2663
3310
  }
2664
- async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
2665
- const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier);
3311
+ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free", step = "plan") {
3312
+ const mcpTool = currentMcpTool();
3313
+ if (mcpTool) {
3314
+ const answered = suppliedAnswer(step);
3315
+ if (typeof answered === "string") return answered;
3316
+ const refusal = new DelegatedGenerationError(mcpTool, step, systemPrompt, userPrompt);
3317
+ recordDelegation(refusal);
3318
+ throw refusal;
3319
+ }
3320
+ const onRetry = ({ attempt, status, delayMs, host }) => {
3321
+ console.log(
3322
+ import_chalk5.default.dim(
3323
+ `
3324
+ ${host} returned ${status}; retrying in ${Math.round(delayMs / 1e3)}s (attempt ${attempt + 1} of 3)`
3325
+ )
3326
+ );
3327
+ };
3328
+ const chatOpts = { onRetry, backoffMs: CLI_BACKOFF_MS };
3329
+ const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier, chatOpts) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier, chatOpts);
2666
3330
  if (!result.success) {
2667
- await logError2("AI API", new Error(result.error));
3331
+ await logError2("AI API", new Error(result.detail ? `${result.error}
3332
+ Body: ${result.detail}` : result.error));
2668
3333
  if (result.isRateLimit) {
2669
3334
  console.log();
2670
3335
  console.log(import_chalk5.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
2671
3336
  console.log();
2672
- console.log(import_chalk5.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
3337
+ console.log(import_chalk5.default.dim(` ${result.error}`));
2673
3338
  console.log(import_chalk5.default.dim(" Options:"));
2674
3339
  if (result.retryAfterSeconds) {
2675
3340
  const minutes = Math.ceil(result.retryAfterSeconds / 60);
@@ -2684,17 +3349,31 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
2684
3349
  }
2685
3350
  if (result.isAuthError) {
2686
3351
  console.log();
2687
- console.log(import_chalk5.default.red(" \u26A0 Authentication Error"));
3352
+ console.log(import_chalk5.default.red(` \u26A0 Authentication Error${result.status ? ` (HTTP ${result.status})` : ""}`));
2688
3353
  console.log();
2689
- console.log(import_chalk5.default.dim(` ${result.error}`));
3354
+ for (const line of wrap(result.error, 76)) {
3355
+ console.log(import_chalk5.default.dim(` ${line}`));
3356
+ }
3357
+ console.log();
3358
+ console.log(import_chalk5.default.dim(` ${diagnose(result.status, result.host)}`));
2690
3359
  console.log();
2691
3360
  process.exit(1);
2692
3361
  }
2693
3362
  console.log();
2694
- console.log(import_chalk5.default.red(" \u26A0 AI Error"));
3363
+ console.log(import_chalk5.default.red(` \u26A0 AI Error${result.status ? ` (HTTP ${result.status})` : ""}`));
3364
+ console.log();
3365
+ for (const line of wrap(result.error, 76)) {
3366
+ console.log(import_chalk5.default.dim(` ${line}`));
3367
+ }
3368
+ if (result.detail && !result.error.includes(result.detail)) {
3369
+ console.log();
3370
+ for (const line of wrap(result.detail, 76).slice(0, 6)) {
3371
+ console.log(import_chalk5.default.dim(` ${line}`));
3372
+ }
3373
+ }
2695
3374
  console.log();
2696
- const firstLine = result.error.split("\n")[0] ?? result.error;
2697
- console.log(import_chalk5.default.dim(` ${firstLine.slice(0, 80)}`));
3375
+ console.log(import_chalk5.default.dim(` ${diagnose(result.status, result.host)}`));
3376
+ console.log(import_chalk5.default.dim(` Full response body: ${logFilePath}`));
2698
3377
  console.log();
2699
3378
  process.exit(1);
2700
3379
  }
@@ -2702,7 +3381,48 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
2702
3381
  return result.content;
2703
3382
  }
2704
3383
 
3384
+ // src/ai/json-plan.ts
3385
+ init_lib();
3386
+ init_utils();
3387
+ var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
3388
+ var PLAN_SHAPE = `The JSON should have: url, title, description, steps array.
3389
+ Each step has: id (number), action, caption, and optionally target, value.`;
3390
+ var jsonFixUserPrompt = (content, shape) => `Fix this malformed JSON. Return ONLY valid JSON, no explanation.
3391
+ ${shape}
3392
+
3393
+ Malformed content:
3394
+ ${content}`;
3395
+ var noopTask2 = () => ({ succeed: () => {
3396
+ }, fail: () => {
3397
+ } });
3398
+ async function parseAIJsonWithRepair(content, opts) {
3399
+ const { logError: logError2, userEmail, tier = "free", shape = PLAN_SHAPE, context = "JSON Fix" } = opts;
3400
+ const direct = parseAIJson(content);
3401
+ if (direct) return direct.value;
3402
+ const makeTask = opts.task ?? noopTask2;
3403
+ const fixTask = makeTask("Fixing malformed JSON");
3404
+ try {
3405
+ const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content, shape), logError2, userEmail, tier);
3406
+ const repaired = parseAIJson(fixed);
3407
+ if (!repaired) throw new Error("No usable JSON in fix");
3408
+ fixTask.succeed("JSON fixed by AI");
3409
+ return repaired.value;
3410
+ } catch (err) {
3411
+ await logError2(context, err);
3412
+ fixTask.fail("Could not fix JSON");
3413
+ throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
3414
+ }
3415
+ }
3416
+
2705
3417
  // src/ai/narrate.ts
3418
+ var SCRIPT_ACTIONS = /* @__PURE__ */ new Set(["click", "type", "scroll", "wait", "select", "check", "navigate"]);
3419
+ var NEEDS_TARGET = /* @__PURE__ */ new Set(["click", "type", "select", "check", "navigate"]);
3420
+ function isUsableStep(step) {
3421
+ if (!step?.action || !SCRIPT_ACTIONS.has(step.action)) return false;
3422
+ if (NEEDS_TARGET.has(step.action) && !step.target) return false;
3423
+ if (step.action === "navigate" && !/^https?:\/\//.test(step.target ?? "")) return false;
3424
+ return true;
3425
+ }
2706
3426
  async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
2707
3427
  const langName = getLanguageName(lang);
2708
3428
  const stepList = steps.map((s, i) => {
@@ -2728,33 +3448,34 @@ Steps to caption:
2728
3448
  ${stepList}
2729
3449
 
2730
3450
  Return exactly ${steps.length} captions as a JSON array.`;
2731
- const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
2732
- const arrayMatch = content.match(/\[[\s\S]*\]/);
2733
- if (!arrayMatch) throw new Error("No JSON array in AI caption response");
2734
- const captions = JSON.parse(arrayMatch[0]);
3451
+ const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "narration");
3452
+ const parsed = parseAIJson(content);
3453
+ if (!parsed || !Array.isArray(parsed.value)) throw new Error("No JSON array in AI caption response");
3454
+ const captions = parsed.value;
2735
3455
  return steps.map((s, i) => ({
2736
3456
  ...s,
2737
3457
  id: i + 1,
2738
3458
  caption: toTitleCase(captions[i] ?? `Step ${i + 1}.`)
2739
3459
  }));
2740
3460
  }
2741
- async function generatePlanFromScript(captions, url, title, pageElementsText, lang, logError2, userEmail, tier = "free") {
3461
+ async function generatePlanFromScript(captions, url, title, pageElementsText, lang, logError2, userEmail, tier = "free", task2) {
2742
3462
  const systemPrompt = `You map narration sentences to web UI actions.
2743
- Given page elements and a script, return a JSON demo plan.
3463
+ Given page elements and a numbered script, return one action per script line.
2744
3464
 
2745
3465
  RULES:
2746
- - Return ONLY valid JSON
2747
- - Preserve captions EXACTLY as given \u2014 never alter the text
3466
+ - Return ONLY valid JSON, no explanation, no captions
3467
+ - Return exactly one entry per script line, in order, with "line" set to that line's number
2748
3468
  - Use page elements to pick realistic actions/targets
2749
- - If a caption doesn't match a specific UI action, use { "action": "wait" }
3469
+ - If a line doesn't match a specific UI action, use { "action": "wait" }
3470
+ - Keep every target and value short
2750
3471
 
2751
3472
  ACTION FORMATS:
2752
- \u2022 click: { "action": "click", "target": "button text" }
2753
- \u2022 type: { "action": "type", "target": "input selector", "value": "text from caption" }
2754
- \u2022 scroll: { "action": "scroll", "value": "up|down" }
2755
- \u2022 wait: { "action": "wait" }
3473
+ \u2022 click: { "line": 1, "action": "click", "target": "button text" }
3474
+ \u2022 type: { "line": 2, "action": "type", "target": "input selector", "value": "text to type" }
3475
+ \u2022 scroll: { "line": 3, "action": "scroll", "value": "up|down" }
3476
+ \u2022 wait: { "line": 4, "action": "wait" }
2756
3477
 
2757
- OUTPUT: full plan JSON with url, title, description, steps[]`;
3478
+ OUTPUT: {"description":"one short sentence","steps":[ ... ]}`;
2758
3479
  const scriptLines = captions.map((c, i) => `${i + 1}. "${c}"`).join("\n");
2759
3480
  const userMessage = `URL: ${url}
2760
3481
  App: ${title}
@@ -2762,22 +3483,46 @@ App: ${title}
2762
3483
  Available page elements:
2763
3484
  ${pageElementsText}
2764
3485
 
2765
- Narration script (map each line to a UI action, preserve caption text exactly):
3486
+ Narration script (map each line to a UI action):
2766
3487
  ${scriptLines}
2767
3488
 
2768
- Return the full plan JSON.`;
2769
- const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
2770
- const jsonMatch = content.match(/\{[\s\S]*\}/);
2771
- if (!jsonMatch) throw new Error("No JSON in AI script response");
2772
- const plan = JSON.parse(jsonMatch[0]);
2773
- plan.url = url;
2774
- if (lang !== void 0) plan.lang = lang;
2775
- plan.steps = plan.steps.map(({ duration: _d, ...s }, i) => ({
2776
- ...s,
2777
- id: i + 1,
2778
- caption: captions[i] ?? s.caption ?? ""
2779
- }));
2780
- return plan;
3489
+ Return JSON with a "description" and a "steps" array of exactly ${captions.length} actions.`;
3490
+ const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "narration");
3491
+ const parsed = await parseAIJsonWithRepair(content, {
3492
+ logError: logError2,
3493
+ task: task2,
3494
+ userEmail,
3495
+ tier,
3496
+ shape: "The JSON should have: description (string), steps (array of {line, action, target, value}).",
3497
+ context: "Script Plan"
3498
+ });
3499
+ const raw = Array.isArray(parsed?.steps) ? parsed.steps : [];
3500
+ const byLine = /* @__PURE__ */ new Map();
3501
+ raw.forEach((step, i) => {
3502
+ if (!step || typeof step !== "object") return;
3503
+ const line = typeof step.line === "number" && step.line >= 1 ? step.line : i + 1;
3504
+ if (!byLine.has(line)) byLine.set(line, step);
3505
+ });
3506
+ const steps = captions.map((caption, i) => {
3507
+ const mapped = byLine.get(i + 1);
3508
+ if (!isUsableStep(mapped)) {
3509
+ return { id: i + 1, action: "wait", caption };
3510
+ }
3511
+ return {
3512
+ id: i + 1,
3513
+ action: mapped.action,
3514
+ caption,
3515
+ ...mapped.target ? { target: mapped.target } : {},
3516
+ ...mapped.value ? { value: mapped.value } : {}
3517
+ };
3518
+ });
3519
+ return {
3520
+ url,
3521
+ title,
3522
+ description: typeof parsed?.description === "string" ? parsed.description : title,
3523
+ ...lang !== void 0 ? { lang } : {},
3524
+ steps
3525
+ };
2781
3526
  }
2782
3527
 
2783
3528
  // src/browser/events-to-plan.ts
@@ -2845,191 +3590,48 @@ function eventsToSteps(events, url) {
2845
3590
  continue;
2846
3591
  }
2847
3592
  if (event.type === "click") {
2848
- const selectorRaw = !isGenericTag(event.selector) ? event.selector : event.target;
2849
- if (isGenericTag(selectorRaw)) continue;
2850
- const selector = selectorRaw;
2851
- const timeDiff = event.timestamp - prevClickTime;
2852
- if (selector === prevClickSelector && timeDiff < DOUBLE_CLICK_THRESHOLD_MS && steps.length > 0) {
2853
- const last = steps[steps.length - 1];
2854
- if (last.action === "click") {
2855
- last.action = "click-2";
2856
- prevClickSelector = void 0;
2857
- continue;
2858
- }
2859
- }
2860
- steps.push({ id: idCounter++, action: "click", target: selector });
2861
- prevClickSelector = selector;
2862
- prevClickTime = event.timestamp;
2863
- continue;
2864
- }
2865
- if (event.type === "scroll") {
2866
- const dir = event.scrollDir ?? "down";
2867
- if (steps.length > 0) {
2868
- const last = steps[steps.length - 1];
2869
- if (last.action === "scroll" && last.value === dir) continue;
2870
- }
2871
- steps.push({ id: idCounter++, action: "scroll", value: dir });
2872
- prevClickSelector = void 0;
2873
- }
2874
- }
2875
- return steps;
2876
- }
2877
- function normalizeUrl2(url) {
2878
- try {
2879
- const u = new URL(url);
2880
- return u.origin + u.pathname.replace(/\/$/, "");
2881
- } catch {
2882
- return url;
2883
- }
2884
- }
2885
-
2886
- // src/capture.ts
2887
- var import_node_child_process5 = require("child_process");
2888
- var import_node_http3 = require("http");
2889
- var import_node_readline4 = require("readline");
2890
- var import_playwright4 = require("playwright");
2891
-
2892
- // src/browser/wsl.ts
2893
- var import_node_child_process4 = require("child_process");
2894
- var import_node_fs5 = require("fs");
2895
- var import_node_http2 = require("http");
2896
- var import_playwright3 = require("playwright");
2897
- function isWSL() {
2898
- if (process.platform !== "linux") return false;
2899
- try {
2900
- const r = (0, import_node_child_process4.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
2901
- return r.includes("microsoft") || r.includes("wsl");
2902
- } catch {
2903
- return false;
2904
- }
2905
- }
2906
- function tryEnableWSLInterop() {
2907
- return (0, import_node_fs5.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
2908
- }
2909
- function wslToWindowsPath(wslPath) {
2910
- if (!wslPath) return wslPath;
2911
- const m = wslPath.match(/^\/mnt\/([a-z])\/(.*)/);
2912
- if (!m) return wslPath;
2913
- return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
2914
- }
2915
- function findExe(whereExeQuery, ...hardcoded) {
2916
- for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
2917
- try {
2918
- const line = (0, import_node_child_process4.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
2919
- if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
2920
- } catch {
2921
- }
2922
- return null;
2923
- }
2924
- var findWindowsChrome = () => findExe(
2925
- "chrome.exe",
2926
- "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
2927
- "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
2928
- );
2929
- var findWindowsNode = () => findExe("node.exe");
2930
- function getWindowsHostIP() {
2931
- try {
2932
- const m = (0, import_node_child_process4.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
2933
- /default via (\d+\.\d+\.\d+\.\d+)/
2934
- );
2935
- if (m?.[1]) return m[1];
2936
- } catch {
2937
- }
2938
- try {
2939
- const m = (0, import_node_fs5.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
2940
- if (m?.[1]) return m[1];
2941
- } catch {
2942
- }
2943
- return "127.0.0.1";
2944
- }
2945
- function startRelay(nodePath, windowsHost, targetPort) {
2946
- const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
2947
- return new Promise((resolve9, reject) => {
2948
- const proc = (0, import_node_child_process4.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
2949
- const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
2950
- let buf = "";
2951
- proc.stdout.on("data", (d) => {
2952
- buf += d.toString();
2953
- const m = buf.match(/PORT:(\d+)/);
2954
- if (m) {
2955
- clearTimeout(timer);
2956
- resolve9({ proc, port: +m[1] });
2957
- }
2958
- });
2959
- proc.on("error", (e) => {
2960
- clearTimeout(timer);
2961
- reject(e);
2962
- });
2963
- });
2964
- }
2965
- function waitForChrome(host, port) {
2966
- return new Promise((resolve9, reject) => {
2967
- const deadline = Date.now() + 2e4;
2968
- const poll = () => {
2969
- if (Date.now() > deadline) {
2970
- reject(new Error("Chrome debug port did not open within 20 s"));
2971
- return;
2972
- }
2973
- const req = (0, import_node_http2.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
2974
- res.resume();
2975
- resolve9();
2976
- });
2977
- req.on("error", () => setTimeout(poll, 600));
2978
- req.setTimeout(1e3, () => {
2979
- req.destroy();
2980
- setTimeout(poll, 600);
2981
- });
2982
- req.end();
2983
- };
2984
- poll();
2985
- });
2986
- }
2987
- async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
2988
- const debugPort = options.debugPort ?? 9222;
2989
- const chromePath = findWindowsChrome();
2990
- if (!chromePath) throw new Error("Windows Chrome not found. Install Google Chrome for Windows.");
2991
- const nodePath = findWindowsNode();
2992
- if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
2993
- const winUserData = wslToWindowsPath(userDataDir);
2994
- const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
2995
- const chromeProc = (0, import_node_child_process4.spawn)(
2996
- chromePath,
2997
- [
2998
- `--remote-debugging-port=${debugPort}`,
2999
- `--user-data-dir=${winUserData}`,
3000
- ...winExtPath ? [`--disable-extensions-except=${winExtPath}`, `--load-extension=${winExtPath}`] : [],
3001
- "--no-first-run",
3002
- "--no-default-browser-check",
3003
- "--disable-background-networking",
3004
- "--disable-sync",
3005
- "--disable-features=TranslateUI",
3006
- options.startMaximized ? "--start-maximized" : "--start-minimized"
3007
- ],
3008
- { stdio: "ignore", detached: true }
3009
- );
3010
- chromeProc.unref();
3011
- const windowsHost = getWindowsHostIP();
3012
- const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
3013
- try {
3014
- await waitForChrome(windowsHost, proxyPort);
3015
- const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
3016
- return {
3017
- browser,
3018
- cleanup: async () => {
3019
- try {
3020
- await browser.close();
3021
- } catch {
3593
+ const selectorRaw = !isGenericTag(event.selector) ? event.selector : event.target;
3594
+ if (isGenericTag(selectorRaw)) continue;
3595
+ const selector = selectorRaw;
3596
+ const timeDiff = event.timestamp - prevClickTime;
3597
+ if (selector === prevClickSelector && timeDiff < DOUBLE_CLICK_THRESHOLD_MS && steps.length > 0) {
3598
+ const last = steps[steps.length - 1];
3599
+ if (last.action === "click") {
3600
+ last.action = "click-2";
3601
+ prevClickSelector = void 0;
3602
+ continue;
3022
3603
  }
3023
- relay.kill();
3024
3604
  }
3025
- };
3026
- } catch (err) {
3027
- relay.kill();
3028
- throw err;
3605
+ steps.push({ id: idCounter++, action: "click", target: selector });
3606
+ prevClickSelector = selector;
3607
+ prevClickTime = event.timestamp;
3608
+ continue;
3609
+ }
3610
+ if (event.type === "scroll") {
3611
+ const dir = event.scrollDir ?? "down";
3612
+ if (steps.length > 0) {
3613
+ const last = steps[steps.length - 1];
3614
+ if (last.action === "scroll" && last.value === dir) continue;
3615
+ }
3616
+ steps.push({ id: idCounter++, action: "scroll", value: dir });
3617
+ prevClickSelector = void 0;
3618
+ }
3619
+ }
3620
+ return steps;
3621
+ }
3622
+ function normalizeUrl2(url) {
3623
+ try {
3624
+ const u = new URL(url);
3625
+ return u.origin + u.pathname.replace(/\/$/, "");
3626
+ } catch {
3627
+ return url;
3029
3628
  }
3030
3629
  }
3031
3630
 
3032
3631
  // src/capture.ts
3632
+ var import_node_child_process5 = require("child_process");
3633
+ var import_node_http3 = require("http");
3634
+ var import_playwright4 = require("playwright");
3033
3635
  var EVENT_TRACKER_SCRIPT = `
3034
3636
  (function () {
3035
3637
  if (window.__qpTracking) return;
@@ -3141,16 +3743,6 @@ var EVENT_TRACKER_SCRIPT = `
3141
3743
  }, { passive: true, capture: true });
3142
3744
  }());
3143
3745
  `;
3144
- function waitForEnter2() {
3145
- return new Promise((resolve9) => {
3146
- const rl = (0, import_node_readline4.createInterface)({ input: process.stdin, output: process.stdout });
3147
- process.stdout.write(" Press Enter when done...\n");
3148
- rl.once("line", () => {
3149
- rl.close();
3150
- resolve9();
3151
- });
3152
- });
3153
- }
3154
3746
  async function launchCaptureChrome() {
3155
3747
  const debugPort = 9222;
3156
3748
  const chromePath = findWindowsChrome();
@@ -3249,7 +3841,7 @@ async function captureUserSession(url, _workDir) {
3249
3841
  return [title, parts.join(" | ")];
3250
3842
  }).catch(() => ["", ""]);
3251
3843
  startTime = Date.now();
3252
- await waitForEnter2();
3844
+ await waitForEnter();
3253
3845
  const durationMs = Date.now() - startTime;
3254
3846
  if (relay) {
3255
3847
  try {
@@ -3264,83 +3856,25 @@ async function captureUserSession(url, _workDir) {
3264
3856
  return { events, durationMs, pageTitle, pageContext };
3265
3857
  }
3266
3858
 
3267
- // src/utils/logging.ts
3268
- var import_promises6 = require("fs/promises");
3269
- var import_node_path9 = require("path");
3270
- var LOG_FILE_NAME = "quickpeek.log";
3271
- var logFileInitialized = false;
3272
- var logFilePath = LOG_FILE_NAME;
3273
- async function writeLogEntry(entry) {
3274
- try {
3275
- if (!logFileInitialized) {
3276
- await (0, import_promises6.writeFile)(logFilePath, entry);
3277
- logFileInitialized = true;
3278
- } else {
3279
- await (0, import_promises6.appendFile)(logFilePath, entry);
3280
- }
3281
- } catch {
3282
- }
3283
- }
3284
- async function setLogFilePath(outputDir) {
3285
- logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
3286
- const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
3287
- ${"=".repeat(60)}
3288
-
3289
- `;
3290
- try {
3291
- await (0, import_promises6.writeFile)(logFilePath, header2);
3292
- logFileInitialized = true;
3293
- } catch {
3294
- }
3295
- }
3296
- async function logInfo(context, message) {
3297
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
3298
- `);
3299
- }
3300
- async function logDebug(context, message) {
3301
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
3302
- `);
3303
- }
3304
- async function logError(context, error) {
3305
- let errorMsg;
3306
- if (error instanceof Error) {
3307
- errorMsg = `${error.message}
3308
- ${error.stack}`;
3309
- } else if (typeof error === "object" && error !== null) {
3310
- errorMsg = JSON.stringify(error, null, 2);
3311
- } else {
3312
- errorMsg = String(error);
3313
- }
3314
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
3315
- ${errorMsg}
3316
-
3317
- `);
3318
- }
3319
- async function logStep(stepNum, action, details) {
3320
- const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
3321
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
3322
- `);
3323
- }
3324
-
3325
3859
  // src/commands/phases/video-phase.ts
3326
- var import_node_fs12 = require("fs");
3327
- var import_promises13 = require("fs/promises");
3860
+ var import_node_fs13 = require("fs");
3861
+ var import_promises14 = require("fs/promises");
3328
3862
  var import_node_path18 = require("path");
3329
3863
  init_lib();
3330
3864
 
3331
3865
  // src/record.ts
3332
- var import_node_fs10 = require("fs");
3333
- var import_promises10 = require("fs/promises");
3866
+ var import_node_fs11 = require("fs");
3867
+ var import_promises11 = require("fs/promises");
3334
3868
  var import_node_path15 = require("path");
3335
3869
  var import_chalk6 = __toESM(require("chalk"));
3336
3870
 
3337
3871
  // src/browser/actions.ts
3338
- var import_node_fs7 = require("fs");
3339
- var import_promises7 = require("fs/promises");
3872
+ var import_node_fs8 = require("fs");
3873
+ var import_promises9 = require("fs/promises");
3340
3874
  var import_node_path11 = require("path");
3341
3875
 
3342
3876
  // src/browser/effects.ts
3343
- var import_node_fs6 = require("fs");
3877
+ var import_node_fs7 = require("fs");
3344
3878
  var import_node_path10 = require("path");
3345
3879
  init_lib();
3346
3880
  function loadHighlightCSS() {
@@ -3351,7 +3885,7 @@ function loadHighlightCSS() {
3351
3885
  // src/browser/ → src/
3352
3886
  ];
3353
3887
  for (const p of candidates) {
3354
- if ((0, import_node_fs6.existsSync)(p)) return (0, import_node_fs6.readFileSync)(p, "utf-8");
3888
+ if ((0, import_node_fs7.existsSync)(p)) return (0, import_node_fs7.readFileSync)(p, "utf-8");
3355
3889
  }
3356
3890
  return "";
3357
3891
  }
@@ -3551,6 +4085,10 @@ async function resolveElement(page, target) {
3551
4085
  await locator.waitFor({ state: "visible", timeout });
3552
4086
  return await locator.elementHandle({ timeout: 1e3 });
3553
4087
  }
4088
+ if (target.startsWith("#") && !/^#[A-Za-z_-][\w-]*$/.test(target)) {
4089
+ const attr = `[id="${target.slice(1).replace(/(["\\])/g, "\\$1")}"]`;
4090
+ return await page.waitForSelector(attr, { state: "visible", timeout });
4091
+ }
3554
4092
  return await page.waitForSelector(target, { state: "visible", timeout });
3555
4093
  } catch {
3556
4094
  return null;
@@ -3700,7 +4238,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3700
4238
  if (!isPlaceholder) {
3701
4239
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
3702
4240
  const filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
3703
- if ((0, import_node_fs7.existsSync)(filePath)) {
4241
+ if ((0, import_node_fs8.existsSync)(filePath)) {
3704
4242
  await fileInput.setInputFiles(filePath);
3705
4243
  await page.waitForTimeout(2e3);
3706
4244
  break;
@@ -3817,7 +4355,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3817
4355
  break;
3818
4356
  case "image": {
3819
4357
  const imagePath = step.value ? (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value) : null;
3820
- if (imagePath && (0, import_node_fs7.existsSync)(imagePath)) {
4358
+ if (imagePath && (0, import_node_fs8.existsSync)(imagePath)) {
3821
4359
  const imgStyle = step.target === "stretch" ? `width: ${config.video.width}px; height: ${config.video.height}px; object-fit: cover;` : "max-width: 90%; max-height: 90%;";
3822
4360
  if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
3823
4361
  }
@@ -3847,7 +4385,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3847
4385
  await pendingDownload;
3848
4386
  pendingDownload = null;
3849
4387
  }
3850
- if (lastDownloadPath && (0, import_node_fs7.existsSync)(lastDownloadPath)) {
4388
+ if (lastDownloadPath && (0, import_node_fs8.existsSync)(lastDownloadPath)) {
3851
4389
  const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
3852
4390
  if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
3853
4391
  }
@@ -3858,7 +4396,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3858
4396
  async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
3859
4397
  const { width, height } = config.video;
3860
4398
  const ext = filePath.split(".").pop()?.toLowerCase() || "";
3861
- const fileBuffer = await (0, import_promises7.readFile)(filePath);
4399
+ const fileBuffer = await (0, import_promises9.readFile)(filePath);
3862
4400
  const base64 = fileBuffer.toString("base64");
3863
4401
  const mimeTypes = {
3864
4402
  png: "image/png",
@@ -3887,64 +4425,6 @@ async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
3887
4425
  return false;
3888
4426
  }
3889
4427
 
3890
- // src/browser/launch.ts
3891
- var import_promises8 = require("fs/promises");
3892
- var import_playwright5 = require("playwright");
3893
- async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
3894
- const extensionPath = config.browser?.extension;
3895
- const viewport = { width: config.video.width, height: config.video.height };
3896
- const videoOpts = recordVideo ? { recordVideo } : {};
3897
- const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
3898
- let context;
3899
- let browser = null;
3900
- let chromeSession = null;
3901
- if (extensionPath) {
3902
- await (0, import_promises8.mkdir)(userDataDir, { recursive: true });
3903
- const persistentArgs = [
3904
- `--disable-extensions-except=${extensionPath}`,
3905
- `--load-extension=${extensionPath}`,
3906
- "--window-position=-32000,-32000",
3907
- "--window-size=1,1"
3908
- ];
3909
- const useWindowsChrome = isWSL() && tryEnableWSLInterop();
3910
- if (useWindowsChrome) {
3911
- try {
3912
- chromeSession = await launchWindowsChrome(extensionPath, userDataDir, { viewport, ...videoOpts });
3913
- const contexts = chromeSession.browser.contexts();
3914
- context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
3915
- } catch {
3916
- context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
3917
- headless: false,
3918
- args: persistentArgs,
3919
- viewport,
3920
- ...videoOpts,
3921
- acceptDownloads: true
3922
- });
3923
- }
3924
- } else {
3925
- context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
3926
- headless: false,
3927
- args: persistentArgs,
3928
- viewport,
3929
- ...videoOpts,
3930
- acceptDownloads: true
3931
- });
3932
- }
3933
- } else {
3934
- browser = await import_playwright5.chromium.launch({ headless: true });
3935
- context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
3936
- }
3937
- return { context, browser, chromeSession };
3938
- }
3939
- async function cleanupBrowserSession(session) {
3940
- if (session.chromeSession) {
3941
- await session.chromeSession.cleanup();
3942
- } else {
3943
- await session.context.close();
3944
- if (session.browser) await session.browser.close();
3945
- }
3946
- }
3947
-
3948
4428
  // src/record.ts
3949
4429
  init_lib();
3950
4430
 
@@ -3953,16 +4433,16 @@ var import_node_path13 = require("path");
3953
4433
  init_utils();
3954
4434
 
3955
4435
  // src/browser/step-validator.ts
3956
- var import_node_fs8 = require("fs");
4436
+ var import_node_fs9 = require("fs");
3957
4437
  var import_node_path12 = require("path");
3958
4438
  init_utils();
3959
4439
  function findMediaFile(filename, baseDir) {
3960
4440
  const name = (0, import_node_path12.basename)(filename);
3961
4441
  const searchDirs = [baseDir, (0, import_node_path12.join)(baseDir, "res"), (0, import_node_path12.join)(baseDir, "assets"), (0, import_node_path12.join)(baseDir, "media")];
3962
4442
  for (const dir of searchDirs) {
3963
- if (!(0, import_node_fs8.existsSync)(dir)) continue;
4443
+ if (!(0, import_node_fs9.existsSync)(dir)) continue;
3964
4444
  try {
3965
- const files = (0, import_node_fs8.readdirSync)(dir);
4445
+ const files = (0, import_node_fs9.readdirSync)(dir);
3966
4446
  if (files.includes(name)) return (0, import_node_path12.join)(dir, name);
3967
4447
  const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
3968
4448
  if (match) return (0, import_node_path12.join)(dir, match);
@@ -4038,7 +4518,7 @@ function resolveMediaStep(step, baseDir, label, startTime) {
4038
4518
  return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
4039
4519
  }
4040
4520
  const specifiedPath = (0, import_node_path12.isAbsolute)(step.value) ? step.value : (0, import_node_path12.resolve)(baseDir, step.value);
4041
- if ((0, import_node_fs8.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
4521
+ if ((0, import_node_fs9.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
4042
4522
  const foundPath = findMediaFile(step.value, baseDir);
4043
4523
  if (foundPath) {
4044
4524
  step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -4160,7 +4640,7 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
4160
4640
  if (!isPlaceholder) {
4161
4641
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
4162
4642
  let filePath = (0, import_node_path12.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path12.resolve)(baseDir, normalizedPath);
4163
- if (!(0, import_node_fs8.existsSync)(filePath)) {
4643
+ if (!(0, import_node_fs9.existsSync)(filePath)) {
4164
4644
  const foundPath = findMediaFile(rawPath, baseDir);
4165
4645
  if (foundPath) {
4166
4646
  const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -4324,8 +4804,8 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
4324
4804
  }
4325
4805
 
4326
4806
  // src/browser/meta.ts
4327
- var import_node_fs9 = require("fs");
4328
- var import_promises9 = require("fs/promises");
4807
+ var import_node_fs10 = require("fs");
4808
+ var import_promises10 = require("fs/promises");
4329
4809
  var import_node_path14 = require("path");
4330
4810
  var RECORDING_FILE = "recording.mp4";
4331
4811
  var RECORDING_META_FILE = "recording.meta.json";
@@ -4334,9 +4814,9 @@ function getRecordingPath(outputDir) {
4334
4814
  }
4335
4815
  async function loadRecordingMeta(outputDir) {
4336
4816
  const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
4337
- if (!(0, import_node_fs9.existsSync)(metaPath)) return null;
4817
+ if (!(0, import_node_fs10.existsSync)(metaPath)) return null;
4338
4818
  try {
4339
- const content = await (0, import_promises9.readFile)(metaPath, "utf-8");
4819
+ const content = await (0, import_promises10.readFile)(metaPath, "utf-8");
4340
4820
  return JSON.parse(content);
4341
4821
  } catch {
4342
4822
  return null;
@@ -4344,7 +4824,7 @@ async function loadRecordingMeta(outputDir) {
4344
4824
  }
4345
4825
  async function saveRecordingMeta(outputDir, meta) {
4346
4826
  const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
4347
- await (0, import_promises9.writeFile)(metaPath, JSON.stringify(meta, null, 2));
4827
+ await (0, import_promises10.writeFile)(metaPath, JSON.stringify(meta, null, 2));
4348
4828
  }
4349
4829
  function canReuseRecording(plan, config, stepDurations, existingMeta) {
4350
4830
  if (!existingMeta) {
@@ -4414,14 +4894,14 @@ function formatStepLabel(step) {
4414
4894
  }
4415
4895
  async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
4416
4896
  const videoDir = (0, import_node_path15.join)(workDir, "video");
4417
- await (0, import_promises10.mkdir)(videoDir, { recursive: true });
4897
+ await (0, import_promises11.mkdir)(videoDir, { recursive: true });
4418
4898
  const getStepDuration = (stepNum) => {
4419
4899
  return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
4420
4900
  };
4421
4901
  const browserTask = task2("Starting browser recording");
4422
4902
  const recordingStartTime = Date.now();
4423
4903
  const userDataDir = (0, import_node_path15.join)(workDir, "browser-data");
4424
- const recordVideo = { dir: videoDir, size: { width: config.video.width, height: config.video.height } };
4904
+ const recordVideo = { dir: videoDir, size: recordingViewport(config) };
4425
4905
  const authState = findAuthState(plan.url);
4426
4906
  const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
4427
4907
  const page = context.pages()[0] || await context.newPage();
@@ -4442,6 +4922,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4442
4922
  let onMediaPage = firstStepIsMedia;
4443
4923
  const failures = [];
4444
4924
  const actualStepDurations = [];
4925
+ let pageClosedEarly = false;
4445
4926
  for (let i = 0; i < plan.steps.length; i++) {
4446
4927
  const step = plan.steps[i];
4447
4928
  const stepNum = i + 1;
@@ -4468,6 +4949,12 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4468
4949
  let targetEl = null;
4469
4950
  if (step.target) {
4470
4951
  targetEl = await resolveElement(page, step.target);
4952
+ if (!targetEl) {
4953
+ console.log(
4954
+ `
4955
+ \u26A0 target "${step.target}" not found on page \u2014 step ${stepNum} recorded without the action`
4956
+ );
4957
+ }
4471
4958
  }
4472
4959
  if (step.zoom && step.zoom !== 1 && targetEl && !isExtensionPage) {
4473
4960
  const preZoomBox = await targetEl.boundingBox() ?? void 0;
@@ -4504,11 +4991,19 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4504
4991
  }
4505
4992
  const stepElapsed = Date.now() - stepStartTime;
4506
4993
  const waitTime = Math.max(0, stepDuration - stepElapsed);
4507
- if (waitTime > 0) await page.waitForTimeout(waitTime);
4508
4994
  const stepDelay = config.video.stepDelay ?? 500;
4509
4995
  const isLastStep = i === plan.steps.length - 1;
4510
- if (!isLastStep && stepDelay > 0) {
4511
- await page.waitForTimeout(stepDelay);
4996
+ try {
4997
+ if (waitTime > 0) await page.waitForTimeout(waitTime);
4998
+ if (!isLastStep && stepDelay > 0) await page.waitForTimeout(stepDelay);
4999
+ } catch (err) {
5000
+ if (page.isClosed()) {
5001
+ console.log(import_chalk6.default.yellow("\n \u26A0 Browser page closed early \u2014 saving the recording captured so far"));
5002
+ pageClosedEarly = true;
5003
+ actualStepDurations.push(Math.max(stepElapsed, stepDuration));
5004
+ break;
5005
+ }
5006
+ throw err;
4512
5007
  }
4513
5008
  if (!isExtensionPage) {
4514
5009
  const shouldHighlight = step.highlight !== void 0 ? step.highlight : config.video.highlight;
@@ -4526,13 +5021,14 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4526
5021
  console.log(import_chalk6.default.yellow(` - ${f}`));
4527
5022
  }
4528
5023
  }
5024
+ while (actualStepDurations.length < plan.steps.length) actualStepDurations.push(0);
4529
5025
  const saveTask = task2("Saving video");
4530
5026
  const video = page.video();
4531
5027
  if (!video) throw new Error("No video recorder available");
4532
- await page.close();
5028
+ if (!pageClosedEarly) await page.close();
4533
5029
  const videoPath = await video.path();
4534
5030
  await cleanupBrowserSession({ context, browser, chromeSession });
4535
- if (!videoPath || !(0, import_node_fs10.existsSync)(videoPath)) {
5031
+ if (!videoPath || !(0, import_node_fs11.existsSync)(videoPath)) {
4536
5032
  throw new Error("Video file was not created");
4537
5033
  }
4538
5034
  saveTask.succeed("Video saved");
@@ -4541,8 +5037,8 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4541
5037
 
4542
5038
  // src/utils/video-pipeline.ts
4543
5039
  var import_video_kit6 = require("@spark-apps/video-kit");
4544
- var import_node_fs11 = require("fs");
4545
- var import_promises11 = require("fs/promises");
5040
+ var import_node_fs12 = require("fs");
5041
+ var import_promises12 = require("fs/promises");
4546
5042
  var import_node_path16 = require("path");
4547
5043
  var import_chalk7 = __toESM(require("chalk"));
4548
5044
  init_lib();
@@ -4593,7 +5089,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
4593
5089
  currentTime += audio.duration;
4594
5090
  }
4595
5091
  const assPath = (0, import_node_path16.join)(workDir, "subtitles.ass");
4596
- await (0, import_promises11.writeFile)(assPath, ass);
5092
+ await (0, import_promises12.writeFile)(assPath, ass);
4597
5093
  return assPath;
4598
5094
  }
4599
5095
  function buildVideoOverlays(steps, stepDurations, baseDir) {
@@ -4607,7 +5103,7 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
4607
5103
  const ext = step.value.split(".").pop()?.toLowerCase() || "";
4608
5104
  if (["mp4", "webm", "mov"].includes(ext)) {
4609
5105
  const mediaPath = (0, import_node_path16.isAbsolute)(step.value) ? step.value : (0, import_node_path16.resolve)(baseDir, step.value);
4610
- if ((0, import_node_fs11.existsSync)(mediaPath)) {
5106
+ if ((0, import_node_fs12.existsSync)(mediaPath)) {
4611
5107
  const partial = parsePartial(step.partial);
4612
5108
  overlays.push({
4613
5109
  path: mediaPath,
@@ -4636,13 +5132,13 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
4636
5132
  process.exit(0);
4637
5133
  }
4638
5134
  const composeTask = task("Building final video");
4639
- await (0, import_promises11.mkdir)(config.output.dir, { recursive: true });
5135
+ await (0, import_promises12.mkdir)(config.output.dir, { recursive: true });
4640
5136
  const suffix = config.lang ? `-${config.lang}` : "";
4641
5137
  const finalPath = (0, import_node_path16.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
4642
5138
  const videoDuration = await getMediaDuration(videoPath) / 1e3;
4643
- const hasVoiceover = audioPath && (0, import_node_fs11.existsSync)(audioPath);
5139
+ const hasVoiceover = audioPath && (0, import_node_fs12.existsSync)(audioPath);
4644
5140
  const musicPath = config.music?.path ? (0, import_node_path16.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path16.resolve)(config.output.dir, config.music.path) : "";
4645
- const hasMusic = musicPath && (0, import_node_fs11.existsSync)(musicPath);
5141
+ const hasMusic = musicPath && (0, import_node_fs12.existsSync)(musicPath);
4646
5142
  try {
4647
5143
  await composeVideo({
4648
5144
  videoPath,
@@ -4707,19 +5203,20 @@ var import_chalk9 = __toESM(require("chalk"));
4707
5203
  init_lib();
4708
5204
 
4709
5205
  // src/plan.ts
4710
- var import_promises12 = require("fs/promises");
5206
+ var import_promises13 = require("fs/promises");
4711
5207
  var import_node_path17 = require("path");
4712
5208
  var import_chalk8 = __toESM(require("chalk"));
4713
- var import_playwright6 = require("playwright");
5209
+ var import_playwright5 = require("playwright");
4714
5210
  init_lib();
5211
+ init_json_repair();
4715
5212
  async function crawlExtension(url, extensionPath, workDir, task2) {
4716
5213
  const browserTask = task2("Launching browser with extension");
4717
5214
  const userDataDir = (0, import_node_path17.join)(workDir, ".browser-data");
4718
- await (0, import_promises12.mkdir)(userDataDir, { recursive: true });
5215
+ await (0, import_promises13.mkdir)(userDataDir, { recursive: true });
4719
5216
  let context;
4720
5217
  let chromeSession = null;
4721
5218
  const useWindowsChrome = isWSL() && tryEnableWSLInterop();
4722
- const launchPersistent = () => import_playwright6.chromium.launchPersistentContext(userDataDir, {
5219
+ const launchPersistent = () => import_playwright5.chromium.launchPersistentContext(userDataDir, {
4723
5220
  headless: false,
4724
5221
  args: [
4725
5222
  `--disable-extensions-except=${extensionPath}`,
@@ -4800,7 +5297,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
4800
5297
  else if (el.type === "select" || el.type === "select-one") action = "select";
4801
5298
  else if (el.type === "checkbox") action = "check";
4802
5299
  else if (["text", "email", "password", "number", "search", "url", "tel"].includes(el.type)) action = "type";
4803
- const selector = el.id ? `#${el.id}` : el.name ? `[name="${el.name}"]` : `text="${el.text}"`;
5300
+ const selector = el.id ? idSelector(el.id) : el.name ? `[name="${el.name}"]` : `text="${el.text}"`;
4804
5301
  return {
4805
5302
  type: el.type === "button" || el.type === "link" ? el.type : "input",
4806
5303
  text: el.text,
@@ -4812,31 +5309,8 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
4812
5309
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
4813
5310
  return { title, elements };
4814
5311
  }
4815
- var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
4816
- var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
4817
- The JSON should have: url, title, description, steps array.
4818
- Each step has: id (number), action, caption, and optionally target, value.
4819
-
4820
- Malformed content:
4821
- ${content}`;
4822
5312
  async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
4823
- try {
4824
- return JSON.parse(content);
4825
- } catch {
4826
- const fixTask = task2("Fixing malformed JSON");
4827
- try {
4828
- const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
4829
- const jsonMatch = fixed.match(/\{[\s\S]*\}/);
4830
- if (!jsonMatch) throw new Error("No JSON in fix");
4831
- const plan = JSON.parse(jsonMatch[0]);
4832
- fixTask.succeed("JSON fixed by AI");
4833
- return plan;
4834
- } catch (err) {
4835
- await logError2("JSON Fix", err);
4836
- fixTask.fail("Could not fix JSON");
4837
- throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
4838
- }
4839
- }
5313
+ return parseAIJsonWithRepair(content, { logError: logError2, task: task2, userEmail, tier });
4840
5314
  }
4841
5315
  async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
4842
5316
  const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
@@ -4845,10 +5319,9 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
4845
5319
  const systemPrompt = buildSystemPrompt(maxSteps);
4846
5320
  const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
4847
5321
  try {
4848
- const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
4849
- const jsonMatch = content.match(/\{[\s\S]*\}/);
4850
- if (!jsonMatch) throw new Error("No JSON found in response");
4851
- const plan = await parsePlanJson(jsonMatch[0], task2, logError2, userEmail, tier);
5322
+ const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier, "plan");
5323
+ const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
5324
+ if (!Array.isArray(plan.steps)) throw new Error("AI response has no steps array");
4852
5325
  if (lang !== void 0) plan.lang = lang;
4853
5326
  plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
4854
5327
  ...s,
@@ -4865,7 +5338,7 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
4865
5338
  }
4866
5339
  }
4867
5340
  async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier = "free") {
4868
- const content = await (0, import_promises12.readFile)(planPath, "utf-8");
5341
+ const content = await (0, import_promises13.readFile)(planPath, "utf-8");
4869
5342
  const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
4870
5343
  if (!plan.url || !plan.steps || !Array.isArray(plan.steps)) {
4871
5344
  throw new Error("Invalid plan: missing url or steps");
@@ -4898,7 +5371,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
4898
5371
  renumberTask.fail("No valid steps");
4899
5372
  throw new Error("Plan has no valid steps");
4900
5373
  }
4901
- await (0, import_promises12.writeFile)(planPath, JSON.stringify(plan, null, 2));
5374
+ await (0, import_promises13.writeFile)(planPath, JSON.stringify(plan, null, 2));
4902
5375
  const removed = originalCount - plan.steps.length;
4903
5376
  renumberTask.succeed(
4904
5377
  removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
@@ -5013,6 +5486,21 @@ function getOutputDirFromUrl(url) {
5013
5486
 
5014
5487
  // src/commands/phases/video-phase.ts
5015
5488
  var INDENT4 = " ";
5489
+ function resolveOutro(config, userTier) {
5490
+ if (userTier !== "free") {
5491
+ const custom = config.video.outro?.trim();
5492
+ if (!custom) return void 0;
5493
+ const path = (0, import_node_path18.resolve)(custom);
5494
+ if ((0, import_node_fs13.existsSync)(path)) return path;
5495
+ info(`${INDENT4}\u26A0 outro not found: ${path}`);
5496
+ return void 0;
5497
+ }
5498
+ const scriptDir = getScriptDir();
5499
+ const bundled = (0, import_node_path18.join)(scriptDir, "assets", "qp.wm");
5500
+ if ((0, import_node_fs13.existsSync)(bundled)) return bundled;
5501
+ const fallback = (0, import_node_path18.join)(scriptDir, "..", "dist", "assets", "qp.wm");
5502
+ return (0, import_node_fs13.existsSync)(fallback) ? fallback : void 0;
5503
+ }
5016
5504
  async function runVideoPhase(params) {
5017
5505
  let { stepAudio } = params;
5018
5506
  const { plan, config, voiceoverPath, workDir, captions, userTier } = params;
@@ -5031,7 +5519,7 @@ async function runVideoPhase(params) {
5031
5519
  mediaDuration = (trimEnd - trimStart2) * 1e3;
5032
5520
  } else {
5033
5521
  const mediaPath = (0, import_node_path18.resolve)(s.value);
5034
- if ((0, import_node_fs12.existsSync)(mediaPath)) {
5522
+ if ((0, import_node_fs13.existsSync)(mediaPath)) {
5035
5523
  try {
5036
5524
  const fullDuration = await getMediaDuration(mediaPath);
5037
5525
  const start = (trimStart2 ?? 0) * 1e3;
@@ -5043,7 +5531,7 @@ async function runVideoPhase(params) {
5043
5531
  }
5044
5532
  } else {
5045
5533
  const mediaPath = (0, import_node_path18.resolve)(s.value);
5046
- if ((0, import_node_fs12.existsSync)(mediaPath)) {
5534
+ if ((0, import_node_fs13.existsSync)(mediaPath)) {
5047
5535
  try {
5048
5536
  mediaDuration = await getMediaDuration(mediaPath);
5049
5537
  } catch {
@@ -5056,7 +5544,7 @@ async function runVideoPhase(params) {
5056
5544
  })
5057
5545
  );
5058
5546
  const recordingPath = getRecordingPath(config.output.dir);
5059
- const hasExistingRecording = existingRecordingMeta && (0, import_node_fs12.existsSync)(recordingPath);
5547
+ const hasExistingRecording = existingRecordingMeta && (0, import_node_fs13.existsSync)(recordingPath);
5060
5548
  let videoPath = recordingPath;
5061
5549
  const firstStep = plan.steps[0];
5062
5550
  const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
@@ -5065,7 +5553,7 @@ async function runVideoPhase(params) {
5065
5553
  header("Record");
5066
5554
  const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
5067
5555
  stepAudio = await alignVoiceoverToRecording(stepAudio, result.actualStepDurations, workDir, config.output.dir);
5068
- await (0, import_promises13.copyFile)(result.videoPath, recordingPath);
5556
+ await (0, import_promises14.copyFile)(result.videoPath, recordingPath);
5069
5557
  const meta = buildRecordingMeta(plan, config, result.actualStepDurations);
5070
5558
  meta.pageLoadTime = result.pageLoadTime;
5071
5559
  await saveRecordingMeta(config.output.dir, meta);
@@ -5117,7 +5605,7 @@ async function runVideoPhase(params) {
5117
5605
  const transitionTask = task("Applying transitions");
5118
5606
  try {
5119
5607
  const transitionedPath = (0, import_node_path18.join)(workDir, "transitioned.mp4");
5120
- const videoDuration = alignedStepDurations.reduce((a, b) => a + b, 0) / 1e3;
5608
+ const videoDuration = await getMediaDuration(videoPath) / 1e3;
5121
5609
  await applyTransitions({
5122
5610
  inputVideo: videoPath,
5123
5611
  outputVideo: transitionedPath,
@@ -5135,30 +5623,24 @@ async function runVideoPhase(params) {
5135
5623
  const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
5136
5624
  let videoToCompose = videoPath;
5137
5625
  let mainContentDuration;
5138
- if (userTier === "free") {
5139
- const scriptDir = getScriptDir();
5140
- let outroPath = (0, import_node_path18.join)(scriptDir, "assets", "qp.wm");
5141
- if (!(0, import_node_fs12.existsSync)(outroPath)) {
5142
- outroPath = (0, import_node_path18.join)(scriptDir, "..", "dist", "assets", "qp.wm");
5143
- }
5144
- if ((0, import_node_fs12.existsSync)(outroPath)) {
5145
- try {
5146
- const mainDuration = await getMediaDuration(videoPath);
5147
- mainContentDuration = mainDuration / 1e3 - trimStart;
5148
- const combinedPath = (0, import_node_path18.join)(workDir, "combined-with-outro.mp4");
5149
- await concatVideoWithOutro({
5150
- inputPath: videoPath,
5151
- outroPath,
5152
- outputPath: combinedPath,
5153
- width: config.video.width,
5154
- height: config.video.height,
5155
- preset: config.video.preset
5156
- });
5157
- videoToCompose = combinedPath;
5158
- } catch (err) {
5159
- await logError("Concat Outro", err);
5160
- mainContentDuration = void 0;
5161
- }
5626
+ const outroPath = resolveOutro(config, userTier);
5627
+ if (outroPath) {
5628
+ try {
5629
+ const mainDuration = await getMediaDuration(videoPath);
5630
+ mainContentDuration = mainDuration / 1e3 - trimStart;
5631
+ const combinedPath = (0, import_node_path18.join)(workDir, "combined-with-outro.mp4");
5632
+ await concatVideoWithOutro({
5633
+ inputPath: videoPath,
5634
+ outroPath,
5635
+ outputPath: combinedPath,
5636
+ width: config.video.width,
5637
+ height: config.video.height,
5638
+ preset: config.video.preset
5639
+ });
5640
+ videoToCompose = combinedPath;
5641
+ } catch (err) {
5642
+ await logError("Concat Outro", err);
5643
+ mainContentDuration = void 0;
5162
5644
  }
5163
5645
  }
5164
5646
  let finalPath = await composeAndFinish(
@@ -5192,7 +5674,7 @@ async function runVideoPhase(params) {
5192
5674
  }
5193
5675
 
5194
5676
  // src/commands/phases/voiceover-phase.ts
5195
- var import_node_fs13 = require("fs");
5677
+ var import_node_fs14 = require("fs");
5196
5678
  var import_node_path19 = require("path");
5197
5679
  var import_chalk10 = __toESM(require("chalk"));
5198
5680
  init_lib();
@@ -5202,9 +5684,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5202
5684
  const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
5203
5685
  for (const file of filesToDelete) {
5204
5686
  const filePath = (0, import_node_path19.join)(config.output.dir, file);
5205
- if ((0, import_node_fs13.existsSync)(filePath)) {
5206
- const { unlink: unlink3 } = await import("fs/promises");
5207
- await unlink3(filePath).catch(() => {
5687
+ if ((0, import_node_fs14.existsSync)(filePath)) {
5688
+ const { unlink: unlink4 } = await import("fs/promises");
5689
+ await unlink4(filePath).catch(() => {
5208
5690
  });
5209
5691
  }
5210
5692
  }
@@ -5213,7 +5695,12 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5213
5695
  const voice = resolveVoice(config);
5214
5696
  const existingMeta = await loadVoiceoverMeta(config.output.dir);
5215
5697
  const voiceoverFile = (0, import_node_path19.join)(config.output.dir, "voiceover.wav");
5216
- const hasExistingVoiceover = existingMeta && (0, import_node_fs13.existsSync)(voiceoverFile);
5698
+ const voiceoverOnDisk = existingMeta && (0, import_node_fs14.existsSync)(voiceoverFile);
5699
+ const staleContainer = voiceoverOnDisk ? !await isHonestWav(voiceoverFile) : false;
5700
+ if (staleContainer) {
5701
+ info("Existing voiceover has a stale container, regenerating");
5702
+ }
5703
+ const hasExistingVoiceover = voiceoverOnDisk && !staleContainer;
5217
5704
  const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
5218
5705
  let voiceoverPath;
5219
5706
  let stepAudio;
@@ -5303,7 +5790,8 @@ async function captureCommand(url, lang) {
5303
5790
  lang,
5304
5791
  logError,
5305
5792
  userData.email,
5306
- captureResult.pageContext
5793
+ captureResult.pageContext,
5794
+ userData.tier ?? "free"
5307
5795
  );
5308
5796
  narrateTask.succeed(`${steps.length} captions generated`);
5309
5797
  } catch (err) {
@@ -5348,7 +5836,7 @@ async function captureCommand(url, lang) {
5348
5836
  }
5349
5837
 
5350
5838
  // src/commands/edit-cmd.ts
5351
- var import_node_fs14 = require("fs");
5839
+ var import_node_fs15 = require("fs");
5352
5840
  var import_node_path21 = require("path");
5353
5841
  var import_chalk12 = __toESM(require("chalk"));
5354
5842
 
@@ -5463,10 +5951,10 @@ var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
5463
5951
  function findPlanFile(dir, lang) {
5464
5952
  if (lang) {
5465
5953
  const exact = (0, import_node_path21.join)(dir, `plan-${lang}.json`);
5466
- return (0, import_node_fs14.existsSync)(exact) ? exact : null;
5954
+ return (0, import_node_fs15.existsSync)(exact) ? exact : null;
5467
5955
  }
5468
- if (!(0, import_node_fs14.existsSync)(dir)) return null;
5469
- const plans = (0, import_node_fs14.readdirSync)(dir).filter((f) => PLAN_FILE.test(f));
5956
+ if (!(0, import_node_fs15.existsSync)(dir)) return null;
5957
+ const plans = (0, import_node_fs15.readdirSync)(dir).filter((f) => PLAN_FILE.test(f));
5470
5958
  const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
5471
5959
  return preferred ? (0, import_node_path21.join)(dir, preferred) : null;
5472
5960
  }
@@ -5481,7 +5969,7 @@ async function editCommand(url, lang) {
5481
5969
  console.log();
5482
5970
  return;
5483
5971
  }
5484
- const parsed = parsePlanText((0, import_node_fs14.readFileSync)(planPath, "utf8"));
5972
+ const parsed = parsePlanText((0, import_node_fs15.readFileSync)(planPath, "utf8"));
5485
5973
  if (!parsed.ok) {
5486
5974
  console.log(import_chalk12.default.red(` ${planPath} cannot be opened: ${parsed.error}`));
5487
5975
  console.log();
@@ -5504,14 +5992,14 @@ async function editCommand(url, lang) {
5504
5992
  }
5505
5993
 
5506
5994
  // src/commands/init-cmd.ts
5507
- var import_node_fs15 = require("fs");
5508
- var import_promises14 = require("fs/promises");
5995
+ var import_node_fs16 = require("fs");
5996
+ var import_promises15 = require("fs/promises");
5509
5997
  var import_node_path22 = require("path");
5510
5998
  var import_chalk13 = __toESM(require("chalk"));
5511
5999
  init_lib();
5512
6000
  async function initCommand() {
5513
6001
  const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
5514
- if ((0, import_node_fs15.existsSync)(configPath)) {
6002
+ if ((0, import_node_fs16.existsSync)(configPath)) {
5515
6003
  console.log(import_chalk13.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
5516
6004
  return;
5517
6005
  }
@@ -5535,7 +6023,7 @@ async function initCommand() {
5535
6023
  volume: 0.3
5536
6024
  }
5537
6025
  };
5538
- await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
6026
+ await (0, import_promises15.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
5539
6027
  `);
5540
6028
  console.log(import_chalk13.default.green(` \u2714 Created ${CONFIG_FILE}`));
5541
6029
  console.log(import_chalk13.default.dim(` Edit it or use: qp config <key> <value>`));
@@ -5543,11 +6031,11 @@ async function initCommand() {
5543
6031
  async function configCommand(key, value) {
5544
6032
  const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
5545
6033
  if (!key) {
5546
- if (!(0, import_node_fs15.existsSync)(configPath)) {
6034
+ if (!(0, import_node_fs16.existsSync)(configPath)) {
5547
6035
  console.log(import_chalk13.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
5548
6036
  return;
5549
6037
  }
5550
- const content = await (0, import_promises14.readFile)(configPath, "utf-8");
6038
+ const content = await (0, import_promises15.readFile)(configPath, "utf-8");
5551
6039
  console.log(import_chalk13.default.cyan(` ${CONFIG_FILE}:`));
5552
6040
  console.log(content);
5553
6041
  return;
@@ -5558,9 +6046,9 @@ async function configCommand(key, value) {
5558
6046
  return;
5559
6047
  }
5560
6048
  let config = {};
5561
- if ((0, import_node_fs15.existsSync)(configPath)) {
6049
+ if ((0, import_node_fs16.existsSync)(configPath)) {
5562
6050
  try {
5563
- config = JSON.parse(await (0, import_promises14.readFile)(configPath, "utf-8"));
6051
+ config = JSON.parse(await (0, import_promises15.readFile)(configPath, "utf-8"));
5564
6052
  } catch {
5565
6053
  }
5566
6054
  }
@@ -5582,7 +6070,7 @@ async function configCommand(key, value) {
5582
6070
  }
5583
6071
  obj[keys[keys.length - 1]] = parsedValue;
5584
6072
  }
5585
- await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
6073
+ await (0, import_promises15.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
5586
6074
  `);
5587
6075
  console.log(import_chalk13.default.green(` \u2714 Set ${key} = ${value}`));
5588
6076
  }
@@ -5648,8 +6136,8 @@ async function logoutCommand() {
5648
6136
  }
5649
6137
 
5650
6138
  // src/commands/record-cmd.ts
5651
- var import_node_fs18 = require("fs");
5652
- var import_promises17 = require("fs/promises");
6139
+ var import_node_fs19 = require("fs");
6140
+ var import_promises18 = require("fs/promises");
5653
6141
  var import_node_path25 = require("path");
5654
6142
  var import_chalk19 = __toESM(require("chalk"));
5655
6143
  init_lib();
@@ -5824,18 +6312,18 @@ async function runAuthPhase(input) {
5824
6312
  }
5825
6313
 
5826
6314
  // src/commands/phases/plan-phase.ts
5827
- var import_node_fs16 = require("fs");
5828
- var import_promises15 = require("fs/promises");
6315
+ var import_node_fs17 = require("fs");
6316
+ var import_promises16 = require("fs/promises");
5829
6317
  var import_node_path23 = require("path");
5830
6318
  var import_chalk17 = __toESM(require("chalk"));
5831
6319
 
5832
6320
  // src/ai/fix.ts
5833
6321
  init_lib();
5834
6322
  async function callGroqAndFinalize(systemPrompt, userMessage, plan, logError2, userEmail, tier = "free") {
5835
- const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
5836
- const jsonMatch = content.match(/\{[\s\S]*\}/);
5837
- if (!jsonMatch) throw new Error("No JSON in AI response");
5838
- return finalizePlan(JSON.parse(jsonMatch[0]), plan);
6323
+ const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "fix");
6324
+ const result = await parseAIJsonWithRepair(content, { logError: logError2, userEmail, tier, context: "Plan Fix" });
6325
+ if (!Array.isArray(result.steps)) throw new Error("AI response has no steps array");
6326
+ return finalizePlan(result, plan);
5839
6327
  }
5840
6328
  function finalizePlan(result, original) {
5841
6329
  result.url = original.url;
@@ -5959,10 +6447,15 @@ async function translatePlan(plan, targetLang, logError2, userEmail, tier = "fre
5959
6447
  ${JSON.stringify(toTranslate)}
5960
6448
 
5961
6449
  Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
5962
- const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
5963
- const jsonMatch = content.match(/\{[\s\S]*\}/);
5964
- if (!jsonMatch) throw new Error("No JSON in translation response");
5965
- const translated = JSON.parse(jsonMatch[0]);
6450
+ const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "fix");
6451
+ const translated = await parseAIJsonWithRepair(content, {
6452
+ logError: logError2,
6453
+ userEmail,
6454
+ tier,
6455
+ shape: "The JSON should have: description (string), captions (array of strings).",
6456
+ context: "Translate Plan"
6457
+ });
6458
+ if (!Array.isArray(translated.captions)) throw new Error("Translation response has no captions array");
5966
6459
  const translatedPlan = JSON.parse(JSON.stringify(plan));
5967
6460
  translatedPlan.title = plan.title;
5968
6461
  translatedPlan.description = translated.description || plan.description;
@@ -5994,9 +6487,9 @@ async function runPlanPhase(params) {
5994
6487
  const extensionPath = config.browser?.extension;
5995
6488
  let existingPlan = null;
5996
6489
  let existingPlanPath = null;
5997
- if ((0, import_node_fs16.existsSync)(planPath)) {
6490
+ if ((0, import_node_fs17.existsSync)(planPath)) {
5998
6491
  try {
5999
- const parsed = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
6492
+ const parsed = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
6000
6493
  if (parsed.url && parsed.steps?.length > 0) {
6001
6494
  existingPlan = parsed;
6002
6495
  existingPlanPath = planPath;
@@ -6006,12 +6499,12 @@ async function runPlanPhase(params) {
6006
6499
  }
6007
6500
  if (!existingPlan) {
6008
6501
  try {
6009
- const files = (0, import_node_fs16.readdirSync)(config.output.dir);
6502
+ const files = (0, import_node_fs17.readdirSync)(config.output.dir);
6010
6503
  const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path23.basename)(planPath));
6011
6504
  for (const file of planFiles) {
6012
6505
  const filePath = (0, import_node_path23.join)(config.output.dir, file);
6013
6506
  try {
6014
- const parsed = JSON.parse(await (0, import_promises15.readFile)(filePath, "utf-8"));
6507
+ const parsed = JSON.parse(await (0, import_promises16.readFile)(filePath, "utf-8"));
6015
6508
  if (parsed.url && parsed.steps?.length > 0) {
6016
6509
  existingPlan = parsed;
6017
6510
  existingPlanPath = filePath;
@@ -6023,9 +6516,9 @@ async function runPlanPhase(params) {
6023
6516
  } catch {
6024
6517
  }
6025
6518
  }
6026
- if (!existingPlan && (0, import_node_fs16.existsSync)(legacyPlanPath)) {
6519
+ if (!existingPlan && (0, import_node_fs17.existsSync)(legacyPlanPath)) {
6027
6520
  try {
6028
- const parsed = JSON.parse(await (0, import_promises15.readFile)(legacyPlanPath, "utf-8"));
6521
+ const parsed = JSON.parse(await (0, import_promises16.readFile)(legacyPlanPath, "utf-8"));
6029
6522
  if (parsed.url && parsed.steps?.length > 0) {
6030
6523
  existingPlan = parsed;
6031
6524
  existingPlanPath = legacyPlanPath;
@@ -6035,6 +6528,11 @@ async function runPlanPhase(params) {
6035
6528
  }
6036
6529
  const normalizeUrl4 = (u) => u.replace(/\/+$/, "").toLowerCase();
6037
6530
  const urlMatches = existingPlan && normalizeUrl4(existingPlan.url) === normalizeUrl4(url);
6531
+ if (existingPlan && !urlMatches && isAutoYes()) {
6532
+ console.log();
6533
+ console.log(import_chalk17.default.dim(` Existing plan is for ${existingPlan.url}, not ${url} \u2014 regenerating.`));
6534
+ existingPlan = null;
6535
+ }
6038
6536
  let plan;
6039
6537
  const doTranslate = async (failMsg) => {
6040
6538
  const translateTask = task(`Translating to ${targetLang}`);
@@ -6168,9 +6666,9 @@ async function runPlanPhase(params) {
6168
6666
  const planLang = plan.lang || "en";
6169
6667
  if (planLang !== targetLang) {
6170
6668
  let useExistingTranslation = false;
6171
- if ((0, import_node_fs16.existsSync)(planPath) && existingPlanPath !== planPath) {
6669
+ if ((0, import_node_fs17.existsSync)(planPath) && existingPlanPath !== planPath) {
6172
6670
  try {
6173
- const translatedPlan = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
6671
+ const translatedPlan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
6174
6672
  if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
6175
6673
  console.log();
6176
6674
  console.log(import_chalk17.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
@@ -6194,7 +6692,7 @@ async function runPlanPhase(params) {
6194
6692
  }
6195
6693
 
6196
6694
  // src/commands/phases/validation-phase.ts
6197
- var import_promises16 = require("fs/promises");
6695
+ var import_promises17 = require("fs/promises");
6198
6696
  var import_chalk18 = __toESM(require("chalk"));
6199
6697
  async function runValidationPhase(params) {
6200
6698
  let { plan } = params;
@@ -6321,7 +6819,7 @@ async function runValidationPhase(params) {
6321
6819
  console.log(import_chalk18.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
6322
6820
  continue;
6323
6821
  }
6324
- plan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
6822
+ plan = JSON.parse(await (0, import_promises17.readFile)(planPath, "utf-8"));
6325
6823
  const finalRevalidateTask = task(`Final validation of step ${stepNum}`);
6326
6824
  const finalRevalidation = await validateStepsOnPage(plan, config, logError, config.output.dir);
6327
6825
  const finalResult = finalRevalidation.results[result.stepIndex];
@@ -6345,7 +6843,7 @@ async function runValidationPhase(params) {
6345
6843
  }
6346
6844
 
6347
6845
  // src/commands/utils/chrome-extension.ts
6348
- var import_node_fs17 = require("fs");
6846
+ var import_node_fs18 = require("fs");
6349
6847
  var import_node_os3 = require("os");
6350
6848
  var import_node_path24 = require("path");
6351
6849
  function resolveChromeExtension(extensionId) {
@@ -6354,7 +6852,7 @@ function resolveChromeExtension(extensionId) {
6354
6852
  const getChromeExtDirs = (userDataPath) => {
6355
6853
  const dirs = [];
6356
6854
  try {
6357
- const profiles = (0, import_node_fs17.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
6855
+ const profiles = (0, import_node_fs18.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
6358
6856
  for (const profile of profiles) {
6359
6857
  dirs.push((0, import_node_path24.join)(userDataPath, profile, "Extensions"));
6360
6858
  }
@@ -6369,7 +6867,7 @@ function resolveChromeExtension(extensionId) {
6369
6867
  possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
6370
6868
  } else {
6371
6869
  try {
6372
- const windowsUsers = (0, import_node_fs17.readdirSync)("/mnt/c/Users").filter(
6870
+ const windowsUsers = (0, import_node_fs18.readdirSync)("/mnt/c/Users").filter(
6373
6871
  (u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
6374
6872
  );
6375
6873
  for (const user of windowsUsers) {
@@ -6381,18 +6879,18 @@ function resolveChromeExtension(extensionId) {
6381
6879
  }
6382
6880
  for (const chromeExtDir of possibleDirs) {
6383
6881
  const extDir = (0, import_node_path24.join)(chromeExtDir, extensionId);
6384
- if (!(0, import_node_fs17.existsSync)(extDir)) continue;
6882
+ if (!(0, import_node_fs18.existsSync)(extDir)) continue;
6385
6883
  try {
6386
- const versions = (0, import_node_fs17.readdirSync)(extDir).filter((f) => !f.startsWith("."));
6884
+ const versions = (0, import_node_fs18.readdirSync)(extDir).filter((f) => !f.startsWith("."));
6387
6885
  if (versions.length === 0) continue;
6388
6886
  const latestVersion = versions.sort().pop();
6389
6887
  const extPath = (0, import_node_path24.join)(extDir, latestVersion);
6390
6888
  let name = extensionId;
6391
6889
  let popupPath = "popup.html";
6392
6890
  const manifestPath = (0, import_node_path24.join)(extPath, "manifest.json");
6393
- if ((0, import_node_fs17.existsSync)(manifestPath)) {
6891
+ if ((0, import_node_fs18.existsSync)(manifestPath)) {
6394
6892
  try {
6395
- const manifest = JSON.parse((0, import_node_fs17.readFileSync)(manifestPath, "utf-8"));
6893
+ const manifest = JSON.parse((0, import_node_fs18.readFileSync)(manifestPath, "utf-8"));
6396
6894
  if (manifest.name && !manifest.name.startsWith("__MSG_")) {
6397
6895
  name = manifest.name;
6398
6896
  }
@@ -6434,6 +6932,13 @@ async function recordCommand(url, options = {}) {
6434
6932
  console.log(import_chalk19.default.dim(` Extension: ${chromeExtension.name}`));
6435
6933
  console.log(import_chalk19.default.dim(` Path: ${chromeExtension.path}`));
6436
6934
  }
6935
+ if (!chromeExtension) {
6936
+ const startUrl = resolveStartUrl(url);
6937
+ if (startUrl !== url) {
6938
+ console.log(import_chalk19.default.dim(` Signed-in start page: ${startUrl}`));
6939
+ url = startUrl;
6940
+ }
6941
+ }
6437
6942
  config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
6438
6943
  const userData = await loadUserData();
6439
6944
  const authResult = await runAuthPhase({
@@ -6467,7 +6972,7 @@ async function recordCommand(url, options = {}) {
6467
6972
  if (langOverride) config.lang = langOverride;
6468
6973
  if (config.music?.path) {
6469
6974
  const musicPath = (0, import_node_path25.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path25.resolve)(config.output.dir, config.music.path);
6470
- if (!(0, import_node_fs18.existsSync)(musicPath)) {
6975
+ if (!(0, import_node_fs19.existsSync)(musicPath)) {
6471
6976
  console.log();
6472
6977
  console.log(import_chalk19.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
6473
6978
  console.log(import_chalk19.default.dim(` Config: ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}`));
@@ -6489,7 +6994,7 @@ async function recordCommand(url, options = {}) {
6489
6994
  const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${targetLang}.json`);
6490
6995
  const legacyPlanPath = (0, import_node_path25.join)(config.output.dir, "plan.json");
6491
6996
  const isExtensionUrl = url.startsWith("chrome-extension://");
6492
- await (0, import_promises17.mkdir)(config.output.dir, { recursive: true });
6997
+ await (0, import_promises18.mkdir)(config.output.dir, { recursive: true });
6493
6998
  await setLogFilePath(config.output.dir);
6494
6999
  try {
6495
7000
  const plan = await runPlanPhase({
@@ -6546,31 +7051,67 @@ async function recordCommand(url, options = {}) {
6546
7051
  }
6547
7052
 
6548
7053
  // src/commands/script-cmd.ts
6549
- var import_promises18 = require("fs/promises");
7054
+ var import_promises19 = require("fs/promises");
6550
7055
  var import_node_path26 = require("path");
6551
7056
  var import_chalk20 = __toESM(require("chalk"));
6552
7057
  init_lib();
7058
+ async function reusablePlan(planPath, lines) {
7059
+ try {
7060
+ const plan = JSON.parse(await (0, import_promises19.readFile)(planPath, "utf-8"));
7061
+ if (!Array.isArray(plan.steps)) return null;
7062
+ if (plan.steps.length < lines.length) return null;
7063
+ const matches = lines.every((line, i) => plan.steps[i]?.caption === line);
7064
+ return matches ? plan : null;
7065
+ } catch {
7066
+ return null;
7067
+ }
7068
+ }
6553
7069
  async function scriptCommand(url, scriptPath, lang) {
7070
+ url = resolveStartUrl(url);
6554
7071
  const { config, workDir, captions } = await initCommandEnv(url, lang);
7072
+ const userData = await loadUserData();
7073
+ const userTier = userData.email ? await getTierByEmail(userData.email) : "free";
6555
7074
  header("Script");
6556
- const raw = await (0, import_promises18.readFile)(scriptPath, "utf-8");
7075
+ const raw = await (0, import_promises19.readFile)(scriptPath, "utf-8");
6557
7076
  const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
6558
7077
  if (lines.length === 0) {
6559
7078
  console.log(import_chalk20.default.red(" Script file is empty or has no non-comment lines"));
6560
7079
  process.exit(1);
6561
7080
  }
6562
7081
  info(`${lines.length} captions from script`);
7082
+ const maxSteps = config.plan?.maxSteps ?? 8;
7083
+ if (lines.length > maxSteps) {
7084
+ console.log(
7085
+ import_chalk20.default.yellow(` \u26A0 ${lines.length} script lines exceeds plan.maxSteps (${maxSteps}) \u2014 recording all of them.`)
7086
+ );
7087
+ console.log(import_chalk20.default.dim(` Raise plan.maxSteps in quickpeek.config.json to silence this, or split the script.`));
7088
+ }
6563
7089
  header("Analyze");
6564
7090
  const crawlTask = task("Crawling page for UI elements");
6565
- const { elements } = await crawlPage(url, () => crawlTask);
7091
+ const { elements } = await crawlPage(url, () => crawlTask, findAuthState(url));
6566
7092
  const elementsText = elements.slice(0, 30).map((e) => ` ${e.action}: ${e.text} (${e.selector})`).join("\n");
6567
7093
  header("Plan");
6568
7094
  const planTask = task("Mapping script to UI actions");
6569
7095
  const appTitle = new URL(url).hostname.replace(/^www\./, "");
6570
- const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
6571
7096
  const planPath = (0, import_node_path26.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
6572
- await savePlan(plan, planPath);
6573
- planTask.succeed(`${plan.steps.length} steps planned`);
7097
+ let plan = await reusablePlan(planPath, lines);
7098
+ if (plan) {
7099
+ planTask.succeed(`${plan.steps.length} steps reused from existing plan`);
7100
+ } else {
7101
+ plan = await generatePlanFromScript(
7102
+ lines,
7103
+ url,
7104
+ appTitle,
7105
+ elementsText,
7106
+ lang,
7107
+ logError,
7108
+ userData.email,
7109
+ userTier,
7110
+ () => planTask
7111
+ );
7112
+ await savePlan(plan, planPath);
7113
+ planTask.succeed(`${plan.steps.length} steps planned`);
7114
+ }
6574
7115
  header("Voiceover");
6575
7116
  const { voiceoverPath, stepAudio } = await generateVoiceover(
6576
7117
  plan.steps,
@@ -6586,7 +7127,7 @@ async function scriptCommand(url, scriptPath, lang) {
6586
7127
  const { videoPath } = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
6587
7128
  header("Compose");
6588
7129
  const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
6589
- const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
7130
+ const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, userTier);
6590
7131
  console.log();
6591
7132
  console.log(import_chalk20.default.bold.green(" Done!"));
6592
7133
  console.log(import_chalk20.default.cyan(` ${finalPath}`));
@@ -6696,7 +7237,9 @@ function parseArgs(args) {
6696
7237
  let fresh = false;
6697
7238
  for (let i = 0; i < args.length; i++) {
6698
7239
  const arg2 = args[i];
6699
- if (arg2.startsWith("lang=")) {
7240
+ if (!url && (arg2.startsWith("http://") || arg2.startsWith("https://"))) {
7241
+ url = normalizeUrl3(arg2);
7242
+ } else if (arg2.startsWith("lang=")) {
6700
7243
  lang = arg2.slice(5);
6701
7244
  } else if (arg2 === "--script" || arg2 === "-s") {
6702
7245
  scriptPath = args[i + 1];