@spark-apps/quickpeek 1.2.0 → 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
@@ -59,7 +59,7 @@ function getErrorMessage(err) {
59
59
  return err instanceof Error ? err.message : String(err);
60
60
  }
61
61
  function toTitleCase(text) {
62
- return text.replace(/\w+/g, (word, index) => {
62
+ return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
63
63
  if (index === 0) {
64
64
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
65
65
  }
@@ -125,6 +125,23 @@ var init_utils = __esm({
125
125
  });
126
126
 
127
127
  // src/lib/api-client.ts
128
+ function apiKey() {
129
+ return process.env.SPARK_AI_KEY || "";
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
+ }
128
145
  async function getOrRefreshDirectToken() {
129
146
  const now = Date.now();
130
147
  if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
@@ -133,7 +150,7 @@ async function getOrRefreshDirectToken() {
133
150
  try {
134
151
  const response = await fetch(SPARK_AI_TOKEN, {
135
152
  method: "POST",
136
- headers: { "X-Api-Key": API_KEY }
153
+ headers: { "X-Api-Key": apiKey() }
137
154
  });
138
155
  if (!response.ok) {
139
156
  throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
@@ -176,8 +193,10 @@ async function getOrRefreshRelayToken(email) {
176
193
  throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
177
194
  }
178
195
  }
179
- async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
180
- 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);
181
200
  for (let attempt = 1; attempt <= 3; attempt++) {
182
201
  try {
183
202
  const response = await fetch(chatUrl, {
@@ -193,36 +212,53 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
193
212
  ]
194
213
  })
195
214
  });
215
+ const bodyText = await response.text().catch(() => "");
196
216
  let data = {};
197
- const contentType = response.headers.get("content-type") ?? "";
198
- if (contentType.includes("application/json")) {
199
- data = await response.json();
217
+ try {
218
+ data = bodyText ? JSON.parse(bodyText) : {};
219
+ } catch {
200
220
  }
201
221
  if (!response.ok) {
202
- const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
203
- if (response.status === 429 && attempt < 3) {
204
- const retryAfter = response.headers.get("Retry-After");
205
- const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
206
- 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 = {
207
231
  success: false,
208
232
  error,
209
- isRateLimit: true,
210
- ...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) } : {}
211
238
  };
212
239
  await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
213
240
  continue;
214
241
  }
242
+ const retryAfter = response.headers.get("Retry-After");
243
+ const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
215
244
  return {
216
245
  success: false,
217
246
  error,
218
247
  isRateLimit: response.status === 429,
219
- 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 } : {}
220
253
  };
221
254
  }
222
255
  if (!data.success || !data.response) {
223
256
  return {
224
257
  success: false,
225
- 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) } : {}
226
262
  };
227
263
  }
228
264
  return {
@@ -236,15 +272,17 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
236
272
  }
237
273
  };
238
274
  } catch (err) {
239
- if (attempt === 3) throw err;
275
+ if (attempt === 3) {
276
+ return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
277
+ }
240
278
  const delayMs = 1e3 * 2 ** (attempt - 1);
241
279
  await new Promise((resolve9) => setTimeout(resolve9, delayMs));
242
280
  }
243
281
  }
244
- return lastRateLimitError || { success: false, error: "Request failed after retries" };
282
+ return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
245
283
  }
246
- async function callAI(systemPrompt, userPrompt, tier = "free") {
247
- if (!API_KEY) {
284
+ async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
285
+ if (!apiKey()) {
248
286
  return {
249
287
  success: false,
250
288
  error: "AI service is not configured. Please report this issue.",
@@ -253,12 +291,12 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
253
291
  }
254
292
  try {
255
293
  const token = await getOrRefreshDirectToken();
256
- return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
294
+ return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier, opts);
257
295
  } catch (err) {
258
296
  return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
259
297
  }
260
298
  }
261
- async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
299
+ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", opts = {}) {
262
300
  if (!email) {
263
301
  return {
264
302
  success: false,
@@ -268,11 +306,11 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
268
306
  }
269
307
  try {
270
308
  let token = await getOrRefreshRelayToken(email);
271
- let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
309
+ let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
272
310
  if (!result.success && result.isAuthError) {
273
311
  cachedRelayToken = null;
274
312
  token = await getOrRefreshRelayToken(email);
275
- result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
313
+ result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
276
314
  }
277
315
  return result;
278
316
  } catch (err) {
@@ -304,18 +342,19 @@ function normalizeUrl(input) {
304
342
  url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
305
343
  return `https://${url}`;
306
344
  }
307
- var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, API_KEY, 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;
308
346
  var init_api_client = __esm({
309
347
  "src/lib/api-client.ts"() {
310
348
  "use strict";
311
349
  init_utils();
350
+ SERVER_BACKOFF_MS = [2e3, 4e3];
351
+ CLI_BACKOFF_MS = [1e4, 3e4];
312
352
  SPARK_AI_BASE = "https://sparkbrain.app";
313
353
  SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
314
354
  SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
315
355
  RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
316
356
  RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
317
357
  RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
318
- API_KEY = process.env.SPARK_AI_KEY || "";
319
358
  cachedDirectToken = null;
320
359
  cachedRelayToken = null;
321
360
  }
@@ -340,7 +379,7 @@ async function getStatusCached(email) {
340
379
  function getTier(d) {
341
380
  if (!d.subscription || !d.registered) return "free";
342
381
  if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
343
- return TIERS[d.subscription.price_id] ?? "free";
382
+ return TIERS[d.subscription.price_id] ?? "pro";
344
383
  }
345
384
  async function getTierByEmail(email) {
346
385
  try {
@@ -357,17 +396,18 @@ async function isVerified(email) {
357
396
  }
358
397
  }
359
398
  function pricingUrl(opts) {
360
- const u = new URL(`${BASE}/pricing/${APP_ID}`);
399
+ const u = new URL(`${BASE}/${OWNER_SLUG}/${APP_ID}`);
361
400
  if (opts?.email) u.searchParams.set("email", opts.email);
362
401
  if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
363
402
  return u.toString();
364
403
  }
365
- 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;
366
405
  var init_SparkStripe_feature_gating = __esm({
367
406
  "src/lib/SparkStripe-feature-gating.ts"() {
368
407
  "use strict";
369
408
  APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
370
- BASE = "https://sparkstripe.com";
409
+ OWNER_SLUG = process.env.SPARK_OWNER_SLUG ?? "spark";
410
+ BASE = "https://sparkpay.dev";
371
411
  TIERS = {
372
412
  price_1TADnPEf9opkuWid9NtqOYON: "pro",
373
413
  price_1TADnPEf9opkuWidgbvvX9ma: "pro",
@@ -419,7 +459,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
419
459
  var init_config = __esm({
420
460
  "src/lib/config.ts"() {
421
461
  "use strict";
422
- VERSION = "1.1.1";
462
+ VERSION = "1.2.1";
423
463
  CONFIG_FILE = "quickpeek.config.json";
424
464
  DEFAULT_CONFIG = {
425
465
  plan: { maxSteps: 8 },
@@ -569,6 +609,9 @@ var init_scraping = __esm({
569
609
  });
570
610
 
571
611
  // src/lib/crawl.ts
612
+ function idSelector(id) {
613
+ return /^[A-Za-z_-][\w-]*$/.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
614
+ }
572
615
  function getActionForType(type) {
573
616
  switch (type) {
574
617
  case "button":
@@ -586,20 +629,29 @@ function getActionForType(type) {
586
629
  return "type";
587
630
  }
588
631
  }
589
- async function crawlPage(url, task2 = noopTask, storageStatePath) {
590
- const cached = crawlCache.get(url);
632
+ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
633
+ const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
634
+ const cached = crawlCache.get(cacheKey);
591
635
  if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
592
636
  task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
593
- return { title: cached.title, elements: [...cached.elements] };
637
+ return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
594
638
  }
595
639
  const browserTask = task2("Launching browser");
596
- const browser = await import_playwright.chromium.launch({ headless: true });
597
- const context = await browser.newContext(storageStatePath ? { storageState: storageStatePath } : {});
640
+ const browser = await import_playwright3.chromium.launch({ headless: true });
641
+ const context = await browser.newContext({
642
+ ...storageStatePath ? { storageState: storageStatePath } : {},
643
+ // Crawl at the size we will record at, or the measurements below describe
644
+ // a page nobody will ever see.
645
+ ...viewport ? { viewport } : {}
646
+ });
598
647
  const page = await context.newPage();
599
648
  browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
600
649
  const crawlTask = task2("Crawling page");
601
650
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
602
651
  const title = await page.title();
652
+ const scrollable = await page.evaluate(
653
+ "Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
654
+ );
603
655
  const rawElements = await page.evaluate(`
604
656
  (() => {
605
657
  const items = [];
@@ -689,9 +741,9 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
689
741
  } else if (el.isFile || el.type === "file") {
690
742
  selector = 'input[type="file"]';
691
743
  } else if (el.type === "range") {
692
- selector = el.id ? `#${el.id}` : 'input[type="range"]';
744
+ selector = el.id ? idSelector(el.id) : 'input[type="range"]';
693
745
  } else if (el.type === "checkbox") {
694
- selector = el.id ? `#${el.id}` : 'input[type="checkbox"]';
746
+ selector = el.id ? idSelector(el.id) : 'input[type="checkbox"]';
695
747
  } else if (el.type === "button") {
696
748
  if (btnTexts[el.text] === 1) {
697
749
  selector = `text="${el.text}"`;
@@ -701,7 +753,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
701
753
  } else if (el.type === "link") {
702
754
  selector = `text="${el.text}"`;
703
755
  } else if (el.id) {
704
- selector = `#${el.id}`;
756
+ selector = idSelector(el.id);
705
757
  } else if (el.name) {
706
758
  selector = `[name="${el.name}"]`;
707
759
  } else if (el.placeholder) {
@@ -729,18 +781,18 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
729
781
  });
730
782
  await browser.close();
731
783
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
732
- crawlCache.set(url, { timestamp: Date.now(), title, elements: [...elements] });
784
+ crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
733
785
  if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
734
786
  const oldest = crawlCache.keys().next().value;
735
787
  if (oldest !== void 0) crawlCache.delete(oldest);
736
788
  }
737
- return { title, elements };
789
+ return { title, elements, scrollable };
738
790
  }
739
- 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;
740
792
  var init_crawl = __esm({
741
793
  "src/lib/crawl.ts"() {
742
794
  "use strict";
743
- import_playwright = require("playwright");
795
+ import_playwright3 = require("playwright");
744
796
  init_scraping();
745
797
  noopTask = () => ({ succeed: () => {
746
798
  } });
@@ -802,9 +854,9 @@ function convertArgToWSL(arg2) {
802
854
  function spawnFFmpeg(cmd2, args) {
803
855
  if (isWindowsRuntime) {
804
856
  const wslArgs = args.map(convertArgToWSL);
805
- return (0, import_node_child_process.spawn)("wsl", ["-e", cmd2, ...wslArgs]);
857
+ return (0, import_node_child_process2.spawn)("wsl", ["-e", cmd2, ...wslArgs]);
806
858
  }
807
- return (0, import_node_child_process.spawn)(cmd2, args);
859
+ return (0, import_node_child_process2.spawn)(cmd2, args);
808
860
  }
809
861
  function runFFmpeg(args, errorLabel) {
810
862
  return new Promise((resolve9, reject) => {
@@ -820,11 +872,11 @@ function runFFmpeg(args, errorLabel) {
820
872
  proc.on("error", reject);
821
873
  });
822
874
  }
823
- var import_node_child_process, isWindowsRuntime;
875
+ var import_node_child_process2, isWindowsRuntime;
824
876
  var init_spawn = __esm({
825
877
  "src/lib/ffmpeg/spawn.ts"() {
826
878
  "use strict";
827
- import_node_child_process = require("child_process");
879
+ import_node_child_process2 = require("child_process");
828
880
  init_utils();
829
881
  isWindowsRuntime = process.platform === "win32";
830
882
  }
@@ -1005,8 +1057,39 @@ function getMediaDuration(filePath) {
1005
1057
  proc.on("error", reject);
1006
1058
  });
1007
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
+ }
1008
1091
  function concatMedia(listPath, output) {
1009
- return runFFmpeg(["-y", "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", output], "ffmpeg concat failed");
1092
+ return runFFmpeg(buildConcatArgs(listPath, output), "ffmpeg concat failed");
1010
1093
  }
1011
1094
  function generateSilence(outputPath, durationMs) {
1012
1095
  return runFFmpeg(
@@ -1255,6 +1338,114 @@ var init_ffmpeg2 = __esm({
1255
1338
  }
1256
1339
  });
1257
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
+
1258
1449
  // src/lib/prompts.ts
1259
1450
  function buildSystemPrompt(maxSteps) {
1260
1451
  let scope;
@@ -1308,7 +1499,7 @@ EXAMPLES:
1308
1499
  \u2022 File upload: "target": "input[type=\\"file\\"]"
1309
1500
  \u2022 Slider: "target": "input[type=\\"range\\"]"`;
1310
1501
  }
1311
- function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
1502
+ function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
1312
1503
  const lang = langName ? `
1313
1504
  Language: Write ALL captions in ${langName}.` : "";
1314
1505
  const purpose = description ? `
@@ -1346,8 +1537,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
1346
1537
  ${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
1347
1538
  }
1348
1539
  const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
1540
+ const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
1541
+ const scrolling = cannotScroll ? `
1542
+
1543
+ SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
1544
+ const scrollRule = cannotScroll ? "\n\u2022 NEVER use the scroll action: this page does not scroll, so the video would freeze. Use wait and click steps only" : "";
1349
1545
  return `URL: ${url}
1350
- Title: ${title}${purpose}
1546
+ Title: ${title}${purpose}${scrolling}
1351
1547
 
1352
1548
  AVAILABLE ELEMENTS (grouped by action):
1353
1549
  ${elementSummary}
@@ -1357,7 +1553,7 @@ REQUIREMENTS:
1357
1553
  \u2022 Complete ONE user journey: input \u2192 process \u2192 output
1358
1554
  \u2022 For CLICK: use simplified text target like "extract frame" or "download"
1359
1555
  \u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
1360
- \u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
1556
+ \u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
1361
1557
 
1362
1558
  Return JSON:
1363
1559
  {
@@ -1380,10 +1576,12 @@ TARGET SYNTAX:
1380
1576
  \u2022 Sliders: input[type="range"]
1381
1577
  \u2022 Text inputs: Use selector from list above`;
1382
1578
  }
1579
+ var MIN_USEFUL_SCROLL_PX;
1383
1580
  var init_prompts = __esm({
1384
1581
  "src/lib/prompts.ts"() {
1385
1582
  "use strict";
1386
1583
  init_utils();
1584
+ MIN_USEFUL_SCROLL_PX = 200;
1387
1585
  }
1388
1586
  });
1389
1587
 
@@ -1449,7 +1647,7 @@ var init_styles = __esm({
1449
1647
  async function registerFreeUser(email) {
1450
1648
  const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
1451
1649
  try {
1452
- const response = await fetch(`https://sparkstripe.com/api/register-free`, {
1650
+ const response = await fetch(`https://sparkpay.dev/api/register-free`, {
1453
1651
  method: "POST",
1454
1652
  headers: { "Content-Type": "application/json" },
1455
1653
  body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
@@ -1516,7 +1714,7 @@ async function getMediaStepDuration(step, baseDir) {
1516
1714
  return 0;
1517
1715
  }
1518
1716
  const mediaPath = (0, import_node_path2.isAbsolute)(step.value) ? step.value : (0, import_node_path2.resolve)(baseDir, step.value);
1519
- if (!(0, import_node_fs2.existsSync)(mediaPath)) {
1717
+ if (!(0, import_node_fs3.existsSync)(mediaPath)) {
1520
1718
  return 0;
1521
1719
  }
1522
1720
  try {
@@ -1538,8 +1736,8 @@ async function getMediaStepDuration(step, baseDir) {
1538
1736
  async function loadVoiceoverMeta(outputDir) {
1539
1737
  const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
1540
1738
  try {
1541
- if (!(0, import_node_fs2.existsSync)(metaPath)) return null;
1542
- 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");
1543
1741
  return JSON.parse(content);
1544
1742
  } catch {
1545
1743
  return null;
@@ -1547,7 +1745,7 @@ async function loadVoiceoverMeta(outputDir) {
1547
1745
  }
1548
1746
  async function saveVoiceoverMeta(outputDir, meta) {
1549
1747
  const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
1550
- await (0, import_promises2.writeFile)(metaPath, JSON.stringify(meta, null, 2));
1748
+ await (0, import_promises3.writeFile)(metaPath, JSON.stringify(meta, null, 2));
1551
1749
  }
1552
1750
  async function concatenateAudio(inputs, output, workDir) {
1553
1751
  const formatPath = (f) => {
@@ -1556,17 +1754,17 @@ async function concatenateAudio(inputs, output, workDir) {
1556
1754
  };
1557
1755
  const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
1558
1756
  const listPath = (0, import_node_path2.join)(workDir, "concat.txt");
1559
- await (0, import_promises2.writeFile)(listPath, listContent);
1757
+ await (0, import_promises3.writeFile)(listPath, listContent);
1560
1758
  await concatMedia(listPath, output);
1561
- await (0, import_promises2.unlink)(listPath).catch(() => {
1759
+ await (0, import_promises3.unlink)(listPath).catch(() => {
1562
1760
  });
1563
1761
  }
1564
- var import_node_fs2, import_promises2, import_node_path2, isWindows;
1762
+ var import_node_fs3, import_promises3, import_node_path2, isWindows;
1565
1763
  var init_helpers = __esm({
1566
1764
  "src/lib/tts/helpers.ts"() {
1567
1765
  "use strict";
1568
- import_node_fs2 = require("fs");
1569
- import_promises2 = require("fs/promises");
1766
+ import_node_fs3 = require("fs");
1767
+ import_promises3 = require("fs/promises");
1570
1768
  import_node_path2 = require("path");
1571
1769
  init_ffmpeg2();
1572
1770
  init_utils();
@@ -1615,9 +1813,9 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1615
1813
  const audioPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}.mp3`);
1616
1814
  const originalPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-padding.mp3`);
1617
1815
  const silencePath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-silence.mp3`);
1618
- if ((0, import_node_fs3.existsSync)(audioPath)) {
1816
+ if ((0, import_node_fs4.existsSync)(audioPath)) {
1619
1817
  segments.push(audioPath);
1620
- if ((0, import_node_fs3.existsSync)(originalPaddingPath)) {
1818
+ if ((0, import_node_fs4.existsSync)(originalPaddingPath)) {
1621
1819
  segments.push(originalPaddingPath);
1622
1820
  }
1623
1821
  if (recordingDuration > voiceDuration) {
@@ -1625,7 +1823,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1625
1823
  await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
1626
1824
  segments.push(alignPaddingPath);
1627
1825
  }
1628
- } else if ((0, import_node_fs3.existsSync)(silencePath)) {
1826
+ } else if ((0, import_node_fs4.existsSync)(silencePath)) {
1629
1827
  if (targetDuration !== voiceDuration) {
1630
1828
  await generateSilence(silencePath, targetDuration);
1631
1829
  }
@@ -1649,11 +1847,11 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1649
1847
  }
1650
1848
  return alignedStepAudio;
1651
1849
  }
1652
- var import_node_fs3, import_node_path3;
1850
+ var import_node_fs4, import_node_path3;
1653
1851
  var init_timing = __esm({
1654
1852
  "src/lib/tts/timing.ts"() {
1655
1853
  "use strict";
1656
- import_node_fs3 = require("fs");
1854
+ import_node_fs4 = require("fs");
1657
1855
  import_node_path3 = require("path");
1658
1856
  init_ffmpeg2();
1659
1857
  init_helpers();
@@ -1718,6 +1916,19 @@ var init_voices = __esm({
1718
1916
 
1719
1917
  // src/lib/tts/synthesize.ts
1720
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) {
1721
1932
  const tempScriptPath = (0, import_node_path5.join)((0, import_node_path5.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
1722
1933
  const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(tempScriptPath)) : tempScriptPath;
1723
1934
  const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(outputPath)) : outputPath;
@@ -1735,44 +1946,49 @@ async def main():
1735
1946
  asyncio.run(main())
1736
1947
  `;
1737
1948
  try {
1738
- await (0, import_promises3.writeFile)(tempScriptPath, pythonScript);
1949
+ await (0, import_promises4.writeFile)(tempScriptPath, pythonScript);
1739
1950
  const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
1740
1951
  await execAsync(pythonCmd, { timeout: 3e4 });
1741
- await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
1742
- }));
1743
- return;
1744
- } catch (pythonErr) {
1745
- await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
1746
- }));
1747
- const pythonMsg = pythonErr instanceof Error ? pythonErr.message : String(pythonErr);
1748
- const maxRetries = 2;
1749
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1750
- try {
1751
- const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
1752
- const tts = new MsEdgeTTS();
1753
- await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
1754
- await tts.toFile(outputPath, text);
1755
- return;
1756
- } catch (nodeErr) {
1757
- if (attempt === maxRetries) {
1758
- const nodeMsg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
1759
- throw new Error(`TTS failed (Python: ${pythonMsg.slice(0, 100)}, Node: ${nodeMsg.slice(0, 100)})`);
1760
- }
1761
- 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)})`);
1762
1974
  }
1975
+ await new Promise((r) => setTimeout(r, 500));
1763
1976
  }
1764
1977
  }
1765
1978
  }
1766
- 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;
1767
1980
  var init_synthesize = __esm({
1768
1981
  "src/lib/tts/synthesize.ts"() {
1769
1982
  "use strict";
1770
- import_node_child_process2 = require("child_process");
1771
- 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");
1772
1986
  import_node_path5 = require("path");
1987
+ import_promises5 = require("stream/promises");
1773
1988
  import_node_util = require("util");
1774
1989
  init_helpers();
1775
- 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;
1776
1992
  }
1777
1993
  });
1778
1994
 
@@ -1809,7 +2025,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1809
2025
  return { voiceoverPath: "", stepAudio: stepAudio2 };
1810
2026
  }
1811
2027
  const audioDir = (0, import_node_path6.join)(workDir, "audio");
1812
- await (0, import_promises4.mkdir)(audioDir, { recursive: true });
2028
+ await (0, import_promises6.mkdir)(audioDir, { recursive: true });
1813
2029
  const segments = [];
1814
2030
  const stepAudio = /* @__PURE__ */ new Map();
1815
2031
  const ttsTask = task2("Generating voiceover");
@@ -1860,7 +2076,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1860
2076
  return { voiceoverPath: "", stepAudio };
1861
2077
  }
1862
2078
  const concatTask = task2("Combining audio");
1863
- await (0, import_promises4.mkdir)(outputDir, { recursive: true });
2079
+ await (0, import_promises6.mkdir)(outputDir, { recursive: true });
1864
2080
  const voiceoverPath = (0, import_node_path6.join)(outputDir, "voiceover.wav");
1865
2081
  await concatenateAudio(segments, voiceoverPath, workDir);
1866
2082
  concatTask.succeed("Done");
@@ -1874,11 +2090,11 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1874
2090
  await saveVoiceoverMeta(outputDir, meta);
1875
2091
  return { voiceoverPath, stepAudio };
1876
2092
  }
1877
- var import_promises4, import_node_path6;
2093
+ var import_promises6, import_node_path6;
1878
2094
  var init_generate = __esm({
1879
2095
  "src/lib/tts/generate.ts"() {
1880
2096
  "use strict";
1881
- import_promises4 = require("fs/promises");
2097
+ import_promises6 = require("fs/promises");
1882
2098
  import_node_path6 = require("path");
1883
2099
  init_ffmpeg2();
1884
2100
  init_voices();
@@ -1925,6 +2141,7 @@ var init_types3 = __esm({
1925
2141
  // src/lib/index.ts
1926
2142
  var lib_exports = {};
1927
2143
  __export(lib_exports, {
2144
+ CLI_BACKOFF_MS: () => CLI_BACKOFF_MS,
1928
2145
  CONFIG_FILE: () => CONFIG_FILE,
1929
2146
  CURSOR_CSS: () => CURSOR_CSS,
1930
2147
  CURSOR_PNG: () => CURSOR_PNG,
@@ -1935,9 +2152,11 @@ __export(lib_exports, {
1935
2152
  FEATURES: () => FEATURES,
1936
2153
  INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
1937
2154
  LANGUAGE_VOICES: () => import_video_kit5.WIDE_VOICES,
2155
+ SERVER_BACKOFF_MS: () => SERVER_BACKOFF_MS,
1938
2156
  VERSION: () => VERSION,
1939
2157
  alignVoiceoverToRecording: () => alignVoiceoverToRecording,
1940
2158
  applyTransitions: () => applyTransitions,
2159
+ buildConcatArgs: () => buildConcatArgs,
1941
2160
  buildKeepSegments: () => buildKeepSegments,
1942
2161
  buildStepTransitions: () => buildStepTransitions,
1943
2162
  buildSystemPrompt: () => buildSystemPrompt,
@@ -1951,9 +2170,11 @@ __export(lib_exports, {
1951
2170
  concatVideoWithOutro: () => concatVideoWithOutro,
1952
2171
  crawlPage: () => crawlPage,
1953
2172
  estimateWordTimings: () => estimateWordTimings,
2173
+ extractJsonCandidates: () => extractJsonCandidates,
1954
2174
  generateASSHeader: () => generateASSHeader,
1955
2175
  generateSilence: () => generateSilence,
1956
2176
  generateVoiceover: () => generateVoiceover,
2177
+ getAudioCodec: () => getAudioCodec,
1957
2178
  getErrorMessage: () => getErrorMessage,
1958
2179
  getLanguageName: () => getLanguageName,
1959
2180
  getMediaDuration: () => getMediaDuration,
@@ -1964,20 +2185,25 @@ __export(lib_exports, {
1964
2185
  hasAccess: () => hasAccess,
1965
2186
  hasTrialRemaining: () => hasTrialRemaining,
1966
2187
  hexToASS: () => import_video_kit.hexToAss,
2188
+ idSelector: () => idSelector,
1967
2189
  isCanceling: () => isCanceling,
1968
2190
  isExcludedLink: () => isExcludedLink,
2191
+ isHonestWav: () => isHonestWav,
1969
2192
  isPaid: () => isPaid,
1970
2193
  isVerified: () => isVerified,
1971
2194
  loadExistingVoiceover: () => loadExistingVoiceover,
1972
2195
  loadVoiceoverMeta: () => loadVoiceoverMeta,
1973
2196
  mapTimestampsAfterCompression: () => mapTimestampsAfterCompression,
1974
2197
  normalizeUrl: () => normalizeUrl,
2198
+ parseAIJson: () => parseAIJson,
1975
2199
  parseAIPlanResponse: () => parseAIPlanResponse,
1976
2200
  parsePartial: () => parsePartial,
1977
2201
  pricingUrl: () => pricingUrl,
1978
2202
  registerFreeUser: () => registerFreeUser,
2203
+ repairUnicodeEscapes: () => repairUnicodeEscapes,
1979
2204
  resolveVoice: () => resolveVoice,
1980
2205
  rgbaToHex: () => rgbaToHex,
2206
+ salvageTruncatedJson: () => salvageTruncatedJson,
1981
2207
  shouldSkipLink: () => shouldSkipLink,
1982
2208
  shouldWatermark: () => shouldWatermark,
1983
2209
  showPaywall: () => showPaywall,
@@ -1996,6 +2222,7 @@ var init_lib = __esm({
1996
2222
  init_crawl();
1997
2223
  init_cursor();
1998
2224
  init_ffmpeg2();
2225
+ init_json_repair();
1999
2226
  init_prompts();
2000
2227
  init_SparkStripe_feature_gating();
2001
2228
  init_scraping();
@@ -2011,19 +2238,17 @@ var init_lib = __esm({
2011
2238
 
2012
2239
  // src/qp.ts
2013
2240
  var import_dotenv = __toESM(require("dotenv"));
2014
- var import_chalk20 = __toESM(require("chalk"));
2241
+ var import_chalk21 = __toESM(require("chalk"));
2015
2242
 
2016
2243
  // src/commands/auth-cmd.ts
2017
- var import_node_readline2 = require("readline");
2018
2244
  var import_chalk3 = __toESM(require("chalk"));
2019
- var import_playwright2 = require("playwright");
2020
2245
 
2021
2246
  // src/browser/auth-state.ts
2022
2247
  var import_node_fs = require("fs");
2023
2248
  var import_promises = require("fs/promises");
2024
2249
  var import_node_os = require("os");
2025
2250
  var import_node_path = require("path");
2026
- 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");
2027
2252
  function hostOf(url) {
2028
2253
  try {
2029
2254
  return new URL(url).host;
@@ -2035,150 +2260,462 @@ function authStatePath(url) {
2035
2260
  const host = hostOf(url);
2036
2261
  return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.json`) : null;
2037
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
+ }
2038
2267
  function findAuthState(url) {
2039
2268
  const path = authStatePath(url);
2040
2269
  return path && (0, import_node_fs.existsSync)(path) ? path : void 0;
2041
2270
  }
2042
- async function saveAuthState(url, state) {
2271
+ async function saveAuthState(url, state, startUrl) {
2043
2272
  const path = authStatePath(url);
2044
2273
  if (!path) throw new Error(`Not a valid URL: ${url}`);
2045
2274
  await (0, import_promises.mkdir)(AUTH_DIR, { recursive: true });
2046
2275
  await (0, import_promises.writeFile)(path, JSON.stringify(state, null, 2));
2047
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
+ }
2048
2280
  return path;
2049
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
+ }
2050
2302
  async function clearAuthState(url) {
2051
2303
  if (url) {
2052
2304
  const path = authStatePath(url);
2053
2305
  if (!path || !(0, import_node_fs.existsSync)(path)) return [];
2054
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);
2055
2309
  return [hostOf(url)];
2056
2310
  }
2057
2311
  if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
2058
2312
  const files = (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json"));
2059
2313
  await Promise.all(files.map((f) => (0, import_promises.unlink)((0, import_node_path.join)(AUTH_DIR, f))));
2060
- 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));
2061
2315
  }
2062
2316
  async function listAuthStates() {
2063
2317
  if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
2064
- 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));
2065
2319
  }
2066
2320
 
2067
- // src/help.ts
2068
- var import_chalk = __toESM(require("chalk"));
2069
- init_lib();
2070
-
2071
- // src/logo.ts
2072
- var import_figlet = __toESM(require("figlet"));
2073
- var import_gradient_string = __toESM(require("gradient-string"));
2074
- var DEFAULT_LOGO_CONFIG = {
2075
- text: "QuickPeek",
2076
- font: "Slant",
2077
- horizontalLayout: "default",
2078
- gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
2079
- suffix: " \u{1F440}"
2080
- };
2081
- function renderLogo(config = {}) {
2082
- const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
2083
- const ascii = import_figlet.default.textSync(resolved.text, {
2084
- font: resolved.font,
2085
- horizontalLayout: resolved.horizontalLayout
2086
- });
2087
- const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
2088
- return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
2089
- }
2321
+ // src/browser/launch.ts
2322
+ var import_promises2 = require("fs/promises");
2323
+ var import_playwright2 = require("playwright");
2090
2324
 
2091
- // src/utils/config-loader.ts
2092
- var import_node_fs4 = require("fs");
2093
- var import_promises5 = require("fs/promises");
2094
- var import_node_os2 = require("os");
2095
- var import_node_path7 = require("path");
2096
- init_lib();
2097
- async function loadConfig(_url, langOverride) {
2098
- const configPath = (0, import_node_path7.join)(process.cwd(), CONFIG_FILE);
2099
- if ((0, import_node_fs4.existsSync)(configPath)) {
2100
- try {
2101
- const content = await (0, import_promises5.readFile)(configPath, "utf-8");
2102
- const userConfig = JSON.parse(content);
2103
- const config = {
2104
- lang: langOverride || userConfig.lang || "en",
2105
- plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
2106
- video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
2107
- audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
2108
- music: userConfig.music,
2109
- timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
2110
- output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
2111
- };
2112
- return { config, created: false };
2113
- } catch {
2114
- }
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;
2115
2337
  }
2116
- const fullDefaultConfig = {
2117
- lang: "en",
2118
- plan: { maxSteps: 8 },
2119
- video: {
2120
- width: 1080,
2121
- height: 1920,
2122
- fps: 30,
2123
- quality: 0.5,
2124
- preset: "fast",
2125
- cursor: true,
2126
- highlight: false,
2127
- fadeIn: 500,
2128
- // ms
2129
- fadeOut: 500,
2130
- // ms
2131
- contrast: 0.25
2132
- },
2133
- audio: {
2134
- // Voice is derived from lang setting via LANGUAGE_VOICES mapping
2135
- // To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
2136
- speed: 1
2137
- },
2138
- music: {
2139
- path: "",
2140
- volume: 0.3
2141
- },
2142
- output: { dir: "./demo" }
2143
- };
2144
- await (0, import_promises5.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
2145
- return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
2146
2338
  }
2147
- var WORK_DIR = ".quickpeek";
2148
- async function prepareWorkDir() {
2149
- const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
2150
- if ((0, import_node_fs4.existsSync)(workDir)) {
2151
- await (0, import_promises5.rm)(workDir, { recursive: true });
2152
- }
2153
- await (0, import_promises5.mkdir)(workDir, { recursive: true });
2154
- return workDir;
2339
+ function tryEnableWSLInterop() {
2340
+ return (0, import_node_fs2.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
2155
2341
  }
2156
- var USER_DATA_DIR = (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".quickpeek");
2157
- var USER_DATA_FILE = (0, import_node_path7.join)(USER_DATA_DIR, "user.json");
2158
- 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;
2159
2350
  try {
2160
- if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
2161
- return JSON.parse(await (0, import_promises5.readFile)(USER_DATA_FILE, "utf-8"));
2162
- }
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, "/");
2163
2353
  } catch {
2164
2354
  }
2165
- return {};
2355
+ return null;
2166
2356
  }
2167
- async function saveUserData(userData) {
2168
- if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
2169
- 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 {
2170
2370
  }
2171
- const existing = await loadUserData();
2172
- const merged = { ...existing, ...userData };
2173
- await (0, import_promises5.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
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";
2174
2377
  }
2175
- var DEFAULT_CAPTIONS_CSS = `.caption {
2176
- font-family: Arial, sans-serif;
2177
- -webkit-text-stroke: 3px #000000;
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
+ });
2178
2397
  }
2179
- .caption .regular {
2180
- font-size: 56px;
2181
- color: #FFFFFF;
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;
2182
2719
  }
2183
2720
  .caption .highlight {
2184
2721
  font-size: 64px;
@@ -2232,12 +2769,12 @@ function parseCaptionCSS(css) {
2232
2769
  }
2233
2770
  async function loadCaptions() {
2234
2771
  const captionsPath = (0, import_node_path7.join)(process.cwd(), "captions.css");
2235
- if (!(0, import_node_fs4.existsSync)(captionsPath)) {
2236
- 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);
2237
2774
  return DEFAULT_CAPTIONS;
2238
2775
  }
2239
2776
  try {
2240
- const css = await (0, import_promises5.readFile)(captionsPath, "utf-8");
2777
+ const css = await (0, import_promises7.readFile)(captionsPath, "utf-8");
2241
2778
  return parseCaptionCSS(css);
2242
2779
  } catch {
2243
2780
  return DEFAULT_CAPTIONS;
@@ -2248,8 +2785,8 @@ async function savePlan(plan, planPath) {
2248
2785
  ...plan,
2249
2786
  steps: plan.steps.map(({ duration, ...step }) => step)
2250
2787
  };
2251
- await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(planPath), { recursive: true });
2252
- 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));
2253
2790
  }
2254
2791
 
2255
2792
  // src/help.ts
@@ -2324,9 +2861,11 @@ async function showHelp() {
2324
2861
  "Use your own narration script"
2325
2862
  ],
2326
2863
  [`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
2864
+ [`${cmd("qp edit")} ${arg("<url>")}`, `qp edit <url>`, "Edit the demo plan in the browser"],
2327
2865
  [`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
2328
2866
  [`${cmd("qp logout")}`, `qp logout`, "Sign out"],
2329
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"],
2330
2869
  [`${cmd("qp auth clear")} ${arg("[url]")}`, `qp auth clear [url]`, "Forget saved sessions"],
2331
2870
  [`${cmd("qp init")}`, `qp init`, "Create config file"],
2332
2871
  [`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
@@ -2381,6 +2920,9 @@ var autoYes = false;
2381
2920
  function setAutoYes(value) {
2382
2921
  autoYes = value;
2383
2922
  }
2923
+ function isAutoYes() {
2924
+ return autoYes;
2925
+ }
2384
2926
  function task(text) {
2385
2927
  return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
2386
2928
  }
@@ -2411,10 +2953,19 @@ function promptInput(prompt2) {
2411
2953
  });
2412
2954
  });
2413
2955
  }
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
+ }
2414
2965
  var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
2415
2966
  function formatClickablePath(filePath) {
2416
2967
  const absolutePath = (0, import_node_path8.resolve)(filePath);
2417
- if (_isWSL) {
2968
+ if (_isWSL || process.platform !== "win32") {
2418
2969
  return absolutePath;
2419
2970
  }
2420
2971
  let formatted = absolutePath;
@@ -2431,15 +2982,6 @@ function formatDuration(ms) {
2431
2982
  }
2432
2983
 
2433
2984
  // src/commands/auth-cmd.ts
2434
- function waitForEnter(prompt2) {
2435
- return new Promise((resolve9) => {
2436
- const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
2437
- rl.question(prompt2, () => {
2438
- rl.close();
2439
- resolve9();
2440
- });
2441
- });
2442
- }
2443
2985
  async function authCommand(url) {
2444
2986
  console.log();
2445
2987
  showLogo();
@@ -2447,9 +2989,10 @@ async function authCommand(url) {
2447
2989
  console.log(import_chalk3.default.red(` Not a valid URL: ${url}`));
2448
2990
  return;
2449
2991
  }
2450
- 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."));
2451
2994
  console.log();
2452
- const browser = await import_playwright2.chromium.launch({ headless: false });
2995
+ const browser = await launchSignInBrowser();
2453
2996
  const context = await browser.newContext();
2454
2997
  const page = await context.newPage();
2455
2998
  try {
@@ -2459,11 +3002,15 @@ async function authCommand(url) {
2459
3002
  await waitForEnter(import_chalk3.default.cyan(" Press Enter when you are signed in... "));
2460
3003
  try {
2461
3004
  const state = await context.storageState();
2462
- const path = await saveAuthState(url, state);
3005
+ const startUrl = captureStartUrl(context, url);
3006
+ const path = await saveAuthState(url, state, startUrl);
2463
3007
  console.log();
2464
3008
  console.log(import_chalk3.default.green(` \u2713 Session saved for ${new URL(url).host}`));
2465
3009
  console.log(import_chalk3.default.dim(` ${formatClickablePath(path)}`));
2466
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
+ }
2467
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."));
2468
3015
  } finally {
2469
3016
  await browser.close().catch(() => {
@@ -2505,24 +3052,31 @@ var import_chalk11 = __toESM(require("chalk"));
2505
3052
  init_lib();
2506
3053
 
2507
3054
  // src/ai/client.ts
2508
- var import_node_readline3 = require("readline");
3055
+ var import_node_readline2 = require("readline");
2509
3056
  var import_chalk5 = __toESM(require("chalk"));
2510
3057
  init_lib();
2511
3058
 
2512
3059
  // src/utils/browser-auth.ts
2513
- var import_node_http = require("http");
3060
+ var import_node_http2 = require("http");
2514
3061
  var import_node_url = require("url");
2515
3062
  var import_chalk4 = __toESM(require("chalk"));
2516
- init_SparkStripe_feature_gating();
2517
- var TIMEOUT_MS = 12e4;
2518
- async function openBrowser(url) {
2519
- const { exec: exec2 } = await import("child_process");
3063
+
3064
+ // src/lib/browser-open.ts
3065
+ var import_node_child_process4 = require("child_process");
3066
+ function editorBaseUrl() {
3067
+ return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
3068
+ }
3069
+ function openInBrowser(url) {
2520
3070
  const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
2521
3071
  const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
2522
3072
  return new Promise((resolve9) => {
2523
- exec2(command, () => resolve9());
3073
+ (0, import_node_child_process4.exec)(command, () => resolve9());
2524
3074
  });
2525
3075
  }
3076
+
3077
+ // src/utils/browser-auth.ts
3078
+ init_SparkStripe_feature_gating();
3079
+ var TIMEOUT_MS = 12e4;
2526
3080
  async function browserLogin() {
2527
3081
  return new Promise((resolve9) => {
2528
3082
  let server;
@@ -2532,7 +3086,7 @@ async function browserLogin() {
2532
3086
  server?.close();
2533
3087
  resolve9(email);
2534
3088
  };
2535
- server = (0, import_node_http.createServer)((req, res) => {
3089
+ server = (0, import_node_http2.createServer)((req, res) => {
2536
3090
  const url = new import_node_url.URL(req.url || "/", `http://localhost`);
2537
3091
  const path = url.pathname;
2538
3092
  if (path === "/callback" || path === "/") {
@@ -2576,7 +3130,7 @@ async function browserLogin() {
2576
3130
  console.log(import_chalk4.default.dim(` ${url}`));
2577
3131
  console.log();
2578
3132
  console.log(import_chalk4.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
2579
- openBrowser(url);
3133
+ void openInBrowser(url);
2580
3134
  });
2581
3135
  timer = setTimeout(() => {
2582
3136
  console.log();
@@ -2586,13 +3140,132 @@ async function browserLogin() {
2586
3140
  });
2587
3141
  }
2588
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
+
2589
3234
  // src/ai/client.ts
2590
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
+ }
2591
3264
  function getLastRateLimitInfo() {
2592
3265
  return lastRateLimitInfo;
2593
3266
  }
2594
3267
  function prompt(question) {
2595
- 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 });
2596
3269
  return new Promise((resolve9) => {
2597
3270
  rl.question(question, (answer) => {
2598
3271
  rl.close();
@@ -2635,15 +3308,33 @@ async function setupUserEmail() {
2635
3308
  console.log();
2636
3309
  return email;
2637
3310
  }
2638
- async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
2639
- 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);
2640
3330
  if (!result.success) {
2641
- 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));
2642
3333
  if (result.isRateLimit) {
2643
3334
  console.log();
2644
3335
  console.log(import_chalk5.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
2645
3336
  console.log();
2646
- 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}`));
2647
3338
  console.log(import_chalk5.default.dim(" Options:"));
2648
3339
  if (result.retryAfterSeconds) {
2649
3340
  const minutes = Math.ceil(result.retryAfterSeconds / 60);
@@ -2658,17 +3349,31 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
2658
3349
  }
2659
3350
  if (result.isAuthError) {
2660
3351
  console.log();
2661
- console.log(import_chalk5.default.red(" \u26A0 Authentication Error"));
3352
+ console.log(import_chalk5.default.red(` \u26A0 Authentication Error${result.status ? ` (HTTP ${result.status})` : ""}`));
2662
3353
  console.log();
2663
- 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)}`));
2664
3359
  console.log();
2665
3360
  process.exit(1);
2666
3361
  }
2667
3362
  console.log();
2668
- console.log(import_chalk5.default.red(" \u26A0 AI Error"));
3363
+ console.log(import_chalk5.default.red(` \u26A0 AI Error${result.status ? ` (HTTP ${result.status})` : ""}`));
2669
3364
  console.log();
2670
- const firstLine = result.error.split("\n")[0] ?? result.error;
2671
- console.log(import_chalk5.default.dim(` ${firstLine.slice(0, 80)}`));
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
+ }
3374
+ console.log();
3375
+ console.log(import_chalk5.default.dim(` ${diagnose(result.status, result.host)}`));
3376
+ console.log(import_chalk5.default.dim(` Full response body: ${logFilePath}`));
2672
3377
  console.log();
2673
3378
  process.exit(1);
2674
3379
  }
@@ -2676,7 +3381,48 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
2676
3381
  return result.content;
2677
3382
  }
2678
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
+
2679
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
+ }
2680
3426
  async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
2681
3427
  const langName = getLanguageName(lang);
2682
3428
  const stepList = steps.map((s, i) => {
@@ -2702,33 +3448,34 @@ Steps to caption:
2702
3448
  ${stepList}
2703
3449
 
2704
3450
  Return exactly ${steps.length} captions as a JSON array.`;
2705
- const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
2706
- const arrayMatch = content.match(/\[[\s\S]*\]/);
2707
- if (!arrayMatch) throw new Error("No JSON array in AI caption response");
2708
- 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;
2709
3455
  return steps.map((s, i) => ({
2710
3456
  ...s,
2711
3457
  id: i + 1,
2712
3458
  caption: toTitleCase(captions[i] ?? `Step ${i + 1}.`)
2713
3459
  }));
2714
3460
  }
2715
- 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) {
2716
3462
  const systemPrompt = `You map narration sentences to web UI actions.
2717
- 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.
2718
3464
 
2719
3465
  RULES:
2720
- - Return ONLY valid JSON
2721
- - 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
2722
3468
  - Use page elements to pick realistic actions/targets
2723
- - 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
2724
3471
 
2725
3472
  ACTION FORMATS:
2726
- \u2022 click: { "action": "click", "target": "button text" }
2727
- \u2022 type: { "action": "type", "target": "input selector", "value": "text from caption" }
2728
- \u2022 scroll: { "action": "scroll", "value": "up|down" }
2729
- \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" }
2730
3477
 
2731
- OUTPUT: full plan JSON with url, title, description, steps[]`;
3478
+ OUTPUT: {"description":"one short sentence","steps":[ ... ]}`;
2732
3479
  const scriptLines = captions.map((c, i) => `${i + 1}. "${c}"`).join("\n");
2733
3480
  const userMessage = `URL: ${url}
2734
3481
  App: ${title}
@@ -2736,22 +3483,46 @@ App: ${title}
2736
3483
  Available page elements:
2737
3484
  ${pageElementsText}
2738
3485
 
2739
- Narration script (map each line to a UI action, preserve caption text exactly):
3486
+ Narration script (map each line to a UI action):
2740
3487
  ${scriptLines}
2741
3488
 
2742
- Return the full plan JSON.`;
2743
- const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
2744
- const jsonMatch = content.match(/\{[\s\S]*\}/);
2745
- if (!jsonMatch) throw new Error("No JSON in AI script response");
2746
- const plan = JSON.parse(jsonMatch[0]);
2747
- plan.url = url;
2748
- if (lang !== void 0) plan.lang = lang;
2749
- plan.steps = plan.steps.map(({ duration: _d, ...s }, i) => ({
2750
- ...s,
2751
- id: i + 1,
2752
- caption: captions[i] ?? s.caption ?? ""
2753
- }));
2754
- 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
+ };
2755
3526
  }
2756
3527
 
2757
3528
  // src/browser/events-to-plan.ts
@@ -2858,152 +3629,9 @@ function normalizeUrl2(url) {
2858
3629
  }
2859
3630
 
2860
3631
  // src/capture.ts
2861
- var import_node_child_process4 = require("child_process");
3632
+ var import_node_child_process5 = require("child_process");
2862
3633
  var import_node_http3 = require("http");
2863
- var import_node_readline4 = require("readline");
2864
3634
  var import_playwright4 = require("playwright");
2865
-
2866
- // src/browser/wsl.ts
2867
- var import_node_child_process3 = require("child_process");
2868
- var import_node_fs5 = require("fs");
2869
- var import_node_http2 = require("http");
2870
- var import_playwright3 = require("playwright");
2871
- function isWSL() {
2872
- if (process.platform !== "linux") return false;
2873
- try {
2874
- const r = (0, import_node_child_process3.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
2875
- return r.includes("microsoft") || r.includes("wsl");
2876
- } catch {
2877
- return false;
2878
- }
2879
- }
2880
- function tryEnableWSLInterop() {
2881
- return (0, import_node_fs5.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
2882
- }
2883
- function wslToWindowsPath(wslPath) {
2884
- if (!wslPath) return wslPath;
2885
- const m = wslPath.match(/^\/mnt\/([a-z])\/(.*)/);
2886
- if (!m) return wslPath;
2887
- return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
2888
- }
2889
- function findExe(whereExeQuery, ...hardcoded) {
2890
- for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
2891
- try {
2892
- const line = (0, import_node_child_process3.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
2893
- if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
2894
- } catch {
2895
- }
2896
- return null;
2897
- }
2898
- var findWindowsChrome = () => findExe(
2899
- "chrome.exe",
2900
- "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
2901
- "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
2902
- );
2903
- var findWindowsNode = () => findExe("node.exe");
2904
- function getWindowsHostIP() {
2905
- try {
2906
- const m = (0, import_node_child_process3.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
2907
- /default via (\d+\.\d+\.\d+\.\d+)/
2908
- );
2909
- if (m?.[1]) return m[1];
2910
- } catch {
2911
- }
2912
- try {
2913
- const m = (0, import_node_fs5.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
2914
- if (m?.[1]) return m[1];
2915
- } catch {
2916
- }
2917
- return "127.0.0.1";
2918
- }
2919
- function startRelay(nodePath, windowsHost, targetPort) {
2920
- 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')});`;
2921
- return new Promise((resolve9, reject) => {
2922
- const proc = (0, import_node_child_process3.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
2923
- const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
2924
- let buf = "";
2925
- proc.stdout.on("data", (d) => {
2926
- buf += d.toString();
2927
- const m = buf.match(/PORT:(\d+)/);
2928
- if (m) {
2929
- clearTimeout(timer);
2930
- resolve9({ proc, port: +m[1] });
2931
- }
2932
- });
2933
- proc.on("error", (e) => {
2934
- clearTimeout(timer);
2935
- reject(e);
2936
- });
2937
- });
2938
- }
2939
- function waitForChrome(host, port) {
2940
- return new Promise((resolve9, reject) => {
2941
- const deadline = Date.now() + 2e4;
2942
- const poll = () => {
2943
- if (Date.now() > deadline) {
2944
- reject(new Error("Chrome debug port did not open within 20 s"));
2945
- return;
2946
- }
2947
- const req = (0, import_node_http2.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
2948
- res.resume();
2949
- resolve9();
2950
- });
2951
- req.on("error", () => setTimeout(poll, 600));
2952
- req.setTimeout(1e3, () => {
2953
- req.destroy();
2954
- setTimeout(poll, 600);
2955
- });
2956
- req.end();
2957
- };
2958
- poll();
2959
- });
2960
- }
2961
- async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
2962
- const debugPort = options.debugPort ?? 9222;
2963
- const chromePath = findWindowsChrome();
2964
- if (!chromePath) throw new Error("Windows Chrome not found. Install Google Chrome for Windows.");
2965
- const nodePath = findWindowsNode();
2966
- if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
2967
- const winUserData = wslToWindowsPath(userDataDir);
2968
- const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
2969
- const chromeProc = (0, import_node_child_process3.spawn)(
2970
- chromePath,
2971
- [
2972
- `--remote-debugging-port=${debugPort}`,
2973
- `--user-data-dir=${winUserData}`,
2974
- ...winExtPath ? [`--disable-extensions-except=${winExtPath}`, `--load-extension=${winExtPath}`] : [],
2975
- "--no-first-run",
2976
- "--no-default-browser-check",
2977
- "--disable-background-networking",
2978
- "--disable-sync",
2979
- "--disable-features=TranslateUI",
2980
- options.startMaximized ? "--start-maximized" : "--start-minimized"
2981
- ],
2982
- { stdio: "ignore", detached: true }
2983
- );
2984
- chromeProc.unref();
2985
- const windowsHost = getWindowsHostIP();
2986
- const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
2987
- try {
2988
- await waitForChrome(windowsHost, proxyPort);
2989
- const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
2990
- return {
2991
- browser,
2992
- cleanup: async () => {
2993
- try {
2994
- await browser.close();
2995
- } catch {
2996
- }
2997
- relay.kill();
2998
- }
2999
- };
3000
- } catch (err) {
3001
- relay.kill();
3002
- throw err;
3003
- }
3004
- }
3005
-
3006
- // src/capture.ts
3007
3635
  var EVENT_TRACKER_SCRIPT = `
3008
3636
  (function () {
3009
3637
  if (window.__qpTracking) return;
@@ -3115,16 +3743,6 @@ var EVENT_TRACKER_SCRIPT = `
3115
3743
  }, { passive: true, capture: true });
3116
3744
  }());
3117
3745
  `;
3118
- function waitForEnter2() {
3119
- return new Promise((resolve9) => {
3120
- const rl = (0, import_node_readline4.createInterface)({ input: process.stdin, output: process.stdout });
3121
- process.stdout.write(" Press Enter when done...\n");
3122
- rl.once("line", () => {
3123
- rl.close();
3124
- resolve9();
3125
- });
3126
- });
3127
- }
3128
3746
  async function launchCaptureChrome() {
3129
3747
  const debugPort = 9222;
3130
3748
  const chromePath = findWindowsChrome();
@@ -3150,9 +3768,9 @@ async function launchCaptureChrome() {
3150
3768
  return { browser, relay: probe.proc };
3151
3769
  }
3152
3770
  probe.proc.kill();
3153
- const winTemp = (0, import_node_child_process4.execSync)("cmd.exe /c echo %TEMP%", { encoding: "utf-8" }).trim().replace(/\r$/, "");
3771
+ const winTemp = (0, import_node_child_process5.execSync)("cmd.exe /c echo %TEMP%", { encoding: "utf-8" }).trim().replace(/\r$/, "");
3154
3772
  const userDataDir = `${winTemp}\\qp-capture`;
3155
- (0, import_node_child_process4.spawn)(
3773
+ (0, import_node_child_process5.spawn)(
3156
3774
  chromePath,
3157
3775
  [
3158
3776
  `--remote-debugging-port=${debugPort}`,
@@ -3223,7 +3841,7 @@ async function captureUserSession(url, _workDir) {
3223
3841
  return [title, parts.join(" | ")];
3224
3842
  }).catch(() => ["", ""]);
3225
3843
  startTime = Date.now();
3226
- await waitForEnter2();
3844
+ await waitForEnter();
3227
3845
  const durationMs = Date.now() - startTime;
3228
3846
  if (relay) {
3229
3847
  try {
@@ -3235,86 +3853,28 @@ async function captureUserSession(url, _workDir) {
3235
3853
  await context.close();
3236
3854
  await browser.close();
3237
3855
  }
3238
- return { events, durationMs, pageTitle, pageContext };
3239
- }
3240
-
3241
- // src/utils/logging.ts
3242
- var import_promises6 = require("fs/promises");
3243
- var import_node_path9 = require("path");
3244
- var LOG_FILE_NAME = "quickpeek.log";
3245
- var logFileInitialized = false;
3246
- var logFilePath = LOG_FILE_NAME;
3247
- async function writeLogEntry(entry) {
3248
- try {
3249
- if (!logFileInitialized) {
3250
- await (0, import_promises6.writeFile)(logFilePath, entry);
3251
- logFileInitialized = true;
3252
- } else {
3253
- await (0, import_promises6.appendFile)(logFilePath, entry);
3254
- }
3255
- } catch {
3256
- }
3257
- }
3258
- async function setLogFilePath(outputDir) {
3259
- logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
3260
- const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
3261
- ${"=".repeat(60)}
3262
-
3263
- `;
3264
- try {
3265
- await (0, import_promises6.writeFile)(logFilePath, header2);
3266
- logFileInitialized = true;
3267
- } catch {
3268
- }
3269
- }
3270
- async function logInfo(context, message) {
3271
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
3272
- `);
3273
- }
3274
- async function logDebug(context, message) {
3275
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
3276
- `);
3277
- }
3278
- async function logError(context, error) {
3279
- let errorMsg;
3280
- if (error instanceof Error) {
3281
- errorMsg = `${error.message}
3282
- ${error.stack}`;
3283
- } else if (typeof error === "object" && error !== null) {
3284
- errorMsg = JSON.stringify(error, null, 2);
3285
- } else {
3286
- errorMsg = String(error);
3287
- }
3288
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
3289
- ${errorMsg}
3290
-
3291
- `);
3292
- }
3293
- async function logStep(stepNum, action, details) {
3294
- const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
3295
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
3296
- `);
3856
+ return { events, durationMs, pageTitle, pageContext };
3297
3857
  }
3298
3858
 
3299
3859
  // src/commands/phases/video-phase.ts
3300
- var import_node_fs12 = require("fs");
3301
- var import_promises13 = require("fs/promises");
3860
+ var import_node_fs13 = require("fs");
3861
+ var import_promises14 = require("fs/promises");
3302
3862
  var import_node_path18 = require("path");
3303
3863
  init_lib();
3304
3864
 
3305
3865
  // src/record.ts
3306
- var import_node_fs10 = require("fs");
3307
- var import_promises10 = require("fs/promises");
3866
+ var import_node_fs11 = require("fs");
3867
+ var import_promises11 = require("fs/promises");
3308
3868
  var import_node_path15 = require("path");
3309
3869
  var import_chalk6 = __toESM(require("chalk"));
3310
3870
 
3311
3871
  // src/browser/actions.ts
3312
- var import_node_fs7 = require("fs");
3313
- var import_promises7 = require("fs/promises");
3872
+ var import_node_fs8 = require("fs");
3873
+ var import_promises9 = require("fs/promises");
3314
3874
  var import_node_path11 = require("path");
3315
3875
 
3316
3876
  // src/browser/effects.ts
3317
- var import_node_fs6 = require("fs");
3877
+ var import_node_fs7 = require("fs");
3318
3878
  var import_node_path10 = require("path");
3319
3879
  init_lib();
3320
3880
  function loadHighlightCSS() {
@@ -3325,7 +3885,7 @@ function loadHighlightCSS() {
3325
3885
  // src/browser/ → src/
3326
3886
  ];
3327
3887
  for (const p of candidates) {
3328
- 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");
3329
3889
  }
3330
3890
  return "";
3331
3891
  }
@@ -3525,6 +4085,10 @@ async function resolveElement(page, target) {
3525
4085
  await locator.waitFor({ state: "visible", timeout });
3526
4086
  return await locator.elementHandle({ timeout: 1e3 });
3527
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
+ }
3528
4092
  return await page.waitForSelector(target, { state: "visible", timeout });
3529
4093
  } catch {
3530
4094
  return null;
@@ -3649,7 +4213,14 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3649
4213
  }
3650
4214
  } else {
3651
4215
  const amount = direction === "up" ? -PAGE_SCROLL_PX : PAGE_SCROLL_PX;
3652
- await page.evaluate((y) => window.scrollBy(0, y), amount);
4216
+ const moved = await page.evaluate((y) => {
4217
+ const before = window.scrollY;
4218
+ window.scrollBy(0, y);
4219
+ return window.scrollY - before;
4220
+ }, amount);
4221
+ if (moved === 0) {
4222
+ throw new Error("scroll moved nothing: the page has no scroll headroom at this viewport");
4223
+ }
3653
4224
  }
3654
4225
  break;
3655
4226
  }
@@ -3667,7 +4238,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3667
4238
  if (!isPlaceholder) {
3668
4239
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
3669
4240
  const filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
3670
- if ((0, import_node_fs7.existsSync)(filePath)) {
4241
+ if ((0, import_node_fs8.existsSync)(filePath)) {
3671
4242
  await fileInput.setInputFiles(filePath);
3672
4243
  await page.waitForTimeout(2e3);
3673
4244
  break;
@@ -3784,7 +4355,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3784
4355
  break;
3785
4356
  case "image": {
3786
4357
  const imagePath = step.value ? (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value) : null;
3787
- if (imagePath && (0, import_node_fs7.existsSync)(imagePath)) {
4358
+ if (imagePath && (0, import_node_fs8.existsSync)(imagePath)) {
3788
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%;";
3789
4360
  if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
3790
4361
  }
@@ -3814,7 +4385,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3814
4385
  await pendingDownload;
3815
4386
  pendingDownload = null;
3816
4387
  }
3817
- if (lastDownloadPath && (0, import_node_fs7.existsSync)(lastDownloadPath)) {
4388
+ if (lastDownloadPath && (0, import_node_fs8.existsSync)(lastDownloadPath)) {
3818
4389
  const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
3819
4390
  if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
3820
4391
  }
@@ -3825,7 +4396,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3825
4396
  async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
3826
4397
  const { width, height } = config.video;
3827
4398
  const ext = filePath.split(".").pop()?.toLowerCase() || "";
3828
- const fileBuffer = await (0, import_promises7.readFile)(filePath);
4399
+ const fileBuffer = await (0, import_promises9.readFile)(filePath);
3829
4400
  const base64 = fileBuffer.toString("base64");
3830
4401
  const mimeTypes = {
3831
4402
  png: "image/png",
@@ -3854,64 +4425,6 @@ async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
3854
4425
  return false;
3855
4426
  }
3856
4427
 
3857
- // src/browser/launch.ts
3858
- var import_promises8 = require("fs/promises");
3859
- var import_playwright5 = require("playwright");
3860
- async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
3861
- const extensionPath = config.browser?.extension;
3862
- const viewport = { width: config.video.width, height: config.video.height };
3863
- const videoOpts = recordVideo ? { recordVideo } : {};
3864
- const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
3865
- let context;
3866
- let browser = null;
3867
- let chromeSession = null;
3868
- if (extensionPath) {
3869
- await (0, import_promises8.mkdir)(userDataDir, { recursive: true });
3870
- const persistentArgs = [
3871
- `--disable-extensions-except=${extensionPath}`,
3872
- `--load-extension=${extensionPath}`,
3873
- "--window-position=-32000,-32000",
3874
- "--window-size=1,1"
3875
- ];
3876
- const useWindowsChrome = isWSL() && tryEnableWSLInterop();
3877
- if (useWindowsChrome) {
3878
- try {
3879
- chromeSession = await launchWindowsChrome(extensionPath, userDataDir, { viewport, ...videoOpts });
3880
- const contexts = chromeSession.browser.contexts();
3881
- context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
3882
- } catch {
3883
- context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
3884
- headless: false,
3885
- args: persistentArgs,
3886
- viewport,
3887
- ...videoOpts,
3888
- acceptDownloads: true
3889
- });
3890
- }
3891
- } else {
3892
- context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
3893
- headless: false,
3894
- args: persistentArgs,
3895
- viewport,
3896
- ...videoOpts,
3897
- acceptDownloads: true
3898
- });
3899
- }
3900
- } else {
3901
- browser = await import_playwright5.chromium.launch({ headless: true });
3902
- context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
3903
- }
3904
- return { context, browser, chromeSession };
3905
- }
3906
- async function cleanupBrowserSession(session) {
3907
- if (session.chromeSession) {
3908
- await session.chromeSession.cleanup();
3909
- } else {
3910
- await session.context.close();
3911
- if (session.browser) await session.browser.close();
3912
- }
3913
- }
3914
-
3915
4428
  // src/record.ts
3916
4429
  init_lib();
3917
4430
 
@@ -3920,16 +4433,16 @@ var import_node_path13 = require("path");
3920
4433
  init_utils();
3921
4434
 
3922
4435
  // src/browser/step-validator.ts
3923
- var import_node_fs8 = require("fs");
4436
+ var import_node_fs9 = require("fs");
3924
4437
  var import_node_path12 = require("path");
3925
4438
  init_utils();
3926
4439
  function findMediaFile(filename, baseDir) {
3927
4440
  const name = (0, import_node_path12.basename)(filename);
3928
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")];
3929
4442
  for (const dir of searchDirs) {
3930
- if (!(0, import_node_fs8.existsSync)(dir)) continue;
4443
+ if (!(0, import_node_fs9.existsSync)(dir)) continue;
3931
4444
  try {
3932
- const files = (0, import_node_fs8.readdirSync)(dir);
4445
+ const files = (0, import_node_fs9.readdirSync)(dir);
3933
4446
  if (files.includes(name)) return (0, import_node_path12.join)(dir, name);
3934
4447
  const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
3935
4448
  if (match) return (0, import_node_path12.join)(dir, match);
@@ -4005,7 +4518,7 @@ function resolveMediaStep(step, baseDir, label, startTime) {
4005
4518
  return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
4006
4519
  }
4007
4520
  const specifiedPath = (0, import_node_path12.isAbsolute)(step.value) ? step.value : (0, import_node_path12.resolve)(baseDir, step.value);
4008
- 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 };
4009
4522
  const foundPath = findMediaFile(step.value, baseDir);
4010
4523
  if (foundPath) {
4011
4524
  step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -4127,7 +4640,7 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
4127
4640
  if (!isPlaceholder) {
4128
4641
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
4129
4642
  let filePath = (0, import_node_path12.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path12.resolve)(baseDir, normalizedPath);
4130
- if (!(0, import_node_fs8.existsSync)(filePath)) {
4643
+ if (!(0, import_node_fs9.existsSync)(filePath)) {
4131
4644
  const foundPath = findMediaFile(rawPath, baseDir);
4132
4645
  if (foundPath) {
4133
4646
  const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -4291,8 +4804,8 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
4291
4804
  }
4292
4805
 
4293
4806
  // src/browser/meta.ts
4294
- var import_node_fs9 = require("fs");
4295
- var import_promises9 = require("fs/promises");
4807
+ var import_node_fs10 = require("fs");
4808
+ var import_promises10 = require("fs/promises");
4296
4809
  var import_node_path14 = require("path");
4297
4810
  var RECORDING_FILE = "recording.mp4";
4298
4811
  var RECORDING_META_FILE = "recording.meta.json";
@@ -4301,9 +4814,9 @@ function getRecordingPath(outputDir) {
4301
4814
  }
4302
4815
  async function loadRecordingMeta(outputDir) {
4303
4816
  const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
4304
- if (!(0, import_node_fs9.existsSync)(metaPath)) return null;
4817
+ if (!(0, import_node_fs10.existsSync)(metaPath)) return null;
4305
4818
  try {
4306
- const content = await (0, import_promises9.readFile)(metaPath, "utf-8");
4819
+ const content = await (0, import_promises10.readFile)(metaPath, "utf-8");
4307
4820
  return JSON.parse(content);
4308
4821
  } catch {
4309
4822
  return null;
@@ -4311,7 +4824,7 @@ async function loadRecordingMeta(outputDir) {
4311
4824
  }
4312
4825
  async function saveRecordingMeta(outputDir, meta) {
4313
4826
  const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
4314
- await (0, import_promises9.writeFile)(metaPath, JSON.stringify(meta, null, 2));
4827
+ await (0, import_promises10.writeFile)(metaPath, JSON.stringify(meta, null, 2));
4315
4828
  }
4316
4829
  function canReuseRecording(plan, config, stepDurations, existingMeta) {
4317
4830
  if (!existingMeta) {
@@ -4381,14 +4894,14 @@ function formatStepLabel(step) {
4381
4894
  }
4382
4895
  async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
4383
4896
  const videoDir = (0, import_node_path15.join)(workDir, "video");
4384
- await (0, import_promises10.mkdir)(videoDir, { recursive: true });
4897
+ await (0, import_promises11.mkdir)(videoDir, { recursive: true });
4385
4898
  const getStepDuration = (stepNum) => {
4386
4899
  return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
4387
4900
  };
4388
4901
  const browserTask = task2("Starting browser recording");
4389
4902
  const recordingStartTime = Date.now();
4390
4903
  const userDataDir = (0, import_node_path15.join)(workDir, "browser-data");
4391
- const recordVideo = { dir: videoDir, size: { width: config.video.width, height: config.video.height } };
4904
+ const recordVideo = { dir: videoDir, size: recordingViewport(config) };
4392
4905
  const authState = findAuthState(plan.url);
4393
4906
  const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
4394
4907
  const page = context.pages()[0] || await context.newPage();
@@ -4409,6 +4922,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4409
4922
  let onMediaPage = firstStepIsMedia;
4410
4923
  const failures = [];
4411
4924
  const actualStepDurations = [];
4925
+ let pageClosedEarly = false;
4412
4926
  for (let i = 0; i < plan.steps.length; i++) {
4413
4927
  const step = plan.steps[i];
4414
4928
  const stepNum = i + 1;
@@ -4435,6 +4949,12 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4435
4949
  let targetEl = null;
4436
4950
  if (step.target) {
4437
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
+ }
4438
4958
  }
4439
4959
  if (step.zoom && step.zoom !== 1 && targetEl && !isExtensionPage) {
4440
4960
  const preZoomBox = await targetEl.boundingBox() ?? void 0;
@@ -4471,11 +4991,19 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4471
4991
  }
4472
4992
  const stepElapsed = Date.now() - stepStartTime;
4473
4993
  const waitTime = Math.max(0, stepDuration - stepElapsed);
4474
- if (waitTime > 0) await page.waitForTimeout(waitTime);
4475
4994
  const stepDelay = config.video.stepDelay ?? 500;
4476
4995
  const isLastStep = i === plan.steps.length - 1;
4477
- if (!isLastStep && stepDelay > 0) {
4478
- 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;
4479
5007
  }
4480
5008
  if (!isExtensionPage) {
4481
5009
  const shouldHighlight = step.highlight !== void 0 ? step.highlight : config.video.highlight;
@@ -4493,13 +5021,14 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4493
5021
  console.log(import_chalk6.default.yellow(` - ${f}`));
4494
5022
  }
4495
5023
  }
5024
+ while (actualStepDurations.length < plan.steps.length) actualStepDurations.push(0);
4496
5025
  const saveTask = task2("Saving video");
4497
5026
  const video = page.video();
4498
5027
  if (!video) throw new Error("No video recorder available");
4499
- await page.close();
5028
+ if (!pageClosedEarly) await page.close();
4500
5029
  const videoPath = await video.path();
4501
5030
  await cleanupBrowserSession({ context, browser, chromeSession });
4502
- if (!videoPath || !(0, import_node_fs10.existsSync)(videoPath)) {
5031
+ if (!videoPath || !(0, import_node_fs11.existsSync)(videoPath)) {
4503
5032
  throw new Error("Video file was not created");
4504
5033
  }
4505
5034
  saveTask.succeed("Video saved");
@@ -4508,8 +5037,8 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4508
5037
 
4509
5038
  // src/utils/video-pipeline.ts
4510
5039
  var import_video_kit6 = require("@spark-apps/video-kit");
4511
- var import_node_fs11 = require("fs");
4512
- var import_promises11 = require("fs/promises");
5040
+ var import_node_fs12 = require("fs");
5041
+ var import_promises12 = require("fs/promises");
4513
5042
  var import_node_path16 = require("path");
4514
5043
  var import_chalk7 = __toESM(require("chalk"));
4515
5044
  init_lib();
@@ -4560,7 +5089,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
4560
5089
  currentTime += audio.duration;
4561
5090
  }
4562
5091
  const assPath = (0, import_node_path16.join)(workDir, "subtitles.ass");
4563
- await (0, import_promises11.writeFile)(assPath, ass);
5092
+ await (0, import_promises12.writeFile)(assPath, ass);
4564
5093
  return assPath;
4565
5094
  }
4566
5095
  function buildVideoOverlays(steps, stepDurations, baseDir) {
@@ -4574,7 +5103,7 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
4574
5103
  const ext = step.value.split(".").pop()?.toLowerCase() || "";
4575
5104
  if (["mp4", "webm", "mov"].includes(ext)) {
4576
5105
  const mediaPath = (0, import_node_path16.isAbsolute)(step.value) ? step.value : (0, import_node_path16.resolve)(baseDir, step.value);
4577
- if ((0, import_node_fs11.existsSync)(mediaPath)) {
5106
+ if ((0, import_node_fs12.existsSync)(mediaPath)) {
4578
5107
  const partial = parsePartial(step.partial);
4579
5108
  overlays.push({
4580
5109
  path: mediaPath,
@@ -4603,13 +5132,13 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
4603
5132
  process.exit(0);
4604
5133
  }
4605
5134
  const composeTask = task("Building final video");
4606
- await (0, import_promises11.mkdir)(config.output.dir, { recursive: true });
5135
+ await (0, import_promises12.mkdir)(config.output.dir, { recursive: true });
4607
5136
  const suffix = config.lang ? `-${config.lang}` : "";
4608
5137
  const finalPath = (0, import_node_path16.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
4609
5138
  const videoDuration = await getMediaDuration(videoPath) / 1e3;
4610
- const hasVoiceover = audioPath && (0, import_node_fs11.existsSync)(audioPath);
5139
+ const hasVoiceover = audioPath && (0, import_node_fs12.existsSync)(audioPath);
4611
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) : "";
4612
- const hasMusic = musicPath && (0, import_node_fs11.existsSync)(musicPath);
5141
+ const hasMusic = musicPath && (0, import_node_fs12.existsSync)(musicPath);
4613
5142
  try {
4614
5143
  await composeVideo({
4615
5144
  videoPath,
@@ -4674,19 +5203,20 @@ var import_chalk9 = __toESM(require("chalk"));
4674
5203
  init_lib();
4675
5204
 
4676
5205
  // src/plan.ts
4677
- var import_promises12 = require("fs/promises");
5206
+ var import_promises13 = require("fs/promises");
4678
5207
  var import_node_path17 = require("path");
4679
5208
  var import_chalk8 = __toESM(require("chalk"));
4680
- var import_playwright6 = require("playwright");
5209
+ var import_playwright5 = require("playwright");
4681
5210
  init_lib();
5211
+ init_json_repair();
4682
5212
  async function crawlExtension(url, extensionPath, workDir, task2) {
4683
5213
  const browserTask = task2("Launching browser with extension");
4684
5214
  const userDataDir = (0, import_node_path17.join)(workDir, ".browser-data");
4685
- await (0, import_promises12.mkdir)(userDataDir, { recursive: true });
5215
+ await (0, import_promises13.mkdir)(userDataDir, { recursive: true });
4686
5216
  let context;
4687
5217
  let chromeSession = null;
4688
5218
  const useWindowsChrome = isWSL() && tryEnableWSLInterop();
4689
- const launchPersistent = () => import_playwright6.chromium.launchPersistentContext(userDataDir, {
5219
+ const launchPersistent = () => import_playwright5.chromium.launchPersistentContext(userDataDir, {
4690
5220
  headless: false,
4691
5221
  args: [
4692
5222
  `--disable-extensions-except=${extensionPath}`,
@@ -4767,7 +5297,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
4767
5297
  else if (el.type === "select" || el.type === "select-one") action = "select";
4768
5298
  else if (el.type === "checkbox") action = "check";
4769
5299
  else if (["text", "email", "password", "number", "search", "url", "tel"].includes(el.type)) action = "type";
4770
- 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}"`;
4771
5301
  return {
4772
5302
  type: el.type === "button" || el.type === "link" ? el.type : "input",
4773
5303
  text: el.text,
@@ -4779,43 +5309,19 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
4779
5309
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
4780
5310
  return { title, elements };
4781
5311
  }
4782
- var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
4783
- var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
4784
- The JSON should have: url, title, description, steps array.
4785
- Each step has: id (number), action, caption, and optionally target, value.
4786
-
4787
- Malformed content:
4788
- ${content}`;
4789
5312
  async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
4790
- try {
4791
- return JSON.parse(content);
4792
- } catch {
4793
- const fixTask = task2("Fixing malformed JSON");
4794
- try {
4795
- const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
4796
- const jsonMatch = fixed.match(/\{[\s\S]*\}/);
4797
- if (!jsonMatch) throw new Error("No JSON in fix");
4798
- const plan = JSON.parse(jsonMatch[0]);
4799
- fixTask.succeed("JSON fixed by AI");
4800
- return plan;
4801
- } catch (err) {
4802
- await logError2("JSON Fix", err);
4803
- fixTask.fail("Could not fix JSON");
4804
- throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
4805
- }
4806
- }
5313
+ return parseAIJsonWithRepair(content, { logError: logError2, task: task2, userEmail, tier });
4807
5314
  }
4808
- async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free") {
4809
- const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2, findAuthState(url));
5315
+ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
5316
+ const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
4810
5317
  const aiTask = task2("Generating plan with AI");
4811
5318
  const langName = getLanguageName(lang);
4812
5319
  const systemPrompt = buildSystemPrompt(maxSteps);
4813
- const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps);
5320
+ const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
4814
5321
  try {
4815
- const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
4816
- const jsonMatch = content.match(/\{[\s\S]*\}/);
4817
- if (!jsonMatch) throw new Error("No JSON found in response");
4818
- 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");
4819
5325
  if (lang !== void 0) plan.lang = lang;
4820
5326
  plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
4821
5327
  ...s,
@@ -4832,14 +5338,14 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
4832
5338
  }
4833
5339
  }
4834
5340
  async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier = "free") {
4835
- const content = await (0, import_promises12.readFile)(planPath, "utf-8");
5341
+ const content = await (0, import_promises13.readFile)(planPath, "utf-8");
4836
5342
  const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
4837
5343
  if (!plan.url || !plan.steps || !Array.isArray(plan.steps)) {
4838
5344
  throw new Error("Invalid plan: missing url or steps");
4839
5345
  }
4840
5346
  const renumberTask = task2("Validating plan structure");
4841
5347
  const originalCount = plan.steps.length;
4842
- const INVALID_TARGETS = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
5348
+ const INVALID_TARGETS2 = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
4843
5349
  const isValidStep = (s) => {
4844
5350
  if (!s?.action) return false;
4845
5351
  if (s.action === "navigate") {
@@ -4848,7 +5354,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
4848
5354
  }
4849
5355
  if (["click", "type", "select", "check", "upload", "drag"].includes(s.action)) {
4850
5356
  if (!s.target) return false;
4851
- if (INVALID_TARGETS.some((pattern) => pattern.test(s.target))) return false;
5357
+ if (INVALID_TARGETS2.some((pattern) => pattern.test(s.target))) return false;
4852
5358
  }
4853
5359
  if (s.action === "upload") {
4854
5360
  if (!s.target?.includes("file") && !s.target?.includes("input")) return false;
@@ -4865,7 +5371,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
4865
5371
  renumberTask.fail("No valid steps");
4866
5372
  throw new Error("Plan has no valid steps");
4867
5373
  }
4868
- await (0, import_promises12.writeFile)(planPath, JSON.stringify(plan, null, 2));
5374
+ await (0, import_promises13.writeFile)(planPath, JSON.stringify(plan, null, 2));
4869
5375
  const removed = originalCount - plan.steps.length;
4870
5376
  renumberTask.succeed(
4871
5377
  removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
@@ -4980,6 +5486,21 @@ function getOutputDirFromUrl(url) {
4980
5486
 
4981
5487
  // src/commands/phases/video-phase.ts
4982
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
+ }
4983
5504
  async function runVideoPhase(params) {
4984
5505
  let { stepAudio } = params;
4985
5506
  const { plan, config, voiceoverPath, workDir, captions, userTier } = params;
@@ -4998,7 +5519,7 @@ async function runVideoPhase(params) {
4998
5519
  mediaDuration = (trimEnd - trimStart2) * 1e3;
4999
5520
  } else {
5000
5521
  const mediaPath = (0, import_node_path18.resolve)(s.value);
5001
- if ((0, import_node_fs12.existsSync)(mediaPath)) {
5522
+ if ((0, import_node_fs13.existsSync)(mediaPath)) {
5002
5523
  try {
5003
5524
  const fullDuration = await getMediaDuration(mediaPath);
5004
5525
  const start = (trimStart2 ?? 0) * 1e3;
@@ -5010,7 +5531,7 @@ async function runVideoPhase(params) {
5010
5531
  }
5011
5532
  } else {
5012
5533
  const mediaPath = (0, import_node_path18.resolve)(s.value);
5013
- if ((0, import_node_fs12.existsSync)(mediaPath)) {
5534
+ if ((0, import_node_fs13.existsSync)(mediaPath)) {
5014
5535
  try {
5015
5536
  mediaDuration = await getMediaDuration(mediaPath);
5016
5537
  } catch {
@@ -5023,7 +5544,7 @@ async function runVideoPhase(params) {
5023
5544
  })
5024
5545
  );
5025
5546
  const recordingPath = getRecordingPath(config.output.dir);
5026
- const hasExistingRecording = existingRecordingMeta && (0, import_node_fs12.existsSync)(recordingPath);
5547
+ const hasExistingRecording = existingRecordingMeta && (0, import_node_fs13.existsSync)(recordingPath);
5027
5548
  let videoPath = recordingPath;
5028
5549
  const firstStep = plan.steps[0];
5029
5550
  const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
@@ -5032,7 +5553,7 @@ async function runVideoPhase(params) {
5032
5553
  header("Record");
5033
5554
  const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
5034
5555
  stepAudio = await alignVoiceoverToRecording(stepAudio, result.actualStepDurations, workDir, config.output.dir);
5035
- await (0, import_promises13.copyFile)(result.videoPath, recordingPath);
5556
+ await (0, import_promises14.copyFile)(result.videoPath, recordingPath);
5036
5557
  const meta = buildRecordingMeta(plan, config, result.actualStepDurations);
5037
5558
  meta.pageLoadTime = result.pageLoadTime;
5038
5559
  await saveRecordingMeta(config.output.dir, meta);
@@ -5084,7 +5605,7 @@ async function runVideoPhase(params) {
5084
5605
  const transitionTask = task("Applying transitions");
5085
5606
  try {
5086
5607
  const transitionedPath = (0, import_node_path18.join)(workDir, "transitioned.mp4");
5087
- const videoDuration = alignedStepDurations.reduce((a, b) => a + b, 0) / 1e3;
5608
+ const videoDuration = await getMediaDuration(videoPath) / 1e3;
5088
5609
  await applyTransitions({
5089
5610
  inputVideo: videoPath,
5090
5611
  outputVideo: transitionedPath,
@@ -5102,30 +5623,24 @@ async function runVideoPhase(params) {
5102
5623
  const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
5103
5624
  let videoToCompose = videoPath;
5104
5625
  let mainContentDuration;
5105
- if (userTier === "free") {
5106
- const scriptDir = getScriptDir();
5107
- let outroPath = (0, import_node_path18.join)(scriptDir, "assets", "qp.wm");
5108
- if (!(0, import_node_fs12.existsSync)(outroPath)) {
5109
- outroPath = (0, import_node_path18.join)(scriptDir, "..", "dist", "assets", "qp.wm");
5110
- }
5111
- if ((0, import_node_fs12.existsSync)(outroPath)) {
5112
- try {
5113
- const mainDuration = await getMediaDuration(videoPath);
5114
- mainContentDuration = mainDuration / 1e3 - trimStart;
5115
- const combinedPath = (0, import_node_path18.join)(workDir, "combined-with-outro.mp4");
5116
- await concatVideoWithOutro({
5117
- inputPath: videoPath,
5118
- outroPath,
5119
- outputPath: combinedPath,
5120
- width: config.video.width,
5121
- height: config.video.height,
5122
- preset: config.video.preset
5123
- });
5124
- videoToCompose = combinedPath;
5125
- } catch (err) {
5126
- await logError("Concat Outro", err);
5127
- mainContentDuration = void 0;
5128
- }
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;
5129
5644
  }
5130
5645
  }
5131
5646
  let finalPath = await composeAndFinish(
@@ -5159,7 +5674,7 @@ async function runVideoPhase(params) {
5159
5674
  }
5160
5675
 
5161
5676
  // src/commands/phases/voiceover-phase.ts
5162
- var import_node_fs13 = require("fs");
5677
+ var import_node_fs14 = require("fs");
5163
5678
  var import_node_path19 = require("path");
5164
5679
  var import_chalk10 = __toESM(require("chalk"));
5165
5680
  init_lib();
@@ -5169,9 +5684,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5169
5684
  const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
5170
5685
  for (const file of filesToDelete) {
5171
5686
  const filePath = (0, import_node_path19.join)(config.output.dir, file);
5172
- if ((0, import_node_fs13.existsSync)(filePath)) {
5173
- const { unlink: unlink3 } = await import("fs/promises");
5174
- 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(() => {
5175
5690
  });
5176
5691
  }
5177
5692
  }
@@ -5180,7 +5695,12 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5180
5695
  const voice = resolveVoice(config);
5181
5696
  const existingMeta = await loadVoiceoverMeta(config.output.dir);
5182
5697
  const voiceoverFile = (0, import_node_path19.join)(config.output.dir, "voiceover.wav");
5183
- 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;
5184
5704
  const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
5185
5705
  let voiceoverPath;
5186
5706
  let stepAudio;
@@ -5270,7 +5790,8 @@ async function captureCommand(url, lang) {
5270
5790
  lang,
5271
5791
  logError,
5272
5792
  userData.email,
5273
- captureResult.pageContext
5793
+ captureResult.pageContext,
5794
+ userData.tier ?? "free"
5274
5795
  );
5275
5796
  narrateTask.succeed(`${steps.length} captions generated`);
5276
5797
  } catch (err) {
@@ -5314,16 +5835,172 @@ async function captureCommand(url, lang) {
5314
5835
  console.log(import_chalk11.default.cyan(` ${finalPath}`));
5315
5836
  }
5316
5837
 
5317
- // src/commands/init-cmd.ts
5318
- var import_node_fs14 = require("fs");
5319
- var import_promises14 = require("fs/promises");
5838
+ // src/commands/edit-cmd.ts
5839
+ var import_node_fs15 = require("fs");
5320
5840
  var import_node_path21 = require("path");
5321
5841
  var import_chalk12 = __toESM(require("chalk"));
5842
+
5843
+ // src/lib/plan-validation.ts
5844
+ var INVALID_TARGETS = [
5845
+ /\.png$/i,
5846
+ /\.jpg$/i,
5847
+ /\.gif$/i,
5848
+ /\.webp$/i,
5849
+ /\.svg$/i,
5850
+ /sprite_sheet/i,
5851
+ /downloaded/i
5852
+ ];
5853
+ var ACTIONS_REQUIRING_TARGET = ["click", "type", "select", "check", "drag"];
5854
+ function isFilePath(value) {
5855
+ if (!value) return false;
5856
+ return /\.\w{2,5}$/.test(value) && (/[/\\]/.test(value) || /^[a-zA-Z]:/.test(value));
5857
+ }
5858
+ function isInvalidTarget(target) {
5859
+ return INVALID_TARGETS.some((pattern) => pattern.test(target));
5860
+ }
5861
+ function validateStep(step) {
5862
+ if (!step?.action) {
5863
+ return "Missing action";
5864
+ }
5865
+ if (step.action === "navigate") {
5866
+ if (!step.target) return "Navigate requires a URL target";
5867
+ if (!step.target.startsWith("http://") && !step.target.startsWith("https://")) {
5868
+ return "Navigate target must be a valid URL";
5869
+ }
5870
+ }
5871
+ if (step.action === "media") {
5872
+ if (!step.mediaFile && !isFilePath(step.value)) {
5873
+ return "Media step requires a file (image or video)";
5874
+ }
5875
+ }
5876
+ if (step.action === "upload") {
5877
+ if (!step.mediaFile && !isFilePath(step.value)) {
5878
+ return "Upload step requires a file";
5879
+ }
5880
+ }
5881
+ if (ACTIONS_REQUIRING_TARGET.includes(step.action)) {
5882
+ if (!step.target) return `${step.action} requires a target`;
5883
+ if (isInvalidTarget(step.target)) return `Invalid target pattern: ${step.target}`;
5884
+ }
5885
+ return null;
5886
+ }
5887
+
5888
+ // src/lib/plan-file.ts
5889
+ var MAX_HANDOFF_URL_CHARS = 32e3;
5890
+ function serializePlan(plan) {
5891
+ return `${JSON.stringify(plan, null, 2)}
5892
+ `;
5893
+ }
5894
+ function parsePlanText(text) {
5895
+ let raw;
5896
+ try {
5897
+ raw = JSON.parse(text);
5898
+ } catch (e) {
5899
+ return { ok: false, error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
5900
+ }
5901
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5902
+ return { ok: false, error: "A plan must be a JSON object." };
5903
+ }
5904
+ const candidate = raw;
5905
+ if (typeof candidate.url !== "string" || !candidate.url.trim()) {
5906
+ return { ok: false, error: "A plan needs a `url` to record." };
5907
+ }
5908
+ if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
5909
+ return { ok: false, error: "A plan needs at least one step." };
5910
+ }
5911
+ const steps = [];
5912
+ for (const [index, step] of candidate.steps.entries()) {
5913
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
5914
+ return { ok: false, error: `Step ${index + 1} is not an object.` };
5915
+ }
5916
+ const typed = step;
5917
+ if (typeof typed.action !== "string" || !typed.action.trim()) {
5918
+ return { ok: false, error: `Step ${index + 1} has no action.` };
5919
+ }
5920
+ const failure = validateStep(typed);
5921
+ if (failure) return { ok: false, error: `Step ${index + 1}: ${failure}` };
5922
+ steps.push({ ...typed, id: typed.id ?? index + 1 });
5923
+ }
5924
+ return {
5925
+ ok: true,
5926
+ plan: {
5927
+ url: candidate.url,
5928
+ title: candidate.title ?? "",
5929
+ description: candidate.description ?? "",
5930
+ ...candidate.lang ? { lang: candidate.lang } : {},
5931
+ steps
5932
+ }
5933
+ };
5934
+ }
5935
+ function toBase64Url(text) {
5936
+ const bytes = new TextEncoder().encode(text);
5937
+ const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
5938
+ const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(text, "utf8").toString("base64");
5939
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5940
+ }
5941
+ function encodePlan(plan) {
5942
+ return toBase64Url(serializePlan(plan));
5943
+ }
5944
+ function planEditorUrl(baseUrl, plan) {
5945
+ const url = `${baseUrl.replace(/\/$/, "")}/edit#plan=${encodePlan(plan)}`;
5946
+ return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
5947
+ }
5948
+
5949
+ // src/commands/edit-cmd.ts
5950
+ var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
5951
+ function findPlanFile(dir, lang) {
5952
+ if (lang) {
5953
+ const exact = (0, import_node_path21.join)(dir, `plan-${lang}.json`);
5954
+ return (0, import_node_fs15.existsSync)(exact) ? exact : null;
5955
+ }
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));
5958
+ const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
5959
+ return preferred ? (0, import_node_path21.join)(dir, preferred) : null;
5960
+ }
5961
+ async function editCommand(url, lang) {
5962
+ console.log();
5963
+ showLogo();
5964
+ const dir = getOutputDirFromUrl(url);
5965
+ const planPath = findPlanFile(dir, lang);
5966
+ if (!planPath) {
5967
+ console.log(import_chalk12.default.red(` No plan found in ${dir}`));
5968
+ console.log(import_chalk12.default.dim(` Record the demo first: `) + import_chalk12.default.cyan(`qp ${url}`));
5969
+ console.log();
5970
+ return;
5971
+ }
5972
+ const parsed = parsePlanText((0, import_node_fs15.readFileSync)(planPath, "utf8"));
5973
+ if (!parsed.ok) {
5974
+ console.log(import_chalk12.default.red(` ${planPath} cannot be opened: ${parsed.error}`));
5975
+ console.log();
5976
+ return;
5977
+ }
5978
+ const base = editorBaseUrl();
5979
+ const editorUrl = planEditorUrl(base, parsed.plan);
5980
+ if (!editorUrl) {
5981
+ console.log(import_chalk12.default.yellow(" This plan is too long to carry in a link."));
5982
+ console.log(import_chalk12.default.dim(` Open ${base}/edit and load `) + import_chalk12.default.cyan(formatClickablePath(planPath)));
5983
+ console.log();
5984
+ await openInBrowser(`${base}/edit`);
5985
+ return;
5986
+ }
5987
+ await openInBrowser(editorUrl);
5988
+ console.log(import_chalk12.default.green(` \u2713 Opened ${parsed.plan.steps.length} steps in the editor`));
5989
+ console.log(import_chalk12.default.dim(` ${formatClickablePath(planPath)}`));
5990
+ console.log(import_chalk12.default.dim(" Save from the editor over this file, then record again."));
5991
+ console.log();
5992
+ }
5993
+
5994
+ // src/commands/init-cmd.ts
5995
+ var import_node_fs16 = require("fs");
5996
+ var import_promises15 = require("fs/promises");
5997
+ var import_node_path22 = require("path");
5998
+ var import_chalk13 = __toESM(require("chalk"));
5322
5999
  init_lib();
5323
6000
  async function initCommand() {
5324
- const configPath = (0, import_node_path21.join)(process.cwd(), CONFIG_FILE);
5325
- if ((0, import_node_fs14.existsSync)(configPath)) {
5326
- console.log(import_chalk12.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
6001
+ const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
6002
+ if ((0, import_node_fs16.existsSync)(configPath)) {
6003
+ console.log(import_chalk13.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
5327
6004
  return;
5328
6005
  }
5329
6006
  const defaultConfig = {
@@ -5346,32 +6023,32 @@ async function initCommand() {
5346
6023
  volume: 0.3
5347
6024
  }
5348
6025
  };
5349
- await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
6026
+ await (0, import_promises15.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
5350
6027
  `);
5351
- console.log(import_chalk12.default.green(` \u2714 Created ${CONFIG_FILE}`));
5352
- console.log(import_chalk12.default.dim(` Edit it or use: qp config <key> <value>`));
6028
+ console.log(import_chalk13.default.green(` \u2714 Created ${CONFIG_FILE}`));
6029
+ console.log(import_chalk13.default.dim(` Edit it or use: qp config <key> <value>`));
5353
6030
  }
5354
6031
  async function configCommand(key, value) {
5355
- const configPath = (0, import_node_path21.join)(process.cwd(), CONFIG_FILE);
6032
+ const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
5356
6033
  if (!key) {
5357
- if (!(0, import_node_fs14.existsSync)(configPath)) {
5358
- console.log(import_chalk12.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
6034
+ if (!(0, import_node_fs16.existsSync)(configPath)) {
6035
+ console.log(import_chalk13.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
5359
6036
  return;
5360
6037
  }
5361
- const content = await (0, import_promises14.readFile)(configPath, "utf-8");
5362
- console.log(import_chalk12.default.cyan(` ${CONFIG_FILE}:`));
6038
+ const content = await (0, import_promises15.readFile)(configPath, "utf-8");
6039
+ console.log(import_chalk13.default.cyan(` ${CONFIG_FILE}:`));
5363
6040
  console.log(content);
5364
6041
  return;
5365
6042
  }
5366
6043
  if (!value) {
5367
- console.log(import_chalk12.default.red(` Missing value for ${key}`));
5368
- console.log(import_chalk12.default.dim(` Usage: qp config ${key} <value>`));
6044
+ console.log(import_chalk13.default.red(` Missing value for ${key}`));
6045
+ console.log(import_chalk13.default.dim(` Usage: qp config ${key} <value>`));
5369
6046
  return;
5370
6047
  }
5371
6048
  let config = {};
5372
- if ((0, import_node_fs14.existsSync)(configPath)) {
6049
+ if ((0, import_node_fs16.existsSync)(configPath)) {
5373
6050
  try {
5374
- config = JSON.parse(await (0, import_promises14.readFile)(configPath, "utf-8"));
6051
+ config = JSON.parse(await (0, import_promises15.readFile)(configPath, "utf-8"));
5375
6052
  } catch {
5376
6053
  }
5377
6054
  }
@@ -5393,42 +6070,42 @@ async function configCommand(key, value) {
5393
6070
  }
5394
6071
  obj[keys[keys.length - 1]] = parsedValue;
5395
6072
  }
5396
- await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
6073
+ await (0, import_promises15.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
5397
6074
  `);
5398
- console.log(import_chalk12.default.green(` \u2714 Set ${key} = ${value}`));
6075
+ console.log(import_chalk13.default.green(` \u2714 Set ${key} = ${value}`));
5399
6076
  }
5400
6077
 
5401
6078
  // src/commands/login-cmd.ts
5402
- var import_chalk13 = __toESM(require("chalk"));
6079
+ var import_chalk14 = __toESM(require("chalk"));
5403
6080
  init_lib();
5404
6081
  async function loginCommand() {
5405
6082
  console.log();
5406
6083
  showLogo();
5407
6084
  const existing = await loadUserData();
5408
6085
  if (existing.email && existing.verified) {
5409
- console.log(import_chalk13.default.dim(` Already signed in as ${existing.email}`));
5410
- console.log(import_chalk13.default.dim(" Run ") + import_chalk13.default.cyan("qp logout") + import_chalk13.default.dim(" first to switch accounts."));
6086
+ console.log(import_chalk14.default.dim(` Already signed in as ${existing.email}`));
6087
+ console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp logout") + import_chalk14.default.dim(" first to switch accounts."));
5411
6088
  console.log();
5412
6089
  try {
5413
6090
  const status = await getStatusCached(existing.email);
5414
6091
  const tier2 = await getTierByEmail(existing.email);
5415
- const tierLabel2 = tier2 === "pro" ? import_chalk13.default.green("Pro") : "Free";
6092
+ const tierLabel2 = tier2 === "pro" ? import_chalk14.default.green("Pro") : "Free";
5416
6093
  console.log(` ${tierLabel2} plan`);
5417
6094
  for (const line of formatUsageLines(status, tier2)) {
5418
- console.log(import_chalk13.default.dim(` ${line}`));
6095
+ console.log(import_chalk14.default.dim(` ${line}`));
5419
6096
  }
5420
6097
  } catch {
5421
6098
  }
5422
6099
  console.log();
5423
6100
  return;
5424
6101
  }
5425
- console.log(import_chalk13.default.white(" Sign in with your Google account to track usage"));
5426
- console.log(import_chalk13.default.white(" and unlock Pro features."));
6102
+ console.log(import_chalk14.default.white(" Sign in with your Google account to track usage"));
6103
+ console.log(import_chalk14.default.white(" and unlock Pro features."));
5427
6104
  const email = await browserLogin();
5428
6105
  if (!email) {
5429
6106
  console.log();
5430
- console.log(import_chalk13.default.yellow(" Sign-in cancelled or timed out."));
5431
- console.log(import_chalk13.default.dim(" Run ") + import_chalk13.default.cyan("qp login") + import_chalk13.default.dim(" to try again."));
6107
+ console.log(import_chalk14.default.yellow(" Sign-in cancelled or timed out."));
6108
+ console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp login") + import_chalk14.default.dim(" to try again."));
5432
6109
  console.log();
5433
6110
  return;
5434
6111
  }
@@ -5439,8 +6116,8 @@ async function loginCommand() {
5439
6116
  }
5440
6117
  await saveUserData({ email, verified: true, tier });
5441
6118
  console.log();
5442
- const tierLabel = tier === "pro" ? import_chalk13.default.green("Pro") : "Free";
5443
- console.log(import_chalk13.default.green(` \u2713 Signed in as ${email}`));
6119
+ const tierLabel = tier === "pro" ? import_chalk14.default.green("Pro") : "Free";
6120
+ console.log(import_chalk14.default.green(` \u2713 Signed in as ${email}`));
5444
6121
  console.log(` ${tierLabel} plan`);
5445
6122
  console.log();
5446
6123
  }
@@ -5448,46 +6125,46 @@ async function logoutCommand() {
5448
6125
  console.log();
5449
6126
  const userData = await loadUserData();
5450
6127
  if (!userData.email) {
5451
- console.log(import_chalk13.default.dim(" Not signed in."));
6128
+ console.log(import_chalk14.default.dim(" Not signed in."));
5452
6129
  console.log();
5453
6130
  return;
5454
6131
  }
5455
6132
  const email = userData.email;
5456
6133
  await saveUserData({ email: "", verified: false, tier: "free" });
5457
- console.log(import_chalk13.default.green(` \u2713 Signed out (${email})`));
6134
+ console.log(import_chalk14.default.green(` \u2713 Signed out (${email})`));
5458
6135
  console.log();
5459
6136
  }
5460
6137
 
5461
6138
  // src/commands/record-cmd.ts
5462
- var import_node_fs17 = require("fs");
5463
- var import_promises17 = require("fs/promises");
5464
- var import_node_path24 = require("path");
5465
- var import_chalk18 = __toESM(require("chalk"));
6139
+ var import_node_fs19 = require("fs");
6140
+ var import_promises18 = require("fs/promises");
6141
+ var import_node_path25 = require("path");
6142
+ var import_chalk19 = __toESM(require("chalk"));
5466
6143
  init_lib();
5467
6144
 
5468
6145
  // src/commands/phases/auth-phase.ts
5469
- var import_chalk15 = __toESM(require("chalk"));
6146
+ var import_chalk16 = __toESM(require("chalk"));
5470
6147
  init_lib();
5471
6148
 
5472
6149
  // src/commands/utils/auth-helpers.ts
5473
- var import_chalk14 = __toESM(require("chalk"));
6150
+ var import_chalk15 = __toESM(require("chalk"));
5474
6151
  init_lib();
5475
6152
  async function handleRegistrationGate() {
5476
6153
  console.log();
5477
- console.log(import_chalk14.default.yellow(" \u{1F389} You've made 5 free videos!"));
5478
- console.log(import_chalk14.default.white(" Sign in to continue creating videos."));
6154
+ console.log(import_chalk15.default.yellow(" \u{1F389} You've made 5 free videos!"));
6155
+ console.log(import_chalk15.default.white(" Sign in to continue creating videos."));
5479
6156
  console.log();
5480
- console.log(import_chalk14.default.dim(" Benefits of signing in:"));
5481
- console.log(import_chalk14.default.dim(" \u2022 20 more videos per month"));
5482
- console.log(import_chalk14.default.dim(" \u2022 Keep your video history"));
5483
- console.log(import_chalk14.default.dim(" \u2022 Upgrade to Pro anytime"));
6157
+ console.log(import_chalk15.default.dim(" Benefits of signing in:"));
6158
+ console.log(import_chalk15.default.dim(" \u2022 20 more videos per month"));
6159
+ console.log(import_chalk15.default.dim(" \u2022 Keep your video history"));
6160
+ console.log(import_chalk15.default.dim(" \u2022 Upgrade to Pro anytime"));
5484
6161
  let email = await browserLogin();
5485
6162
  if (!email) {
5486
6163
  console.log();
5487
6164
  email = await promptInput("Email: ");
5488
6165
  }
5489
6166
  if (!email || !email.includes("@")) {
5490
- console.log(import_chalk14.default.red(" Invalid email. Run qp again to try again."));
6167
+ console.log(import_chalk15.default.red(" Invalid email. Run qp again to try again."));
5491
6168
  return null;
5492
6169
  }
5493
6170
  const registerTask = task("Sending verification email");
@@ -5502,10 +6179,10 @@ async function handleRegistrationGate() {
5502
6179
  }
5503
6180
  registerTask.succeed("Check your email!");
5504
6181
  console.log();
5505
- console.log(import_chalk14.default.cyan(" \u{1F4E7} Verification email sent to:"));
5506
- console.log(import_chalk14.default.white(` ${email}`));
6182
+ console.log(import_chalk15.default.cyan(" \u{1F4E7} Verification email sent to:"));
6183
+ console.log(import_chalk15.default.white(` ${email}`));
5507
6184
  console.log();
5508
- console.log(import_chalk14.default.dim(" Click the link in the email, then press Enter to continue..."));
6185
+ console.log(import_chalk15.default.dim(" Click the link in the email, then press Enter to continue..."));
5509
6186
  await promptInput("");
5510
6187
  const verifyTask = task("Checking verification");
5511
6188
  const verified = await isVerified(email);
@@ -5515,10 +6192,10 @@ async function handleRegistrationGate() {
5515
6192
  }
5516
6193
  verifyTask.fail("Not verified yet");
5517
6194
  console.log();
5518
- console.log(import_chalk14.default.yellow(" Email not verified. Check your inbox and spam folder."));
5519
- console.log(import_chalk14.default.dim(" Run qp again after clicking the verification link."));
6195
+ console.log(import_chalk15.default.yellow(" Email not verified. Check your inbox and spam folder."));
6196
+ console.log(import_chalk15.default.dim(" Run qp again after clicking the verification link."));
5520
6197
  console.log();
5521
- console.log(import_chalk14.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
6198
+ console.log(import_chalk15.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
5522
6199
  console.log();
5523
6200
  return { email, verified: false };
5524
6201
  }
@@ -5546,7 +6223,7 @@ async function runAuthPhase(input) {
5546
6223
  } else {
5547
6224
  verifyTask.fail("Email not verified");
5548
6225
  console.log();
5549
- console.log(import_chalk15.default.yellow(" Check your inbox for the verification email."));
6226
+ console.log(import_chalk16.default.yellow(" Check your inbox for the verification email."));
5550
6227
  console.log();
5551
6228
  const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
5552
6229
  if (input2.toLowerCase() === "exit") {
@@ -5571,8 +6248,8 @@ async function runAuthPhase(input) {
5571
6248
  registerTask.succeed("Check your email!");
5572
6249
  await saveUserData({ email: newEmail, verified: false });
5573
6250
  console.log();
5574
- console.log(import_chalk15.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
5575
- console.log(import_chalk15.default.dim(" Click the link in the email, then run qp again."));
6251
+ console.log(import_chalk16.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
6252
+ console.log(import_chalk16.default.dim(" Click the link in the email, then run qp again."));
5576
6253
  console.log();
5577
6254
  gracefulExit();
5578
6255
  return null;
@@ -5588,7 +6265,7 @@ async function runAuthPhase(input) {
5588
6265
  } else {
5589
6266
  recheckTask.fail("Still not verified");
5590
6267
  console.log();
5591
- console.log(import_chalk15.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
6268
+ console.log(import_chalk16.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
5592
6269
  console.log();
5593
6270
  gracefulExit();
5594
6271
  return null;
@@ -5615,8 +6292,8 @@ async function runAuthPhase(input) {
5615
6292
  } else {
5616
6293
  const remaining = FREE_VIDEO_LIMIT - localVideoCount;
5617
6294
  console.log();
5618
- console.log(import_chalk15.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
5619
- console.log(import_chalk15.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
6295
+ console.log(import_chalk16.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
6296
+ console.log(import_chalk16.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
5620
6297
  userEmail = await setupUserEmail();
5621
6298
  let registeredVerified = false;
5622
6299
  try {
@@ -5635,18 +6312,18 @@ async function runAuthPhase(input) {
5635
6312
  }
5636
6313
 
5637
6314
  // src/commands/phases/plan-phase.ts
5638
- var import_node_fs15 = require("fs");
5639
- var import_promises15 = require("fs/promises");
5640
- var import_node_path22 = require("path");
5641
- var import_chalk16 = __toESM(require("chalk"));
6315
+ var import_node_fs17 = require("fs");
6316
+ var import_promises16 = require("fs/promises");
6317
+ var import_node_path23 = require("path");
6318
+ var import_chalk17 = __toESM(require("chalk"));
5642
6319
 
5643
6320
  // src/ai/fix.ts
5644
6321
  init_lib();
5645
6322
  async function callGroqAndFinalize(systemPrompt, userMessage, plan, logError2, userEmail, tier = "free") {
5646
- const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
5647
- const jsonMatch = content.match(/\{[\s\S]*\}/);
5648
- if (!jsonMatch) throw new Error("No JSON in AI response");
5649
- 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);
5650
6327
  }
5651
6328
  function finalizePlan(result, original) {
5652
6329
  result.url = original.url;
@@ -5770,10 +6447,15 @@ async function translatePlan(plan, targetLang, logError2, userEmail, tier = "fre
5770
6447
  ${JSON.stringify(toTranslate)}
5771
6448
 
5772
6449
  Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
5773
- const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
5774
- const jsonMatch = content.match(/\{[\s\S]*\}/);
5775
- if (!jsonMatch) throw new Error("No JSON in translation response");
5776
- 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");
5777
6459
  const translatedPlan = JSON.parse(JSON.stringify(plan));
5778
6460
  translatedPlan.title = plan.title;
5779
6461
  translatedPlan.description = translated.description || plan.description;
@@ -5790,14 +6472,24 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
5790
6472
  // src/commands/phases/plan-phase.ts
5791
6473
  var INDENT6 = " ";
5792
6474
  async function runPlanPhase(params) {
5793
- const { url, config, userEmail, description, planPath, legacyPlanPath, targetLang, userTier = "free" } = params;
6475
+ const {
6476
+ url,
6477
+ config,
6478
+ userEmail,
6479
+ description,
6480
+ planPath,
6481
+ legacyPlanPath,
6482
+ targetLang,
6483
+ userTier = "free",
6484
+ fresh = false
6485
+ } = params;
5794
6486
  const maxSteps = config.plan?.maxSteps ?? 8;
5795
6487
  const extensionPath = config.browser?.extension;
5796
6488
  let existingPlan = null;
5797
6489
  let existingPlanPath = null;
5798
- if ((0, import_node_fs15.existsSync)(planPath)) {
6490
+ if ((0, import_node_fs17.existsSync)(planPath)) {
5799
6491
  try {
5800
- 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"));
5801
6493
  if (parsed.url && parsed.steps?.length > 0) {
5802
6494
  existingPlan = parsed;
5803
6495
  existingPlanPath = planPath;
@@ -5807,12 +6499,12 @@ async function runPlanPhase(params) {
5807
6499
  }
5808
6500
  if (!existingPlan) {
5809
6501
  try {
5810
- const files = (0, import_node_fs15.readdirSync)(config.output.dir);
5811
- const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path22.basename)(planPath));
6502
+ const files = (0, import_node_fs17.readdirSync)(config.output.dir);
6503
+ const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path23.basename)(planPath));
5812
6504
  for (const file of planFiles) {
5813
- const filePath = (0, import_node_path22.join)(config.output.dir, file);
6505
+ const filePath = (0, import_node_path23.join)(config.output.dir, file);
5814
6506
  try {
5815
- 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"));
5816
6508
  if (parsed.url && parsed.steps?.length > 0) {
5817
6509
  existingPlan = parsed;
5818
6510
  existingPlanPath = filePath;
@@ -5824,9 +6516,9 @@ async function runPlanPhase(params) {
5824
6516
  } catch {
5825
6517
  }
5826
6518
  }
5827
- if (!existingPlan && (0, import_node_fs15.existsSync)(legacyPlanPath)) {
6519
+ if (!existingPlan && (0, import_node_fs17.existsSync)(legacyPlanPath)) {
5828
6520
  try {
5829
- 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"));
5830
6522
  if (parsed.url && parsed.steps?.length > 0) {
5831
6523
  existingPlan = parsed;
5832
6524
  existingPlanPath = legacyPlanPath;
@@ -5836,6 +6528,11 @@ async function runPlanPhase(params) {
5836
6528
  }
5837
6529
  const normalizeUrl4 = (u) => u.replace(/\/+$/, "").toLowerCase();
5838
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
+ }
5839
6536
  let plan;
5840
6537
  const doTranslate = async (failMsg) => {
5841
6538
  const translateTask = task(`Translating to ${targetLang}`);
@@ -5850,25 +6547,25 @@ async function runPlanPhase(params) {
5850
6547
  translateTask.fail(failMsg);
5851
6548
  }
5852
6549
  };
5853
- if (existingPlan && existingPlanPath) {
6550
+ if (existingPlan && existingPlanPath && !fresh) {
5854
6551
  console.log();
5855
- console.log(import_chalk16.default.dim(` Found existing plan: `) + import_chalk16.default.cyan(formatClickablePath(existingPlanPath)));
5856
- console.log(import_chalk16.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
6552
+ console.log(import_chalk17.default.dim(` Found existing plan: `) + import_chalk17.default.cyan(formatClickablePath(existingPlanPath)));
6553
+ console.log(import_chalk17.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
5857
6554
  if (!urlMatches) {
5858
- console.log(import_chalk16.default.yellow(` \u26A0 Plan URL differs from argument:`));
5859
- console.log(import_chalk16.default.yellow(` Plan: ${existingPlan.url}`));
5860
- console.log(import_chalk16.default.yellow(` Arg: ${url}`));
6555
+ console.log(import_chalk17.default.yellow(` \u26A0 Plan URL differs from argument:`));
6556
+ console.log(import_chalk17.default.yellow(` Plan: ${existingPlan.url}`));
6557
+ console.log(import_chalk17.default.yellow(` Arg: ${url}`));
5861
6558
  }
5862
6559
  const existingLang = existingPlan.lang || "en";
5863
6560
  if (existingLang !== targetLang) {
5864
- console.log(import_chalk16.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
6561
+ console.log(import_chalk17.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
5865
6562
  }
5866
6563
  const reuse = await confirm(`${INDENT6}Use existing plan?`);
5867
6564
  if (reuse) {
5868
6565
  plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
5869
6566
  if (existingPlanPath !== planPath) {
5870
6567
  await savePlan(plan, planPath);
5871
- console.log(import_chalk16.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
6568
+ console.log(import_chalk17.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
5872
6569
  }
5873
6570
  if (existingLang !== targetLang) {
5874
6571
  const translate = await confirm(`${INDENT6}Translate to ${targetLang}?`);
@@ -5886,7 +6583,8 @@ async function runPlanPhase(params) {
5886
6583
  userEmail,
5887
6584
  extensionPath,
5888
6585
  config.output.dir,
5889
- userTier
6586
+ userTier,
6587
+ { width: config.video.width, height: config.video.height }
5890
6588
  );
5891
6589
  displayRateLimitInfo();
5892
6590
  if (urlMatches) {
@@ -5903,7 +6601,7 @@ async function runPlanPhase(params) {
5903
6601
  uploadIdx++;
5904
6602
  }
5905
6603
  }
5906
- console.log(import_chalk16.default.dim(` Preserved upload paths from previous plan`));
6604
+ console.log(import_chalk17.default.dim(` Preserved upload paths from previous plan`));
5907
6605
  }
5908
6606
  await savePlan(plan, planPath);
5909
6607
  plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
@@ -5919,7 +6617,9 @@ async function runPlanPhase(params) {
5919
6617
  logError,
5920
6618
  userEmail,
5921
6619
  extensionPath,
5922
- config.output.dir
6620
+ config.output.dir,
6621
+ void 0,
6622
+ { width: config.video.width, height: config.video.height }
5923
6623
  );
5924
6624
  displayRateLimitInfo();
5925
6625
  await savePlan(plan, planPath);
@@ -5927,8 +6627,8 @@ async function runPlanPhase(params) {
5927
6627
  }
5928
6628
  displayPlanSummary(plan);
5929
6629
  console.log();
5930
- console.log(import_chalk16.default.dim(` Plan: ${formatClickablePath(planPath)}`));
5931
- console.log(import_chalk16.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
6630
+ console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
6631
+ console.log(import_chalk17.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
5932
6632
  const MAX_REFINEMENTS = 10;
5933
6633
  let refinementCount = 0;
5934
6634
  while (true) {
@@ -5946,15 +6646,15 @@ async function runPlanPhase(params) {
5946
6646
  refineTask.succeed("Plan refined");
5947
6647
  displayRateLimitInfo();
5948
6648
  if (plan.steps.length > maxSteps) {
5949
- console.log(import_chalk16.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
6649
+ console.log(import_chalk17.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
5950
6650
  }
5951
6651
  displayPlanSummary(plan);
5952
6652
  console.log();
5953
- console.log(import_chalk16.default.dim(` Plan: ${formatClickablePath(planPath)}`));
6653
+ console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
5954
6654
  if (refinementCount >= MAX_REFINEMENTS) {
5955
6655
  console.log();
5956
- console.log(import_chalk16.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
5957
- console.log(import_chalk16.default.dim(` Further refinements may incur additional costs.`));
6656
+ console.log(import_chalk17.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
6657
+ console.log(import_chalk17.default.dim(` Further refinements may incur additional costs.`));
5958
6658
  const continueRefining = await confirm(`${INDENT6}Continue refining?`);
5959
6659
  if (!continueRefining) break;
5960
6660
  }
@@ -5966,12 +6666,12 @@ async function runPlanPhase(params) {
5966
6666
  const planLang = plan.lang || "en";
5967
6667
  if (planLang !== targetLang) {
5968
6668
  let useExistingTranslation = false;
5969
- if ((0, import_node_fs15.existsSync)(planPath) && existingPlanPath !== planPath) {
6669
+ if ((0, import_node_fs17.existsSync)(planPath) && existingPlanPath !== planPath) {
5970
6670
  try {
5971
- 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"));
5972
6672
  if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
5973
6673
  console.log();
5974
- console.log(import_chalk16.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
6674
+ console.log(import_chalk17.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
5975
6675
  const useTranslated = await confirm(`${INDENT6}Use ${targetLang} translation?`);
5976
6676
  if (useTranslated) {
5977
6677
  plan = translatedPlan;
@@ -5983,7 +6683,7 @@ async function runPlanPhase(params) {
5983
6683
  }
5984
6684
  if (!useExistingTranslation) {
5985
6685
  console.log();
5986
- console.log(import_chalk16.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
6686
+ console.log(import_chalk17.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
5987
6687
  const translate = await confirm(`${INDENT6}Translate captions to ${targetLang}?`);
5988
6688
  if (translate) await doTranslate("Translation failed, using original");
5989
6689
  }
@@ -5992,8 +6692,8 @@ async function runPlanPhase(params) {
5992
6692
  }
5993
6693
 
5994
6694
  // src/commands/phases/validation-phase.ts
5995
- var import_promises16 = require("fs/promises");
5996
- var import_chalk17 = __toESM(require("chalk"));
6695
+ var import_promises17 = require("fs/promises");
6696
+ var import_chalk18 = __toESM(require("chalk"));
5997
6697
  async function runValidationPhase(params) {
5998
6698
  let { plan } = params;
5999
6699
  const { config, planPath, isExtensionUrl, userEmail, userTier = "free" } = params;
@@ -6044,21 +6744,21 @@ async function runValidationPhase(params) {
6044
6744
  const stepNum = result.stepIndex + 1;
6045
6745
  const step = plan.steps[result.stepIndex];
6046
6746
  console.log();
6047
- console.log(import_chalk17.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
6747
+ console.log(import_chalk18.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
6048
6748
  const caption = step.caption || "";
6049
- if (caption) console.log(import_chalk17.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
6050
- console.log(import_chalk17.default.dim(` Target: ${step.target || "none"}`));
6051
- if (step.value) console.log(import_chalk17.default.dim(` Value: ${step.value}`));
6052
- console.log(import_chalk17.default.dim(` Error: ${result.error}`));
6749
+ if (caption) console.log(import_chalk18.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
6750
+ console.log(import_chalk18.default.dim(` Target: ${step.target || "none"}`));
6751
+ if (step.value) console.log(import_chalk18.default.dim(` Value: ${step.value}`));
6752
+ console.log(import_chalk18.default.dim(` Error: ${result.error}`));
6053
6753
  if (result.suggestion) {
6054
- console.log(import_chalk17.default.yellow(` Hint: ${result.suggestion}`));
6754
+ console.log(import_chalk18.default.yellow(` Hint: ${result.suggestion}`));
6055
6755
  }
6056
6756
  console.log();
6057
6757
  const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
6058
6758
  if (isUploadMissingFile) {
6059
6759
  const filePath = await promptInput('Enter file path to upload (or "skip"): ');
6060
6760
  if (filePath.toLowerCase() === "skip") {
6061
- console.log(import_chalk17.default.yellow(` Skipping step ${stepNum}`));
6761
+ console.log(import_chalk18.default.yellow(` Skipping step ${stepNum}`));
6062
6762
  continue;
6063
6763
  }
6064
6764
  if (filePath) {
@@ -6067,13 +6767,13 @@ async function runValidationPhase(params) {
6067
6767
  const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
6068
6768
  const newResult2 = revalidation2.results[result.stepIndex];
6069
6769
  if (newResult2?.success) {
6070
- console.log(import_chalk17.default.green(` \u2713 Step ${stepNum} now passes`));
6770
+ console.log(import_chalk18.default.green(` \u2713 Step ${stepNum} now passes`));
6071
6771
  validationResult = revalidation2;
6072
6772
  continue;
6073
6773
  }
6074
6774
  result.error = newResult2?.error;
6075
6775
  result.suggestion = newResult2?.suggestion;
6076
- console.log(import_chalk17.default.red(` Still failing: ${result.error}`));
6776
+ console.log(import_chalk18.default.red(` Still failing: ${result.error}`));
6077
6777
  }
6078
6778
  }
6079
6779
  const fixTask = task(`Fixing step ${stepNum} with AI`);
@@ -6095,8 +6795,8 @@ async function runValidationPhase(params) {
6095
6795
  await savePlan(plan, planPath);
6096
6796
  fixTask.succeed("AI suggested a fix");
6097
6797
  const fixedStep = plan.steps[result.stepIndex];
6098
- console.log(import_chalk17.default.dim(` New target: ${fixedStep.target || "none"}`));
6099
- if (fixedStep.value) console.log(import_chalk17.default.dim(` New value: ${fixedStep.value}`));
6798
+ console.log(import_chalk18.default.dim(` New target: ${fixedStep.target || "none"}`));
6799
+ if (fixedStep.value) console.log(import_chalk18.default.dim(` New value: ${fixedStep.value}`));
6100
6800
  } catch {
6101
6801
  fixTask.fail("AI could not fix automatically");
6102
6802
  }
@@ -6112,14 +6812,14 @@ async function runValidationPhase(params) {
6112
6812
  result.error = newResult?.error;
6113
6813
  result.suggestion = newResult?.suggestion;
6114
6814
  console.log();
6115
- console.log(import_chalk17.default.yellow(` Edit step ${stepNum} in: `) + import_chalk17.default.cyan(formatClickablePath(planPath)));
6815
+ console.log(import_chalk18.default.yellow(` Edit step ${stepNum} in: `) + import_chalk18.default.cyan(formatClickablePath(planPath)));
6116
6816
  console.log();
6117
6817
  const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
6118
6818
  if (choice.toLowerCase() === "skip") {
6119
- console.log(import_chalk17.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
6819
+ console.log(import_chalk18.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
6120
6820
  continue;
6121
6821
  }
6122
- plan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
6822
+ plan = JSON.parse(await (0, import_promises17.readFile)(planPath, "utf-8"));
6123
6823
  const finalRevalidateTask = task(`Final validation of step ${stepNum}`);
6124
6824
  const finalRevalidation = await validateStepsOnPage(plan, config, logError, config.output.dir);
6125
6825
  const finalResult = finalRevalidation.results[result.stepIndex];
@@ -6143,31 +6843,31 @@ async function runValidationPhase(params) {
6143
6843
  }
6144
6844
 
6145
6845
  // src/commands/utils/chrome-extension.ts
6146
- var import_node_fs16 = require("fs");
6846
+ var import_node_fs18 = require("fs");
6147
6847
  var import_node_os3 = require("os");
6148
- var import_node_path23 = require("path");
6848
+ var import_node_path24 = require("path");
6149
6849
  function resolveChromeExtension(extensionId) {
6150
6850
  const platform = process.platform;
6151
6851
  const possibleDirs = [];
6152
6852
  const getChromeExtDirs = (userDataPath) => {
6153
6853
  const dirs = [];
6154
6854
  try {
6155
- const profiles = (0, import_node_fs16.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 "));
6156
6856
  for (const profile of profiles) {
6157
- dirs.push((0, import_node_path23.join)(userDataPath, profile, "Extensions"));
6857
+ dirs.push((0, import_node_path24.join)(userDataPath, profile, "Extensions"));
6158
6858
  }
6159
6859
  } catch {
6160
- dirs.push((0, import_node_path23.join)(userDataPath, "Default", "Extensions"));
6860
+ dirs.push((0, import_node_path24.join)(userDataPath, "Default", "Extensions"));
6161
6861
  }
6162
6862
  return dirs;
6163
6863
  };
6164
6864
  if (platform === "win32") {
6165
- possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
6865
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
6166
6866
  } else if (platform === "darwin") {
6167
- possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
6867
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
6168
6868
  } else {
6169
6869
  try {
6170
- const windowsUsers = (0, import_node_fs16.readdirSync)("/mnt/c/Users").filter(
6870
+ const windowsUsers = (0, import_node_fs18.readdirSync)("/mnt/c/Users").filter(
6171
6871
  (u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
6172
6872
  );
6173
6873
  for (const user of windowsUsers) {
@@ -6175,22 +6875,22 @@ function resolveChromeExtension(extensionId) {
6175
6875
  }
6176
6876
  } catch {
6177
6877
  }
6178
- possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
6878
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
6179
6879
  }
6180
6880
  for (const chromeExtDir of possibleDirs) {
6181
- const extDir = (0, import_node_path23.join)(chromeExtDir, extensionId);
6182
- if (!(0, import_node_fs16.existsSync)(extDir)) continue;
6881
+ const extDir = (0, import_node_path24.join)(chromeExtDir, extensionId);
6882
+ if (!(0, import_node_fs18.existsSync)(extDir)) continue;
6183
6883
  try {
6184
- const versions = (0, import_node_fs16.readdirSync)(extDir).filter((f) => !f.startsWith("."));
6884
+ const versions = (0, import_node_fs18.readdirSync)(extDir).filter((f) => !f.startsWith("."));
6185
6885
  if (versions.length === 0) continue;
6186
6886
  const latestVersion = versions.sort().pop();
6187
- const extPath = (0, import_node_path23.join)(extDir, latestVersion);
6887
+ const extPath = (0, import_node_path24.join)(extDir, latestVersion);
6188
6888
  let name = extensionId;
6189
6889
  let popupPath = "popup.html";
6190
- const manifestPath = (0, import_node_path23.join)(extPath, "manifest.json");
6191
- if ((0, import_node_fs16.existsSync)(manifestPath)) {
6890
+ const manifestPath = (0, import_node_path24.join)(extPath, "manifest.json");
6891
+ if ((0, import_node_fs18.existsSync)(manifestPath)) {
6192
6892
  try {
6193
- const manifest = JSON.parse((0, import_node_fs16.readFileSync)(manifestPath, "utf-8"));
6893
+ const manifest = JSON.parse((0, import_node_fs18.readFileSync)(manifestPath, "utf-8"));
6194
6894
  if (manifest.name && !manifest.name.startsWith("__MSG_")) {
6195
6895
  name = manifest.name;
6196
6896
  }
@@ -6221,16 +6921,23 @@ async function recordCommand(url, options = {}) {
6221
6921
  const extensionId = url.slice(7);
6222
6922
  chromeExtension = resolveChromeExtension(extensionId);
6223
6923
  if (!chromeExtension) {
6224
- console.log(import_chalk18.default.red(`
6924
+ console.log(import_chalk19.default.red(`
6225
6925
  \u2717 Chrome extension not found: ${extensionId}`));
6226
- console.log(import_chalk18.default.dim(` Make sure the extension is installed in Chrome
6926
+ console.log(import_chalk19.default.dim(` Make sure the extension is installed in Chrome
6227
6927
  `));
6228
6928
  process.exit(1);
6229
6929
  }
6230
6930
  config.browser = { extension: chromeExtension.path };
6231
6931
  url = chromeExtension.popupUrl;
6232
- console.log(import_chalk18.default.dim(` Extension: ${chromeExtension.name}`));
6233
- console.log(import_chalk18.default.dim(` Path: ${chromeExtension.path}`));
6932
+ console.log(import_chalk19.default.dim(` Extension: ${chromeExtension.name}`));
6933
+ console.log(import_chalk19.default.dim(` Path: ${chromeExtension.path}`));
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
+ }
6234
6941
  }
6235
6942
  config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
6236
6943
  const userData = await loadUserData();
@@ -6243,20 +6950,20 @@ async function recordCommand(url, options = {}) {
6243
6950
  if (!authResult) return;
6244
6951
  const { userEmail, userTier, localVideoCount } = authResult;
6245
6952
  if (configCreated) {
6246
- const configPath = (0, import_node_path24.join)(process.cwd(), CONFIG_FILE);
6953
+ const configPath = (0, import_node_path25.join)(process.cwd(), CONFIG_FILE);
6247
6954
  console.log();
6248
- console.log(import_chalk18.default.green(` \u2714 Created ${CONFIG_FILE}`));
6249
- console.log(import_chalk18.default.dim(` ${configPath}`));
6955
+ console.log(import_chalk19.default.green(` \u2714 Created ${CONFIG_FILE}`));
6956
+ console.log(import_chalk19.default.dim(` ${configPath}`));
6250
6957
  console.log();
6251
- console.log(import_chalk18.default.cyan(" Review the config file to customize:"));
6252
- console.log(import_chalk18.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
6253
- console.log(import_chalk18.default.dim(" \u2022 video: Resolution, quality, effects"));
6254
- console.log(import_chalk18.default.dim(" \u2022 music: Background music path and volume"));
6255
- console.log(import_chalk18.default.dim(" \u2022 output: Output directory"));
6958
+ console.log(import_chalk19.default.cyan(" Review the config file to customize:"));
6959
+ console.log(import_chalk19.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
6960
+ console.log(import_chalk19.default.dim(" \u2022 video: Resolution, quality, effects"));
6961
+ console.log(import_chalk19.default.dim(" \u2022 music: Background music path and volume"));
6962
+ console.log(import_chalk19.default.dim(" \u2022 output: Output directory"));
6256
6963
  console.log();
6257
6964
  const proceed = await confirm(`${INDENT3}Proceed with default settings?`);
6258
6965
  if (!proceed) {
6259
- console.log(import_chalk18.default.yellow(`
6966
+ console.log(import_chalk19.default.yellow(`
6260
6967
  Edit ${CONFIG_FILE} and run again.
6261
6968
  `));
6262
6969
  process.exit(0);
@@ -6264,17 +6971,17 @@ async function recordCommand(url, options = {}) {
6264
6971
  }
6265
6972
  if (langOverride) config.lang = langOverride;
6266
6973
  if (config.music?.path) {
6267
- const musicPath = (0, import_node_path24.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path24.resolve)(config.output.dir, config.music.path);
6268
- if (!(0, import_node_fs17.existsSync)(musicPath)) {
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);
6975
+ if (!(0, import_node_fs19.existsSync)(musicPath)) {
6269
6976
  console.log();
6270
- console.log(import_chalk18.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
6271
- console.log(import_chalk18.default.dim(` Config: ${(0, import_node_path24.join)(process.cwd(), CONFIG_FILE)}`));
6977
+ console.log(import_chalk19.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
6978
+ console.log(import_chalk19.default.dim(` Config: ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}`));
6272
6979
  console.log();
6273
6980
  const proceed = await confirm(`${INDENT3}Continue without background music?`);
6274
6981
  if (!proceed) {
6275
- console.log(import_chalk18.default.yellow(`
6982
+ console.log(import_chalk19.default.yellow(`
6276
6983
  Edit music.path in config and run again.`));
6277
- console.log(import_chalk18.default.dim(` ${(0, import_node_path24.join)(process.cwd(), CONFIG_FILE)}
6984
+ console.log(import_chalk19.default.dim(` ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}
6278
6985
  `));
6279
6986
  process.exit(0);
6280
6987
  }
@@ -6284,10 +6991,10 @@ async function recordCommand(url, options = {}) {
6284
6991
  const captions = await loadCaptions();
6285
6992
  const workDir = await prepareWorkDir();
6286
6993
  const targetLang = config.lang || "en";
6287
- const planPath = (0, import_node_path24.join)(config.output.dir, `plan-${targetLang}.json`);
6288
- const legacyPlanPath = (0, import_node_path24.join)(config.output.dir, "plan.json");
6994
+ const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${targetLang}.json`);
6995
+ const legacyPlanPath = (0, import_node_path25.join)(config.output.dir, "plan.json");
6289
6996
  const isExtensionUrl = url.startsWith("chrome-extension://");
6290
- await (0, import_promises17.mkdir)(config.output.dir, { recursive: true });
6997
+ await (0, import_promises18.mkdir)(config.output.dir, { recursive: true });
6291
6998
  await setLogFilePath(config.output.dir);
6292
6999
  try {
6293
7000
  const plan = await runPlanPhase({
@@ -6298,7 +7005,8 @@ async function recordCommand(url, options = {}) {
6298
7005
  ...description !== void 0 && { description },
6299
7006
  planPath,
6300
7007
  legacyPlanPath,
6301
- targetLang
7008
+ targetLang,
7009
+ ...fresh !== void 0 && { fresh }
6302
7010
  });
6303
7011
  const validatedPlan = await runValidationPhase({
6304
7012
  plan,
@@ -6323,51 +7031,87 @@ async function recordCommand(url, options = {}) {
6323
7031
  await saveUserData({ localVideoCount: newCount });
6324
7032
  const remaining = FREE_VIDEO_LIMIT - newCount;
6325
7033
  if (remaining > 0) {
6326
- console.log(import_chalk18.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
7034
+ console.log(import_chalk19.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
6327
7035
  }
6328
7036
  }
6329
7037
  console.log();
6330
- console.log(import_chalk18.default.bold.green(" \u2713 Done!"));
7038
+ console.log(import_chalk19.default.bold.green(" \u2713 Done!"));
6331
7039
  console.log();
6332
- console.log(import_chalk18.default.cyan(` ${formatClickablePath(finalPath)}`));
7040
+ console.log(import_chalk19.default.cyan(` ${formatClickablePath(finalPath)}`));
6333
7041
  console.log();
6334
7042
  return finalPath;
6335
7043
  } catch (err) {
6336
7044
  await logError("Record Command", err);
6337
7045
  if (rethrow) throw err;
6338
7046
  console.log();
6339
- console.log(import_chalk18.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
6340
- console.log(import_chalk18.default.dim(` See ${logFilePath} for details`));
7047
+ console.log(import_chalk19.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
7048
+ console.log(import_chalk19.default.dim(` See ${logFilePath} for details`));
6341
7049
  process.exit(1);
6342
7050
  }
6343
7051
  }
6344
7052
 
6345
7053
  // src/commands/script-cmd.ts
6346
- var import_promises18 = require("fs/promises");
6347
- var import_node_path25 = require("path");
6348
- var import_chalk19 = __toESM(require("chalk"));
7054
+ var import_promises19 = require("fs/promises");
7055
+ var import_node_path26 = require("path");
7056
+ var import_chalk20 = __toESM(require("chalk"));
6349
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
+ }
6350
7069
  async function scriptCommand(url, scriptPath, lang) {
7070
+ url = resolveStartUrl(url);
6351
7071
  const { config, workDir, captions } = await initCommandEnv(url, lang);
7072
+ const userData = await loadUserData();
7073
+ const userTier = userData.email ? await getTierByEmail(userData.email) : "free";
6352
7074
  header("Script");
6353
- const raw = await (0, import_promises18.readFile)(scriptPath, "utf-8");
7075
+ const raw = await (0, import_promises19.readFile)(scriptPath, "utf-8");
6354
7076
  const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
6355
7077
  if (lines.length === 0) {
6356
- console.log(import_chalk19.default.red(" Script file is empty or has no non-comment lines"));
7078
+ console.log(import_chalk20.default.red(" Script file is empty or has no non-comment lines"));
6357
7079
  process.exit(1);
6358
7080
  }
6359
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
+ }
6360
7089
  header("Analyze");
6361
7090
  const crawlTask = task("Crawling page for UI elements");
6362
- const { elements } = await crawlPage(url, () => crawlTask);
7091
+ const { elements } = await crawlPage(url, () => crawlTask, findAuthState(url));
6363
7092
  const elementsText = elements.slice(0, 30).map((e) => ` ${e.action}: ${e.text} (${e.selector})`).join("\n");
6364
7093
  header("Plan");
6365
7094
  const planTask = task("Mapping script to UI actions");
6366
7095
  const appTitle = new URL(url).hostname.replace(/^www\./, "");
6367
- const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
6368
- const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
6369
- await savePlan(plan, planPath);
6370
- planTask.succeed(`${plan.steps.length} steps planned`);
7096
+ const planPath = (0, import_node_path26.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
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
+ }
6371
7115
  header("Voiceover");
6372
7116
  const { voiceoverPath, stepAudio } = await generateVoiceover(
6373
7117
  plan.steps,
@@ -6383,10 +7127,10 @@ async function scriptCommand(url, scriptPath, lang) {
6383
7127
  const { videoPath } = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
6384
7128
  header("Compose");
6385
7129
  const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
6386
- const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
7130
+ const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, userTier);
6387
7131
  console.log();
6388
- console.log(import_chalk19.default.bold.green(" Done!"));
6389
- console.log(import_chalk19.default.cyan(` ${finalPath}`));
7132
+ console.log(import_chalk20.default.bold.green(" Done!"));
7133
+ console.log(import_chalk20.default.cyan(` ${finalPath}`));
6390
7134
  }
6391
7135
 
6392
7136
  // src/qp.ts
@@ -6456,6 +7200,20 @@ function parseArgs(args) {
6456
7200
  if (first === "record") {
6457
7201
  args = args.slice(1);
6458
7202
  }
7203
+ if (first === "edit") {
7204
+ const rest = args.slice(1);
7205
+ let url2;
7206
+ let lang2;
7207
+ for (const arg2 of rest) {
7208
+ if (arg2.startsWith("lang=")) {
7209
+ lang2 = arg2.slice(5);
7210
+ } else if (!arg2.startsWith("-") && !url2 && !arg2.includes("=")) {
7211
+ url2 = normalizeUrl3(arg2);
7212
+ }
7213
+ }
7214
+ if (!url2) return { ...base, command: "help" };
7215
+ return { ...base, command: "edit", url: url2, lang: lang2 };
7216
+ }
6459
7217
  if (first === "capture") {
6460
7218
  const rest = args.slice(1);
6461
7219
  let url2;
@@ -6479,7 +7237,9 @@ function parseArgs(args) {
6479
7237
  let fresh = false;
6480
7238
  for (let i = 0; i < args.length; i++) {
6481
7239
  const arg2 = args[i];
6482
- if (arg2.startsWith("lang=")) {
7240
+ if (!url && (arg2.startsWith("http://") || arg2.startsWith("https://"))) {
7241
+ url = normalizeUrl3(arg2);
7242
+ } else if (arg2.startsWith("lang=")) {
6483
7243
  lang = arg2.slice(5);
6484
7244
  } else if (arg2 === "--script" || arg2 === "-s") {
6485
7245
  scriptPath = args[i + 1];
@@ -6523,8 +7283,8 @@ async function main() {
6523
7283
  break;
6524
7284
  case "auth":
6525
7285
  if (!parsed.url) {
6526
- console.log(import_chalk20.default.red(" Missing URL"));
6527
- console.log(import_chalk20.default.dim(" Run: qp auth <url>"));
7286
+ console.log(import_chalk21.default.red(" Missing URL"));
7287
+ console.log(import_chalk21.default.dim(" Run: qp auth <url>"));
6528
7288
  process.exit(1);
6529
7289
  }
6530
7290
  await authCommand(parsed.url);
@@ -6535,6 +7295,14 @@ async function main() {
6535
7295
  case "auth-list":
6536
7296
  await authListCommand();
6537
7297
  break;
7298
+ case "edit":
7299
+ if (!parsed.url) {
7300
+ console.log(import_chalk21.default.red(" Missing URL"));
7301
+ console.log(import_chalk21.default.dim(" Run: qp edit <url>"));
7302
+ process.exit(1);
7303
+ }
7304
+ await editCommand(parsed.url, parsed.lang);
7305
+ break;
6538
7306
  case "init":
6539
7307
  await initCommand();
6540
7308
  break;
@@ -6543,8 +7311,8 @@ async function main() {
6543
7311
  break;
6544
7312
  case "record":
6545
7313
  if (!parsed.url) {
6546
- console.log(import_chalk20.default.red(" Missing URL"));
6547
- console.log(import_chalk20.default.dim(" Run: qp --help"));
7314
+ console.log(import_chalk21.default.red(" Missing URL"));
7315
+ console.log(import_chalk21.default.dim(" Run: qp --help"));
6548
7316
  process.exit(1);
6549
7317
  }
6550
7318
  setAutoYes(parsed.yes);
@@ -6563,8 +7331,8 @@ async function main() {
6563
7331
  break;
6564
7332
  case "capture":
6565
7333
  if (!parsed.url) {
6566
- console.log(import_chalk20.default.red(" Missing URL"));
6567
- console.log(import_chalk20.default.dim(" Run: qp --help"));
7334
+ console.log(import_chalk21.default.red(" Missing URL"));
7335
+ console.log(import_chalk21.default.dim(" Run: qp --help"));
6568
7336
  process.exit(1);
6569
7337
  }
6570
7338
  console.log();