@spark-apps/quickpeek 1.2.0 → 1.2.1

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,9 @@ 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
+ }
128
131
  async function getOrRefreshDirectToken() {
129
132
  const now = Date.now();
130
133
  if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
@@ -133,7 +136,7 @@ async function getOrRefreshDirectToken() {
133
136
  try {
134
137
  const response = await fetch(SPARK_AI_TOKEN, {
135
138
  method: "POST",
136
- headers: { "X-Api-Key": API_KEY }
139
+ headers: { "X-Api-Key": apiKey() }
137
140
  });
138
141
  if (!response.ok) {
139
142
  throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
@@ -244,7 +247,7 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
244
247
  return lastRateLimitError || { success: false, error: "Request failed after retries" };
245
248
  }
246
249
  async function callAI(systemPrompt, userPrompt, tier = "free") {
247
- if (!API_KEY) {
250
+ if (!apiKey()) {
248
251
  return {
249
252
  success: false,
250
253
  error: "AI service is not configured. Please report this issue.",
@@ -304,7 +307,7 @@ function normalizeUrl(input) {
304
307
  url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
305
308
  return `https://${url}`;
306
309
  }
307
- var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, API_KEY, cachedDirectToken, cachedRelayToken;
310
+ var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
308
311
  var init_api_client = __esm({
309
312
  "src/lib/api-client.ts"() {
310
313
  "use strict";
@@ -315,7 +318,6 @@ var init_api_client = __esm({
315
318
  RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
316
319
  RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
317
320
  RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
318
- API_KEY = process.env.SPARK_AI_KEY || "";
319
321
  cachedDirectToken = null;
320
322
  cachedRelayToken = null;
321
323
  }
@@ -419,7 +421,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
419
421
  var init_config = __esm({
420
422
  "src/lib/config.ts"() {
421
423
  "use strict";
422
- VERSION = "1.1.1";
424
+ VERSION = "1.2.1";
423
425
  CONFIG_FILE = "quickpeek.config.json";
424
426
  DEFAULT_CONFIG = {
425
427
  plan: { maxSteps: 8 },
@@ -586,20 +588,29 @@ function getActionForType(type) {
586
588
  return "type";
587
589
  }
588
590
  }
589
- async function crawlPage(url, task2 = noopTask, storageStatePath) {
590
- const cached = crawlCache.get(url);
591
+ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
592
+ const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
593
+ const cached = crawlCache.get(cacheKey);
591
594
  if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
592
595
  task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
593
- return { title: cached.title, elements: [...cached.elements] };
596
+ return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
594
597
  }
595
598
  const browserTask = task2("Launching browser");
596
599
  const browser = await import_playwright.chromium.launch({ headless: true });
597
- const context = await browser.newContext(storageStatePath ? { storageState: storageStatePath } : {});
600
+ const context = await browser.newContext({
601
+ ...storageStatePath ? { storageState: storageStatePath } : {},
602
+ // Crawl at the size we will record at, or the measurements below describe
603
+ // a page nobody will ever see.
604
+ ...viewport ? { viewport } : {}
605
+ });
598
606
  const page = await context.newPage();
599
607
  browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
600
608
  const crawlTask = task2("Crawling page");
601
609
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
602
610
  const title = await page.title();
611
+ const scrollable = await page.evaluate(
612
+ "Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
613
+ );
603
614
  const rawElements = await page.evaluate(`
604
615
  (() => {
605
616
  const items = [];
@@ -729,12 +740,12 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
729
740
  });
730
741
  await browser.close();
731
742
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
732
- crawlCache.set(url, { timestamp: Date.now(), title, elements: [...elements] });
743
+ crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
733
744
  if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
734
745
  const oldest = crawlCache.keys().next().value;
735
746
  if (oldest !== void 0) crawlCache.delete(oldest);
736
747
  }
737
- return { title, elements };
748
+ return { title, elements, scrollable };
738
749
  }
739
750
  var import_playwright, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
740
751
  var init_crawl = __esm({
@@ -1308,7 +1319,7 @@ EXAMPLES:
1308
1319
  \u2022 File upload: "target": "input[type=\\"file\\"]"
1309
1320
  \u2022 Slider: "target": "input[type=\\"range\\"]"`;
1310
1321
  }
1311
- function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
1322
+ function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
1312
1323
  const lang = langName ? `
1313
1324
  Language: Write ALL captions in ${langName}.` : "";
1314
1325
  const purpose = description ? `
@@ -1346,8 +1357,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
1346
1357
  ${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
1347
1358
  }
1348
1359
  const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
1360
+ const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
1361
+ const scrolling = cannotScroll ? `
1362
+
1363
+ SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
1364
+ 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
1365
  return `URL: ${url}
1350
- Title: ${title}${purpose}
1366
+ Title: ${title}${purpose}${scrolling}
1351
1367
 
1352
1368
  AVAILABLE ELEMENTS (grouped by action):
1353
1369
  ${elementSummary}
@@ -1357,7 +1373,7 @@ REQUIREMENTS:
1357
1373
  \u2022 Complete ONE user journey: input \u2192 process \u2192 output
1358
1374
  \u2022 For CLICK: use simplified text target like "extract frame" or "download"
1359
1375
  \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}
1376
+ \u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
1361
1377
 
1362
1378
  Return JSON:
1363
1379
  {
@@ -1380,10 +1396,12 @@ TARGET SYNTAX:
1380
1396
  \u2022 Sliders: input[type="range"]
1381
1397
  \u2022 Text inputs: Use selector from list above`;
1382
1398
  }
1399
+ var MIN_USEFUL_SCROLL_PX;
1383
1400
  var init_prompts = __esm({
1384
1401
  "src/lib/prompts.ts"() {
1385
1402
  "use strict";
1386
1403
  init_utils();
1404
+ MIN_USEFUL_SCROLL_PX = 200;
1387
1405
  }
1388
1406
  });
1389
1407
 
@@ -2011,7 +2029,7 @@ var init_lib = __esm({
2011
2029
 
2012
2030
  // src/qp.ts
2013
2031
  var import_dotenv = __toESM(require("dotenv"));
2014
- var import_chalk20 = __toESM(require("chalk"));
2032
+ var import_chalk21 = __toESM(require("chalk"));
2015
2033
 
2016
2034
  // src/commands/auth-cmd.ts
2017
2035
  var import_node_readline2 = require("readline");
@@ -2324,6 +2342,7 @@ async function showHelp() {
2324
2342
  "Use your own narration script"
2325
2343
  ],
2326
2344
  [`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
2345
+ [`${cmd("qp edit")} ${arg("<url>")}`, `qp edit <url>`, "Edit the demo plan in the browser"],
2327
2346
  [`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
2328
2347
  [`${cmd("qp logout")}`, `qp logout`, "Sign out"],
2329
2348
  [`${cmd("qp auth")} ${arg("<url>")}`, `qp auth <url>`, "Save a signed-in session for a site you demo"],
@@ -2414,7 +2433,7 @@ function promptInput(prompt2) {
2414
2433
  var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
2415
2434
  function formatClickablePath(filePath) {
2416
2435
  const absolutePath = (0, import_node_path8.resolve)(filePath);
2417
- if (_isWSL) {
2436
+ if (_isWSL || process.platform !== "win32") {
2418
2437
  return absolutePath;
2419
2438
  }
2420
2439
  let formatted = absolutePath;
@@ -2513,16 +2532,23 @@ init_lib();
2513
2532
  var import_node_http = require("http");
2514
2533
  var import_node_url = require("url");
2515
2534
  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");
2535
+
2536
+ // src/lib/browser-open.ts
2537
+ var import_node_child_process3 = require("child_process");
2538
+ function editorBaseUrl() {
2539
+ return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
2540
+ }
2541
+ function openInBrowser(url) {
2520
2542
  const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
2521
2543
  const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
2522
2544
  return new Promise((resolve9) => {
2523
- exec2(command, () => resolve9());
2545
+ (0, import_node_child_process3.exec)(command, () => resolve9());
2524
2546
  });
2525
2547
  }
2548
+
2549
+ // src/utils/browser-auth.ts
2550
+ init_SparkStripe_feature_gating();
2551
+ var TIMEOUT_MS = 12e4;
2526
2552
  async function browserLogin() {
2527
2553
  return new Promise((resolve9) => {
2528
2554
  let server;
@@ -2576,7 +2602,7 @@ async function browserLogin() {
2576
2602
  console.log(import_chalk4.default.dim(` ${url}`));
2577
2603
  console.log();
2578
2604
  console.log(import_chalk4.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
2579
- openBrowser(url);
2605
+ void openInBrowser(url);
2580
2606
  });
2581
2607
  timer = setTimeout(() => {
2582
2608
  console.log();
@@ -2858,20 +2884,20 @@ function normalizeUrl2(url) {
2858
2884
  }
2859
2885
 
2860
2886
  // src/capture.ts
2861
- var import_node_child_process4 = require("child_process");
2887
+ var import_node_child_process5 = require("child_process");
2862
2888
  var import_node_http3 = require("http");
2863
2889
  var import_node_readline4 = require("readline");
2864
2890
  var import_playwright4 = require("playwright");
2865
2891
 
2866
2892
  // src/browser/wsl.ts
2867
- var import_node_child_process3 = require("child_process");
2893
+ var import_node_child_process4 = require("child_process");
2868
2894
  var import_node_fs5 = require("fs");
2869
2895
  var import_node_http2 = require("http");
2870
2896
  var import_playwright3 = require("playwright");
2871
2897
  function isWSL() {
2872
2898
  if (process.platform !== "linux") return false;
2873
2899
  try {
2874
- const r = (0, import_node_child_process3.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
2900
+ const r = (0, import_node_child_process4.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
2875
2901
  return r.includes("microsoft") || r.includes("wsl");
2876
2902
  } catch {
2877
2903
  return false;
@@ -2889,7 +2915,7 @@ function wslToWindowsPath(wslPath) {
2889
2915
  function findExe(whereExeQuery, ...hardcoded) {
2890
2916
  for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
2891
2917
  try {
2892
- const line = (0, import_node_child_process3.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
2918
+ const line = (0, import_node_child_process4.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
2893
2919
  if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
2894
2920
  } catch {
2895
2921
  }
@@ -2903,7 +2929,7 @@ var findWindowsChrome = () => findExe(
2903
2929
  var findWindowsNode = () => findExe("node.exe");
2904
2930
  function getWindowsHostIP() {
2905
2931
  try {
2906
- const m = (0, import_node_child_process3.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
2932
+ const m = (0, import_node_child_process4.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
2907
2933
  /default via (\d+\.\d+\.\d+\.\d+)/
2908
2934
  );
2909
2935
  if (m?.[1]) return m[1];
@@ -2919,7 +2945,7 @@ function getWindowsHostIP() {
2919
2945
  function startRelay(nodePath, windowsHost, targetPort) {
2920
2946
  const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
2921
2947
  return new Promise((resolve9, reject) => {
2922
- const proc = (0, import_node_child_process3.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
2948
+ const proc = (0, import_node_child_process4.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
2923
2949
  const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
2924
2950
  let buf = "";
2925
2951
  proc.stdout.on("data", (d) => {
@@ -2966,7 +2992,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
2966
2992
  if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
2967
2993
  const winUserData = wslToWindowsPath(userDataDir);
2968
2994
  const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
2969
- const chromeProc = (0, import_node_child_process3.spawn)(
2995
+ const chromeProc = (0, import_node_child_process4.spawn)(
2970
2996
  chromePath,
2971
2997
  [
2972
2998
  `--remote-debugging-port=${debugPort}`,
@@ -3150,9 +3176,9 @@ async function launchCaptureChrome() {
3150
3176
  return { browser, relay: probe.proc };
3151
3177
  }
3152
3178
  probe.proc.kill();
3153
- const winTemp = (0, import_node_child_process4.execSync)("cmd.exe /c echo %TEMP%", { encoding: "utf-8" }).trim().replace(/\r$/, "");
3179
+ const winTemp = (0, import_node_child_process5.execSync)("cmd.exe /c echo %TEMP%", { encoding: "utf-8" }).trim().replace(/\r$/, "");
3154
3180
  const userDataDir = `${winTemp}\\qp-capture`;
3155
- (0, import_node_child_process4.spawn)(
3181
+ (0, import_node_child_process5.spawn)(
3156
3182
  chromePath,
3157
3183
  [
3158
3184
  `--remote-debugging-port=${debugPort}`,
@@ -3649,7 +3675,14 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3649
3675
  }
3650
3676
  } else {
3651
3677
  const amount = direction === "up" ? -PAGE_SCROLL_PX : PAGE_SCROLL_PX;
3652
- await page.evaluate((y) => window.scrollBy(0, y), amount);
3678
+ const moved = await page.evaluate((y) => {
3679
+ const before = window.scrollY;
3680
+ window.scrollBy(0, y);
3681
+ return window.scrollY - before;
3682
+ }, amount);
3683
+ if (moved === 0) {
3684
+ throw new Error("scroll moved nothing: the page has no scroll headroom at this viewport");
3685
+ }
3653
3686
  }
3654
3687
  break;
3655
3688
  }
@@ -4805,12 +4838,12 @@ async function parsePlanJson(content, task2, logError2, userEmail, tier = "free"
4805
4838
  }
4806
4839
  }
4807
4840
  }
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));
4841
+ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
4842
+ const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
4810
4843
  const aiTask = task2("Generating plan with AI");
4811
4844
  const langName = getLanguageName(lang);
4812
4845
  const systemPrompt = buildSystemPrompt(maxSteps);
4813
- const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps);
4846
+ const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
4814
4847
  try {
4815
4848
  const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
4816
4849
  const jsonMatch = content.match(/\{[\s\S]*\}/);
@@ -4839,7 +4872,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
4839
4872
  }
4840
4873
  const renumberTask = task2("Validating plan structure");
4841
4874
  const originalCount = plan.steps.length;
4842
- const INVALID_TARGETS = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
4875
+ const INVALID_TARGETS2 = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
4843
4876
  const isValidStep = (s) => {
4844
4877
  if (!s?.action) return false;
4845
4878
  if (s.action === "navigate") {
@@ -4848,7 +4881,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
4848
4881
  }
4849
4882
  if (["click", "type", "select", "check", "upload", "drag"].includes(s.action)) {
4850
4883
  if (!s.target) return false;
4851
- if (INVALID_TARGETS.some((pattern) => pattern.test(s.target))) return false;
4884
+ if (INVALID_TARGETS2.some((pattern) => pattern.test(s.target))) return false;
4852
4885
  }
4853
4886
  if (s.action === "upload") {
4854
4887
  if (!s.target?.includes("file") && !s.target?.includes("input")) return false;
@@ -5314,16 +5347,172 @@ async function captureCommand(url, lang) {
5314
5347
  console.log(import_chalk11.default.cyan(` ${finalPath}`));
5315
5348
  }
5316
5349
 
5317
- // src/commands/init-cmd.ts
5350
+ // src/commands/edit-cmd.ts
5318
5351
  var import_node_fs14 = require("fs");
5319
- var import_promises14 = require("fs/promises");
5320
5352
  var import_node_path21 = require("path");
5321
5353
  var import_chalk12 = __toESM(require("chalk"));
5354
+
5355
+ // src/lib/plan-validation.ts
5356
+ var INVALID_TARGETS = [
5357
+ /\.png$/i,
5358
+ /\.jpg$/i,
5359
+ /\.gif$/i,
5360
+ /\.webp$/i,
5361
+ /\.svg$/i,
5362
+ /sprite_sheet/i,
5363
+ /downloaded/i
5364
+ ];
5365
+ var ACTIONS_REQUIRING_TARGET = ["click", "type", "select", "check", "drag"];
5366
+ function isFilePath(value) {
5367
+ if (!value) return false;
5368
+ return /\.\w{2,5}$/.test(value) && (/[/\\]/.test(value) || /^[a-zA-Z]:/.test(value));
5369
+ }
5370
+ function isInvalidTarget(target) {
5371
+ return INVALID_TARGETS.some((pattern) => pattern.test(target));
5372
+ }
5373
+ function validateStep(step) {
5374
+ if (!step?.action) {
5375
+ return "Missing action";
5376
+ }
5377
+ if (step.action === "navigate") {
5378
+ if (!step.target) return "Navigate requires a URL target";
5379
+ if (!step.target.startsWith("http://") && !step.target.startsWith("https://")) {
5380
+ return "Navigate target must be a valid URL";
5381
+ }
5382
+ }
5383
+ if (step.action === "media") {
5384
+ if (!step.mediaFile && !isFilePath(step.value)) {
5385
+ return "Media step requires a file (image or video)";
5386
+ }
5387
+ }
5388
+ if (step.action === "upload") {
5389
+ if (!step.mediaFile && !isFilePath(step.value)) {
5390
+ return "Upload step requires a file";
5391
+ }
5392
+ }
5393
+ if (ACTIONS_REQUIRING_TARGET.includes(step.action)) {
5394
+ if (!step.target) return `${step.action} requires a target`;
5395
+ if (isInvalidTarget(step.target)) return `Invalid target pattern: ${step.target}`;
5396
+ }
5397
+ return null;
5398
+ }
5399
+
5400
+ // src/lib/plan-file.ts
5401
+ var MAX_HANDOFF_URL_CHARS = 32e3;
5402
+ function serializePlan(plan) {
5403
+ return `${JSON.stringify(plan, null, 2)}
5404
+ `;
5405
+ }
5406
+ function parsePlanText(text) {
5407
+ let raw;
5408
+ try {
5409
+ raw = JSON.parse(text);
5410
+ } catch (e) {
5411
+ return { ok: false, error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
5412
+ }
5413
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5414
+ return { ok: false, error: "A plan must be a JSON object." };
5415
+ }
5416
+ const candidate = raw;
5417
+ if (typeof candidate.url !== "string" || !candidate.url.trim()) {
5418
+ return { ok: false, error: "A plan needs a `url` to record." };
5419
+ }
5420
+ if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
5421
+ return { ok: false, error: "A plan needs at least one step." };
5422
+ }
5423
+ const steps = [];
5424
+ for (const [index, step] of candidate.steps.entries()) {
5425
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
5426
+ return { ok: false, error: `Step ${index + 1} is not an object.` };
5427
+ }
5428
+ const typed = step;
5429
+ if (typeof typed.action !== "string" || !typed.action.trim()) {
5430
+ return { ok: false, error: `Step ${index + 1} has no action.` };
5431
+ }
5432
+ const failure = validateStep(typed);
5433
+ if (failure) return { ok: false, error: `Step ${index + 1}: ${failure}` };
5434
+ steps.push({ ...typed, id: typed.id ?? index + 1 });
5435
+ }
5436
+ return {
5437
+ ok: true,
5438
+ plan: {
5439
+ url: candidate.url,
5440
+ title: candidate.title ?? "",
5441
+ description: candidate.description ?? "",
5442
+ ...candidate.lang ? { lang: candidate.lang } : {},
5443
+ steps
5444
+ }
5445
+ };
5446
+ }
5447
+ function toBase64Url(text) {
5448
+ const bytes = new TextEncoder().encode(text);
5449
+ const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
5450
+ const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(text, "utf8").toString("base64");
5451
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5452
+ }
5453
+ function encodePlan(plan) {
5454
+ return toBase64Url(serializePlan(plan));
5455
+ }
5456
+ function planEditorUrl(baseUrl, plan) {
5457
+ const url = `${baseUrl.replace(/\/$/, "")}/edit#plan=${encodePlan(plan)}`;
5458
+ return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
5459
+ }
5460
+
5461
+ // src/commands/edit-cmd.ts
5462
+ var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
5463
+ function findPlanFile(dir, lang) {
5464
+ if (lang) {
5465
+ const exact = (0, import_node_path21.join)(dir, `plan-${lang}.json`);
5466
+ return (0, import_node_fs14.existsSync)(exact) ? exact : null;
5467
+ }
5468
+ if (!(0, import_node_fs14.existsSync)(dir)) return null;
5469
+ const plans = (0, import_node_fs14.readdirSync)(dir).filter((f) => PLAN_FILE.test(f));
5470
+ const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
5471
+ return preferred ? (0, import_node_path21.join)(dir, preferred) : null;
5472
+ }
5473
+ async function editCommand(url, lang) {
5474
+ console.log();
5475
+ showLogo();
5476
+ const dir = getOutputDirFromUrl(url);
5477
+ const planPath = findPlanFile(dir, lang);
5478
+ if (!planPath) {
5479
+ console.log(import_chalk12.default.red(` No plan found in ${dir}`));
5480
+ console.log(import_chalk12.default.dim(` Record the demo first: `) + import_chalk12.default.cyan(`qp ${url}`));
5481
+ console.log();
5482
+ return;
5483
+ }
5484
+ const parsed = parsePlanText((0, import_node_fs14.readFileSync)(planPath, "utf8"));
5485
+ if (!parsed.ok) {
5486
+ console.log(import_chalk12.default.red(` ${planPath} cannot be opened: ${parsed.error}`));
5487
+ console.log();
5488
+ return;
5489
+ }
5490
+ const base = editorBaseUrl();
5491
+ const editorUrl = planEditorUrl(base, parsed.plan);
5492
+ if (!editorUrl) {
5493
+ console.log(import_chalk12.default.yellow(" This plan is too long to carry in a link."));
5494
+ console.log(import_chalk12.default.dim(` Open ${base}/edit and load `) + import_chalk12.default.cyan(formatClickablePath(planPath)));
5495
+ console.log();
5496
+ await openInBrowser(`${base}/edit`);
5497
+ return;
5498
+ }
5499
+ await openInBrowser(editorUrl);
5500
+ console.log(import_chalk12.default.green(` \u2713 Opened ${parsed.plan.steps.length} steps in the editor`));
5501
+ console.log(import_chalk12.default.dim(` ${formatClickablePath(planPath)}`));
5502
+ console.log(import_chalk12.default.dim(" Save from the editor over this file, then record again."));
5503
+ console.log();
5504
+ }
5505
+
5506
+ // src/commands/init-cmd.ts
5507
+ var import_node_fs15 = require("fs");
5508
+ var import_promises14 = require("fs/promises");
5509
+ var import_node_path22 = require("path");
5510
+ var import_chalk13 = __toESM(require("chalk"));
5322
5511
  init_lib();
5323
5512
  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`));
5513
+ const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
5514
+ if ((0, import_node_fs15.existsSync)(configPath)) {
5515
+ console.log(import_chalk13.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
5327
5516
  return;
5328
5517
  }
5329
5518
  const defaultConfig = {
@@ -5348,28 +5537,28 @@ async function initCommand() {
5348
5537
  };
5349
5538
  await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
5350
5539
  `);
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>`));
5540
+ console.log(import_chalk13.default.green(` \u2714 Created ${CONFIG_FILE}`));
5541
+ console.log(import_chalk13.default.dim(` Edit it or use: qp config <key> <value>`));
5353
5542
  }
5354
5543
  async function configCommand(key, value) {
5355
- const configPath = (0, import_node_path21.join)(process.cwd(), CONFIG_FILE);
5544
+ const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
5356
5545
  if (!key) {
5357
- if (!(0, import_node_fs14.existsSync)(configPath)) {
5358
- console.log(import_chalk12.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
5546
+ if (!(0, import_node_fs15.existsSync)(configPath)) {
5547
+ console.log(import_chalk13.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
5359
5548
  return;
5360
5549
  }
5361
5550
  const content = await (0, import_promises14.readFile)(configPath, "utf-8");
5362
- console.log(import_chalk12.default.cyan(` ${CONFIG_FILE}:`));
5551
+ console.log(import_chalk13.default.cyan(` ${CONFIG_FILE}:`));
5363
5552
  console.log(content);
5364
5553
  return;
5365
5554
  }
5366
5555
  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>`));
5556
+ console.log(import_chalk13.default.red(` Missing value for ${key}`));
5557
+ console.log(import_chalk13.default.dim(` Usage: qp config ${key} <value>`));
5369
5558
  return;
5370
5559
  }
5371
5560
  let config = {};
5372
- if ((0, import_node_fs14.existsSync)(configPath)) {
5561
+ if ((0, import_node_fs15.existsSync)(configPath)) {
5373
5562
  try {
5374
5563
  config = JSON.parse(await (0, import_promises14.readFile)(configPath, "utf-8"));
5375
5564
  } catch {
@@ -5395,40 +5584,40 @@ async function configCommand(key, value) {
5395
5584
  }
5396
5585
  await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
5397
5586
  `);
5398
- console.log(import_chalk12.default.green(` \u2714 Set ${key} = ${value}`));
5587
+ console.log(import_chalk13.default.green(` \u2714 Set ${key} = ${value}`));
5399
5588
  }
5400
5589
 
5401
5590
  // src/commands/login-cmd.ts
5402
- var import_chalk13 = __toESM(require("chalk"));
5591
+ var import_chalk14 = __toESM(require("chalk"));
5403
5592
  init_lib();
5404
5593
  async function loginCommand() {
5405
5594
  console.log();
5406
5595
  showLogo();
5407
5596
  const existing = await loadUserData();
5408
5597
  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."));
5598
+ console.log(import_chalk14.default.dim(` Already signed in as ${existing.email}`));
5599
+ console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp logout") + import_chalk14.default.dim(" first to switch accounts."));
5411
5600
  console.log();
5412
5601
  try {
5413
5602
  const status = await getStatusCached(existing.email);
5414
5603
  const tier2 = await getTierByEmail(existing.email);
5415
- const tierLabel2 = tier2 === "pro" ? import_chalk13.default.green("Pro") : "Free";
5604
+ const tierLabel2 = tier2 === "pro" ? import_chalk14.default.green("Pro") : "Free";
5416
5605
  console.log(` ${tierLabel2} plan`);
5417
5606
  for (const line of formatUsageLines(status, tier2)) {
5418
- console.log(import_chalk13.default.dim(` ${line}`));
5607
+ console.log(import_chalk14.default.dim(` ${line}`));
5419
5608
  }
5420
5609
  } catch {
5421
5610
  }
5422
5611
  console.log();
5423
5612
  return;
5424
5613
  }
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."));
5614
+ console.log(import_chalk14.default.white(" Sign in with your Google account to track usage"));
5615
+ console.log(import_chalk14.default.white(" and unlock Pro features."));
5427
5616
  const email = await browserLogin();
5428
5617
  if (!email) {
5429
5618
  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."));
5619
+ console.log(import_chalk14.default.yellow(" Sign-in cancelled or timed out."));
5620
+ console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp login") + import_chalk14.default.dim(" to try again."));
5432
5621
  console.log();
5433
5622
  return;
5434
5623
  }
@@ -5439,8 +5628,8 @@ async function loginCommand() {
5439
5628
  }
5440
5629
  await saveUserData({ email, verified: true, tier });
5441
5630
  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}`));
5631
+ const tierLabel = tier === "pro" ? import_chalk14.default.green("Pro") : "Free";
5632
+ console.log(import_chalk14.default.green(` \u2713 Signed in as ${email}`));
5444
5633
  console.log(` ${tierLabel} plan`);
5445
5634
  console.log();
5446
5635
  }
@@ -5448,46 +5637,46 @@ async function logoutCommand() {
5448
5637
  console.log();
5449
5638
  const userData = await loadUserData();
5450
5639
  if (!userData.email) {
5451
- console.log(import_chalk13.default.dim(" Not signed in."));
5640
+ console.log(import_chalk14.default.dim(" Not signed in."));
5452
5641
  console.log();
5453
5642
  return;
5454
5643
  }
5455
5644
  const email = userData.email;
5456
5645
  await saveUserData({ email: "", verified: false, tier: "free" });
5457
- console.log(import_chalk13.default.green(` \u2713 Signed out (${email})`));
5646
+ console.log(import_chalk14.default.green(` \u2713 Signed out (${email})`));
5458
5647
  console.log();
5459
5648
  }
5460
5649
 
5461
5650
  // src/commands/record-cmd.ts
5462
- var import_node_fs17 = require("fs");
5651
+ var import_node_fs18 = require("fs");
5463
5652
  var import_promises17 = require("fs/promises");
5464
- var import_node_path24 = require("path");
5465
- var import_chalk18 = __toESM(require("chalk"));
5653
+ var import_node_path25 = require("path");
5654
+ var import_chalk19 = __toESM(require("chalk"));
5466
5655
  init_lib();
5467
5656
 
5468
5657
  // src/commands/phases/auth-phase.ts
5469
- var import_chalk15 = __toESM(require("chalk"));
5658
+ var import_chalk16 = __toESM(require("chalk"));
5470
5659
  init_lib();
5471
5660
 
5472
5661
  // src/commands/utils/auth-helpers.ts
5473
- var import_chalk14 = __toESM(require("chalk"));
5662
+ var import_chalk15 = __toESM(require("chalk"));
5474
5663
  init_lib();
5475
5664
  async function handleRegistrationGate() {
5476
5665
  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."));
5666
+ console.log(import_chalk15.default.yellow(" \u{1F389} You've made 5 free videos!"));
5667
+ console.log(import_chalk15.default.white(" Sign in to continue creating videos."));
5479
5668
  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"));
5669
+ console.log(import_chalk15.default.dim(" Benefits of signing in:"));
5670
+ console.log(import_chalk15.default.dim(" \u2022 20 more videos per month"));
5671
+ console.log(import_chalk15.default.dim(" \u2022 Keep your video history"));
5672
+ console.log(import_chalk15.default.dim(" \u2022 Upgrade to Pro anytime"));
5484
5673
  let email = await browserLogin();
5485
5674
  if (!email) {
5486
5675
  console.log();
5487
5676
  email = await promptInput("Email: ");
5488
5677
  }
5489
5678
  if (!email || !email.includes("@")) {
5490
- console.log(import_chalk14.default.red(" Invalid email. Run qp again to try again."));
5679
+ console.log(import_chalk15.default.red(" Invalid email. Run qp again to try again."));
5491
5680
  return null;
5492
5681
  }
5493
5682
  const registerTask = task("Sending verification email");
@@ -5502,10 +5691,10 @@ async function handleRegistrationGate() {
5502
5691
  }
5503
5692
  registerTask.succeed("Check your email!");
5504
5693
  console.log();
5505
- console.log(import_chalk14.default.cyan(" \u{1F4E7} Verification email sent to:"));
5506
- console.log(import_chalk14.default.white(` ${email}`));
5694
+ console.log(import_chalk15.default.cyan(" \u{1F4E7} Verification email sent to:"));
5695
+ console.log(import_chalk15.default.white(` ${email}`));
5507
5696
  console.log();
5508
- console.log(import_chalk14.default.dim(" Click the link in the email, then press Enter to continue..."));
5697
+ console.log(import_chalk15.default.dim(" Click the link in the email, then press Enter to continue..."));
5509
5698
  await promptInput("");
5510
5699
  const verifyTask = task("Checking verification");
5511
5700
  const verified = await isVerified(email);
@@ -5515,10 +5704,10 @@ async function handleRegistrationGate() {
5515
5704
  }
5516
5705
  verifyTask.fail("Not verified yet");
5517
5706
  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."));
5707
+ console.log(import_chalk15.default.yellow(" Email not verified. Check your inbox and spam folder."));
5708
+ console.log(import_chalk15.default.dim(" Run qp again after clicking the verification link."));
5520
5709
  console.log();
5521
- console.log(import_chalk14.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
5710
+ console.log(import_chalk15.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
5522
5711
  console.log();
5523
5712
  return { email, verified: false };
5524
5713
  }
@@ -5546,7 +5735,7 @@ async function runAuthPhase(input) {
5546
5735
  } else {
5547
5736
  verifyTask.fail("Email not verified");
5548
5737
  console.log();
5549
- console.log(import_chalk15.default.yellow(" Check your inbox for the verification email."));
5738
+ console.log(import_chalk16.default.yellow(" Check your inbox for the verification email."));
5550
5739
  console.log();
5551
5740
  const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
5552
5741
  if (input2.toLowerCase() === "exit") {
@@ -5571,8 +5760,8 @@ async function runAuthPhase(input) {
5571
5760
  registerTask.succeed("Check your email!");
5572
5761
  await saveUserData({ email: newEmail, verified: false });
5573
5762
  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."));
5763
+ console.log(import_chalk16.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
5764
+ console.log(import_chalk16.default.dim(" Click the link in the email, then run qp again."));
5576
5765
  console.log();
5577
5766
  gracefulExit();
5578
5767
  return null;
@@ -5588,7 +5777,7 @@ async function runAuthPhase(input) {
5588
5777
  } else {
5589
5778
  recheckTask.fail("Still not verified");
5590
5779
  console.log();
5591
- console.log(import_chalk15.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
5780
+ console.log(import_chalk16.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
5592
5781
  console.log();
5593
5782
  gracefulExit();
5594
5783
  return null;
@@ -5615,8 +5804,8 @@ async function runAuthPhase(input) {
5615
5804
  } else {
5616
5805
  const remaining = FREE_VIDEO_LIMIT - localVideoCount;
5617
5806
  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`));
5807
+ console.log(import_chalk16.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
5808
+ console.log(import_chalk16.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
5620
5809
  userEmail = await setupUserEmail();
5621
5810
  let registeredVerified = false;
5622
5811
  try {
@@ -5635,10 +5824,10 @@ async function runAuthPhase(input) {
5635
5824
  }
5636
5825
 
5637
5826
  // src/commands/phases/plan-phase.ts
5638
- var import_node_fs15 = require("fs");
5827
+ var import_node_fs16 = require("fs");
5639
5828
  var import_promises15 = require("fs/promises");
5640
- var import_node_path22 = require("path");
5641
- var import_chalk16 = __toESM(require("chalk"));
5829
+ var import_node_path23 = require("path");
5830
+ var import_chalk17 = __toESM(require("chalk"));
5642
5831
 
5643
5832
  // src/ai/fix.ts
5644
5833
  init_lib();
@@ -5790,12 +5979,22 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
5790
5979
  // src/commands/phases/plan-phase.ts
5791
5980
  var INDENT6 = " ";
5792
5981
  async function runPlanPhase(params) {
5793
- const { url, config, userEmail, description, planPath, legacyPlanPath, targetLang, userTier = "free" } = params;
5982
+ const {
5983
+ url,
5984
+ config,
5985
+ userEmail,
5986
+ description,
5987
+ planPath,
5988
+ legacyPlanPath,
5989
+ targetLang,
5990
+ userTier = "free",
5991
+ fresh = false
5992
+ } = params;
5794
5993
  const maxSteps = config.plan?.maxSteps ?? 8;
5795
5994
  const extensionPath = config.browser?.extension;
5796
5995
  let existingPlan = null;
5797
5996
  let existingPlanPath = null;
5798
- if ((0, import_node_fs15.existsSync)(planPath)) {
5997
+ if ((0, import_node_fs16.existsSync)(planPath)) {
5799
5998
  try {
5800
5999
  const parsed = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
5801
6000
  if (parsed.url && parsed.steps?.length > 0) {
@@ -5807,10 +6006,10 @@ async function runPlanPhase(params) {
5807
6006
  }
5808
6007
  if (!existingPlan) {
5809
6008
  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));
6009
+ const files = (0, import_node_fs16.readdirSync)(config.output.dir);
6010
+ const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path23.basename)(planPath));
5812
6011
  for (const file of planFiles) {
5813
- const filePath = (0, import_node_path22.join)(config.output.dir, file);
6012
+ const filePath = (0, import_node_path23.join)(config.output.dir, file);
5814
6013
  try {
5815
6014
  const parsed = JSON.parse(await (0, import_promises15.readFile)(filePath, "utf-8"));
5816
6015
  if (parsed.url && parsed.steps?.length > 0) {
@@ -5824,7 +6023,7 @@ async function runPlanPhase(params) {
5824
6023
  } catch {
5825
6024
  }
5826
6025
  }
5827
- if (!existingPlan && (0, import_node_fs15.existsSync)(legacyPlanPath)) {
6026
+ if (!existingPlan && (0, import_node_fs16.existsSync)(legacyPlanPath)) {
5828
6027
  try {
5829
6028
  const parsed = JSON.parse(await (0, import_promises15.readFile)(legacyPlanPath, "utf-8"));
5830
6029
  if (parsed.url && parsed.steps?.length > 0) {
@@ -5850,25 +6049,25 @@ async function runPlanPhase(params) {
5850
6049
  translateTask.fail(failMsg);
5851
6050
  }
5852
6051
  };
5853
- if (existingPlan && existingPlanPath) {
6052
+ if (existingPlan && existingPlanPath && !fresh) {
5854
6053
  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}`));
6054
+ console.log(import_chalk17.default.dim(` Found existing plan: `) + import_chalk17.default.cyan(formatClickablePath(existingPlanPath)));
6055
+ console.log(import_chalk17.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
5857
6056
  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}`));
6057
+ console.log(import_chalk17.default.yellow(` \u26A0 Plan URL differs from argument:`));
6058
+ console.log(import_chalk17.default.yellow(` Plan: ${existingPlan.url}`));
6059
+ console.log(import_chalk17.default.yellow(` Arg: ${url}`));
5861
6060
  }
5862
6061
  const existingLang = existingPlan.lang || "en";
5863
6062
  if (existingLang !== targetLang) {
5864
- console.log(import_chalk16.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
6063
+ console.log(import_chalk17.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
5865
6064
  }
5866
6065
  const reuse = await confirm(`${INDENT6}Use existing plan?`);
5867
6066
  if (reuse) {
5868
6067
  plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
5869
6068
  if (existingPlanPath !== planPath) {
5870
6069
  await savePlan(plan, planPath);
5871
- console.log(import_chalk16.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
6070
+ console.log(import_chalk17.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
5872
6071
  }
5873
6072
  if (existingLang !== targetLang) {
5874
6073
  const translate = await confirm(`${INDENT6}Translate to ${targetLang}?`);
@@ -5886,7 +6085,8 @@ async function runPlanPhase(params) {
5886
6085
  userEmail,
5887
6086
  extensionPath,
5888
6087
  config.output.dir,
5889
- userTier
6088
+ userTier,
6089
+ { width: config.video.width, height: config.video.height }
5890
6090
  );
5891
6091
  displayRateLimitInfo();
5892
6092
  if (urlMatches) {
@@ -5903,7 +6103,7 @@ async function runPlanPhase(params) {
5903
6103
  uploadIdx++;
5904
6104
  }
5905
6105
  }
5906
- console.log(import_chalk16.default.dim(` Preserved upload paths from previous plan`));
6106
+ console.log(import_chalk17.default.dim(` Preserved upload paths from previous plan`));
5907
6107
  }
5908
6108
  await savePlan(plan, planPath);
5909
6109
  plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
@@ -5919,7 +6119,9 @@ async function runPlanPhase(params) {
5919
6119
  logError,
5920
6120
  userEmail,
5921
6121
  extensionPath,
5922
- config.output.dir
6122
+ config.output.dir,
6123
+ void 0,
6124
+ { width: config.video.width, height: config.video.height }
5923
6125
  );
5924
6126
  displayRateLimitInfo();
5925
6127
  await savePlan(plan, planPath);
@@ -5927,8 +6129,8 @@ async function runPlanPhase(params) {
5927
6129
  }
5928
6130
  displayPlanSummary(plan);
5929
6131
  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.`));
6132
+ console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
6133
+ console.log(import_chalk17.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
5932
6134
  const MAX_REFINEMENTS = 10;
5933
6135
  let refinementCount = 0;
5934
6136
  while (true) {
@@ -5946,15 +6148,15 @@ async function runPlanPhase(params) {
5946
6148
  refineTask.succeed("Plan refined");
5947
6149
  displayRateLimitInfo();
5948
6150
  if (plan.steps.length > maxSteps) {
5949
- console.log(import_chalk16.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
6151
+ console.log(import_chalk17.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
5950
6152
  }
5951
6153
  displayPlanSummary(plan);
5952
6154
  console.log();
5953
- console.log(import_chalk16.default.dim(` Plan: ${formatClickablePath(planPath)}`));
6155
+ console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
5954
6156
  if (refinementCount >= MAX_REFINEMENTS) {
5955
6157
  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.`));
6158
+ console.log(import_chalk17.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
6159
+ console.log(import_chalk17.default.dim(` Further refinements may incur additional costs.`));
5958
6160
  const continueRefining = await confirm(`${INDENT6}Continue refining?`);
5959
6161
  if (!continueRefining) break;
5960
6162
  }
@@ -5966,12 +6168,12 @@ async function runPlanPhase(params) {
5966
6168
  const planLang = plan.lang || "en";
5967
6169
  if (planLang !== targetLang) {
5968
6170
  let useExistingTranslation = false;
5969
- if ((0, import_node_fs15.existsSync)(planPath) && existingPlanPath !== planPath) {
6171
+ if ((0, import_node_fs16.existsSync)(planPath) && existingPlanPath !== planPath) {
5970
6172
  try {
5971
6173
  const translatedPlan = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
5972
6174
  if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
5973
6175
  console.log();
5974
- console.log(import_chalk16.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
6176
+ console.log(import_chalk17.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
5975
6177
  const useTranslated = await confirm(`${INDENT6}Use ${targetLang} translation?`);
5976
6178
  if (useTranslated) {
5977
6179
  plan = translatedPlan;
@@ -5983,7 +6185,7 @@ async function runPlanPhase(params) {
5983
6185
  }
5984
6186
  if (!useExistingTranslation) {
5985
6187
  console.log();
5986
- console.log(import_chalk16.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
6188
+ console.log(import_chalk17.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
5987
6189
  const translate = await confirm(`${INDENT6}Translate captions to ${targetLang}?`);
5988
6190
  if (translate) await doTranslate("Translation failed, using original");
5989
6191
  }
@@ -5993,7 +6195,7 @@ async function runPlanPhase(params) {
5993
6195
 
5994
6196
  // src/commands/phases/validation-phase.ts
5995
6197
  var import_promises16 = require("fs/promises");
5996
- var import_chalk17 = __toESM(require("chalk"));
6198
+ var import_chalk18 = __toESM(require("chalk"));
5997
6199
  async function runValidationPhase(params) {
5998
6200
  let { plan } = params;
5999
6201
  const { config, planPath, isExtensionUrl, userEmail, userTier = "free" } = params;
@@ -6044,21 +6246,21 @@ async function runValidationPhase(params) {
6044
6246
  const stepNum = result.stepIndex + 1;
6045
6247
  const step = plan.steps[result.stepIndex];
6046
6248
  console.log();
6047
- console.log(import_chalk17.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
6249
+ console.log(import_chalk18.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
6048
6250
  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}`));
6251
+ if (caption) console.log(import_chalk18.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
6252
+ console.log(import_chalk18.default.dim(` Target: ${step.target || "none"}`));
6253
+ if (step.value) console.log(import_chalk18.default.dim(` Value: ${step.value}`));
6254
+ console.log(import_chalk18.default.dim(` Error: ${result.error}`));
6053
6255
  if (result.suggestion) {
6054
- console.log(import_chalk17.default.yellow(` Hint: ${result.suggestion}`));
6256
+ console.log(import_chalk18.default.yellow(` Hint: ${result.suggestion}`));
6055
6257
  }
6056
6258
  console.log();
6057
6259
  const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
6058
6260
  if (isUploadMissingFile) {
6059
6261
  const filePath = await promptInput('Enter file path to upload (or "skip"): ');
6060
6262
  if (filePath.toLowerCase() === "skip") {
6061
- console.log(import_chalk17.default.yellow(` Skipping step ${stepNum}`));
6263
+ console.log(import_chalk18.default.yellow(` Skipping step ${stepNum}`));
6062
6264
  continue;
6063
6265
  }
6064
6266
  if (filePath) {
@@ -6067,13 +6269,13 @@ async function runValidationPhase(params) {
6067
6269
  const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
6068
6270
  const newResult2 = revalidation2.results[result.stepIndex];
6069
6271
  if (newResult2?.success) {
6070
- console.log(import_chalk17.default.green(` \u2713 Step ${stepNum} now passes`));
6272
+ console.log(import_chalk18.default.green(` \u2713 Step ${stepNum} now passes`));
6071
6273
  validationResult = revalidation2;
6072
6274
  continue;
6073
6275
  }
6074
6276
  result.error = newResult2?.error;
6075
6277
  result.suggestion = newResult2?.suggestion;
6076
- console.log(import_chalk17.default.red(` Still failing: ${result.error}`));
6278
+ console.log(import_chalk18.default.red(` Still failing: ${result.error}`));
6077
6279
  }
6078
6280
  }
6079
6281
  const fixTask = task(`Fixing step ${stepNum} with AI`);
@@ -6095,8 +6297,8 @@ async function runValidationPhase(params) {
6095
6297
  await savePlan(plan, planPath);
6096
6298
  fixTask.succeed("AI suggested a fix");
6097
6299
  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}`));
6300
+ console.log(import_chalk18.default.dim(` New target: ${fixedStep.target || "none"}`));
6301
+ if (fixedStep.value) console.log(import_chalk18.default.dim(` New value: ${fixedStep.value}`));
6100
6302
  } catch {
6101
6303
  fixTask.fail("AI could not fix automatically");
6102
6304
  }
@@ -6112,11 +6314,11 @@ async function runValidationPhase(params) {
6112
6314
  result.error = newResult?.error;
6113
6315
  result.suggestion = newResult?.suggestion;
6114
6316
  console.log();
6115
- console.log(import_chalk17.default.yellow(` Edit step ${stepNum} in: `) + import_chalk17.default.cyan(formatClickablePath(planPath)));
6317
+ console.log(import_chalk18.default.yellow(` Edit step ${stepNum} in: `) + import_chalk18.default.cyan(formatClickablePath(planPath)));
6116
6318
  console.log();
6117
6319
  const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
6118
6320
  if (choice.toLowerCase() === "skip") {
6119
- console.log(import_chalk17.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
6321
+ console.log(import_chalk18.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
6120
6322
  continue;
6121
6323
  }
6122
6324
  plan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
@@ -6143,31 +6345,31 @@ async function runValidationPhase(params) {
6143
6345
  }
6144
6346
 
6145
6347
  // src/commands/utils/chrome-extension.ts
6146
- var import_node_fs16 = require("fs");
6348
+ var import_node_fs17 = require("fs");
6147
6349
  var import_node_os3 = require("os");
6148
- var import_node_path23 = require("path");
6350
+ var import_node_path24 = require("path");
6149
6351
  function resolveChromeExtension(extensionId) {
6150
6352
  const platform = process.platform;
6151
6353
  const possibleDirs = [];
6152
6354
  const getChromeExtDirs = (userDataPath) => {
6153
6355
  const dirs = [];
6154
6356
  try {
6155
- const profiles = (0, import_node_fs16.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
6357
+ const profiles = (0, import_node_fs17.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
6156
6358
  for (const profile of profiles) {
6157
- dirs.push((0, import_node_path23.join)(userDataPath, profile, "Extensions"));
6359
+ dirs.push((0, import_node_path24.join)(userDataPath, profile, "Extensions"));
6158
6360
  }
6159
6361
  } catch {
6160
- dirs.push((0, import_node_path23.join)(userDataPath, "Default", "Extensions"));
6362
+ dirs.push((0, import_node_path24.join)(userDataPath, "Default", "Extensions"));
6161
6363
  }
6162
6364
  return dirs;
6163
6365
  };
6164
6366
  if (platform === "win32") {
6165
- possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
6367
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
6166
6368
  } else if (platform === "darwin") {
6167
- possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
6369
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
6168
6370
  } else {
6169
6371
  try {
6170
- const windowsUsers = (0, import_node_fs16.readdirSync)("/mnt/c/Users").filter(
6372
+ const windowsUsers = (0, import_node_fs17.readdirSync)("/mnt/c/Users").filter(
6171
6373
  (u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
6172
6374
  );
6173
6375
  for (const user of windowsUsers) {
@@ -6175,22 +6377,22 @@ function resolveChromeExtension(extensionId) {
6175
6377
  }
6176
6378
  } catch {
6177
6379
  }
6178
- possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
6380
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
6179
6381
  }
6180
6382
  for (const chromeExtDir of possibleDirs) {
6181
- const extDir = (0, import_node_path23.join)(chromeExtDir, extensionId);
6182
- if (!(0, import_node_fs16.existsSync)(extDir)) continue;
6383
+ const extDir = (0, import_node_path24.join)(chromeExtDir, extensionId);
6384
+ if (!(0, import_node_fs17.existsSync)(extDir)) continue;
6183
6385
  try {
6184
- const versions = (0, import_node_fs16.readdirSync)(extDir).filter((f) => !f.startsWith("."));
6386
+ const versions = (0, import_node_fs17.readdirSync)(extDir).filter((f) => !f.startsWith("."));
6185
6387
  if (versions.length === 0) continue;
6186
6388
  const latestVersion = versions.sort().pop();
6187
- const extPath = (0, import_node_path23.join)(extDir, latestVersion);
6389
+ const extPath = (0, import_node_path24.join)(extDir, latestVersion);
6188
6390
  let name = extensionId;
6189
6391
  let popupPath = "popup.html";
6190
- const manifestPath = (0, import_node_path23.join)(extPath, "manifest.json");
6191
- if ((0, import_node_fs16.existsSync)(manifestPath)) {
6392
+ const manifestPath = (0, import_node_path24.join)(extPath, "manifest.json");
6393
+ if ((0, import_node_fs17.existsSync)(manifestPath)) {
6192
6394
  try {
6193
- const manifest = JSON.parse((0, import_node_fs16.readFileSync)(manifestPath, "utf-8"));
6395
+ const manifest = JSON.parse((0, import_node_fs17.readFileSync)(manifestPath, "utf-8"));
6194
6396
  if (manifest.name && !manifest.name.startsWith("__MSG_")) {
6195
6397
  name = manifest.name;
6196
6398
  }
@@ -6221,16 +6423,16 @@ async function recordCommand(url, options = {}) {
6221
6423
  const extensionId = url.slice(7);
6222
6424
  chromeExtension = resolveChromeExtension(extensionId);
6223
6425
  if (!chromeExtension) {
6224
- console.log(import_chalk18.default.red(`
6426
+ console.log(import_chalk19.default.red(`
6225
6427
  \u2717 Chrome extension not found: ${extensionId}`));
6226
- console.log(import_chalk18.default.dim(` Make sure the extension is installed in Chrome
6428
+ console.log(import_chalk19.default.dim(` Make sure the extension is installed in Chrome
6227
6429
  `));
6228
6430
  process.exit(1);
6229
6431
  }
6230
6432
  config.browser = { extension: chromeExtension.path };
6231
6433
  url = chromeExtension.popupUrl;
6232
- console.log(import_chalk18.default.dim(` Extension: ${chromeExtension.name}`));
6233
- console.log(import_chalk18.default.dim(` Path: ${chromeExtension.path}`));
6434
+ console.log(import_chalk19.default.dim(` Extension: ${chromeExtension.name}`));
6435
+ console.log(import_chalk19.default.dim(` Path: ${chromeExtension.path}`));
6234
6436
  }
6235
6437
  config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
6236
6438
  const userData = await loadUserData();
@@ -6243,20 +6445,20 @@ async function recordCommand(url, options = {}) {
6243
6445
  if (!authResult) return;
6244
6446
  const { userEmail, userTier, localVideoCount } = authResult;
6245
6447
  if (configCreated) {
6246
- const configPath = (0, import_node_path24.join)(process.cwd(), CONFIG_FILE);
6448
+ const configPath = (0, import_node_path25.join)(process.cwd(), CONFIG_FILE);
6247
6449
  console.log();
6248
- console.log(import_chalk18.default.green(` \u2714 Created ${CONFIG_FILE}`));
6249
- console.log(import_chalk18.default.dim(` ${configPath}`));
6450
+ console.log(import_chalk19.default.green(` \u2714 Created ${CONFIG_FILE}`));
6451
+ console.log(import_chalk19.default.dim(` ${configPath}`));
6250
6452
  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"));
6453
+ console.log(import_chalk19.default.cyan(" Review the config file to customize:"));
6454
+ console.log(import_chalk19.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
6455
+ console.log(import_chalk19.default.dim(" \u2022 video: Resolution, quality, effects"));
6456
+ console.log(import_chalk19.default.dim(" \u2022 music: Background music path and volume"));
6457
+ console.log(import_chalk19.default.dim(" \u2022 output: Output directory"));
6256
6458
  console.log();
6257
6459
  const proceed = await confirm(`${INDENT3}Proceed with default settings?`);
6258
6460
  if (!proceed) {
6259
- console.log(import_chalk18.default.yellow(`
6461
+ console.log(import_chalk19.default.yellow(`
6260
6462
  Edit ${CONFIG_FILE} and run again.
6261
6463
  `));
6262
6464
  process.exit(0);
@@ -6264,17 +6466,17 @@ async function recordCommand(url, options = {}) {
6264
6466
  }
6265
6467
  if (langOverride) config.lang = langOverride;
6266
6468
  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)) {
6469
+ const musicPath = (0, import_node_path25.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path25.resolve)(config.output.dir, config.music.path);
6470
+ if (!(0, import_node_fs18.existsSync)(musicPath)) {
6269
6471
  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)}`));
6472
+ console.log(import_chalk19.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
6473
+ console.log(import_chalk19.default.dim(` Config: ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}`));
6272
6474
  console.log();
6273
6475
  const proceed = await confirm(`${INDENT3}Continue without background music?`);
6274
6476
  if (!proceed) {
6275
- console.log(import_chalk18.default.yellow(`
6477
+ console.log(import_chalk19.default.yellow(`
6276
6478
  Edit music.path in config and run again.`));
6277
- console.log(import_chalk18.default.dim(` ${(0, import_node_path24.join)(process.cwd(), CONFIG_FILE)}
6479
+ console.log(import_chalk19.default.dim(` ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}
6278
6480
  `));
6279
6481
  process.exit(0);
6280
6482
  }
@@ -6284,8 +6486,8 @@ async function recordCommand(url, options = {}) {
6284
6486
  const captions = await loadCaptions();
6285
6487
  const workDir = await prepareWorkDir();
6286
6488
  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");
6489
+ const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${targetLang}.json`);
6490
+ const legacyPlanPath = (0, import_node_path25.join)(config.output.dir, "plan.json");
6289
6491
  const isExtensionUrl = url.startsWith("chrome-extension://");
6290
6492
  await (0, import_promises17.mkdir)(config.output.dir, { recursive: true });
6291
6493
  await setLogFilePath(config.output.dir);
@@ -6298,7 +6500,8 @@ async function recordCommand(url, options = {}) {
6298
6500
  ...description !== void 0 && { description },
6299
6501
  planPath,
6300
6502
  legacyPlanPath,
6301
- targetLang
6503
+ targetLang,
6504
+ ...fresh !== void 0 && { fresh }
6302
6505
  });
6303
6506
  const validatedPlan = await runValidationPhase({
6304
6507
  plan,
@@ -6323,29 +6526,29 @@ async function recordCommand(url, options = {}) {
6323
6526
  await saveUserData({ localVideoCount: newCount });
6324
6527
  const remaining = FREE_VIDEO_LIMIT - newCount;
6325
6528
  if (remaining > 0) {
6326
- console.log(import_chalk18.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
6529
+ console.log(import_chalk19.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
6327
6530
  }
6328
6531
  }
6329
6532
  console.log();
6330
- console.log(import_chalk18.default.bold.green(" \u2713 Done!"));
6533
+ console.log(import_chalk19.default.bold.green(" \u2713 Done!"));
6331
6534
  console.log();
6332
- console.log(import_chalk18.default.cyan(` ${formatClickablePath(finalPath)}`));
6535
+ console.log(import_chalk19.default.cyan(` ${formatClickablePath(finalPath)}`));
6333
6536
  console.log();
6334
6537
  return finalPath;
6335
6538
  } catch (err) {
6336
6539
  await logError("Record Command", err);
6337
6540
  if (rethrow) throw err;
6338
6541
  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`));
6542
+ console.log(import_chalk19.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
6543
+ console.log(import_chalk19.default.dim(` See ${logFilePath} for details`));
6341
6544
  process.exit(1);
6342
6545
  }
6343
6546
  }
6344
6547
 
6345
6548
  // src/commands/script-cmd.ts
6346
6549
  var import_promises18 = require("fs/promises");
6347
- var import_node_path25 = require("path");
6348
- var import_chalk19 = __toESM(require("chalk"));
6550
+ var import_node_path26 = require("path");
6551
+ var import_chalk20 = __toESM(require("chalk"));
6349
6552
  init_lib();
6350
6553
  async function scriptCommand(url, scriptPath, lang) {
6351
6554
  const { config, workDir, captions } = await initCommandEnv(url, lang);
@@ -6353,7 +6556,7 @@ async function scriptCommand(url, scriptPath, lang) {
6353
6556
  const raw = await (0, import_promises18.readFile)(scriptPath, "utf-8");
6354
6557
  const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
6355
6558
  if (lines.length === 0) {
6356
- console.log(import_chalk19.default.red(" Script file is empty or has no non-comment lines"));
6559
+ console.log(import_chalk20.default.red(" Script file is empty or has no non-comment lines"));
6357
6560
  process.exit(1);
6358
6561
  }
6359
6562
  info(`${lines.length} captions from script`);
@@ -6365,7 +6568,7 @@ async function scriptCommand(url, scriptPath, lang) {
6365
6568
  const planTask = task("Mapping script to UI actions");
6366
6569
  const appTitle = new URL(url).hostname.replace(/^www\./, "");
6367
6570
  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`);
6571
+ const planPath = (0, import_node_path26.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
6369
6572
  await savePlan(plan, planPath);
6370
6573
  planTask.succeed(`${plan.steps.length} steps planned`);
6371
6574
  header("Voiceover");
@@ -6385,8 +6588,8 @@ async function scriptCommand(url, scriptPath, lang) {
6385
6588
  const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
6386
6589
  const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
6387
6590
  console.log();
6388
- console.log(import_chalk19.default.bold.green(" Done!"));
6389
- console.log(import_chalk19.default.cyan(` ${finalPath}`));
6591
+ console.log(import_chalk20.default.bold.green(" Done!"));
6592
+ console.log(import_chalk20.default.cyan(` ${finalPath}`));
6390
6593
  }
6391
6594
 
6392
6595
  // src/qp.ts
@@ -6456,6 +6659,20 @@ function parseArgs(args) {
6456
6659
  if (first === "record") {
6457
6660
  args = args.slice(1);
6458
6661
  }
6662
+ if (first === "edit") {
6663
+ const rest = args.slice(1);
6664
+ let url2;
6665
+ let lang2;
6666
+ for (const arg2 of rest) {
6667
+ if (arg2.startsWith("lang=")) {
6668
+ lang2 = arg2.slice(5);
6669
+ } else if (!arg2.startsWith("-") && !url2 && !arg2.includes("=")) {
6670
+ url2 = normalizeUrl3(arg2);
6671
+ }
6672
+ }
6673
+ if (!url2) return { ...base, command: "help" };
6674
+ return { ...base, command: "edit", url: url2, lang: lang2 };
6675
+ }
6459
6676
  if (first === "capture") {
6460
6677
  const rest = args.slice(1);
6461
6678
  let url2;
@@ -6523,8 +6740,8 @@ async function main() {
6523
6740
  break;
6524
6741
  case "auth":
6525
6742
  if (!parsed.url) {
6526
- console.log(import_chalk20.default.red(" Missing URL"));
6527
- console.log(import_chalk20.default.dim(" Run: qp auth <url>"));
6743
+ console.log(import_chalk21.default.red(" Missing URL"));
6744
+ console.log(import_chalk21.default.dim(" Run: qp auth <url>"));
6528
6745
  process.exit(1);
6529
6746
  }
6530
6747
  await authCommand(parsed.url);
@@ -6535,6 +6752,14 @@ async function main() {
6535
6752
  case "auth-list":
6536
6753
  await authListCommand();
6537
6754
  break;
6755
+ case "edit":
6756
+ if (!parsed.url) {
6757
+ console.log(import_chalk21.default.red(" Missing URL"));
6758
+ console.log(import_chalk21.default.dim(" Run: qp edit <url>"));
6759
+ process.exit(1);
6760
+ }
6761
+ await editCommand(parsed.url, parsed.lang);
6762
+ break;
6538
6763
  case "init":
6539
6764
  await initCommand();
6540
6765
  break;
@@ -6543,8 +6768,8 @@ async function main() {
6543
6768
  break;
6544
6769
  case "record":
6545
6770
  if (!parsed.url) {
6546
- console.log(import_chalk20.default.red(" Missing URL"));
6547
- console.log(import_chalk20.default.dim(" Run: qp --help"));
6771
+ console.log(import_chalk21.default.red(" Missing URL"));
6772
+ console.log(import_chalk21.default.dim(" Run: qp --help"));
6548
6773
  process.exit(1);
6549
6774
  }
6550
6775
  setAutoYes(parsed.yes);
@@ -6563,8 +6788,8 @@ async function main() {
6563
6788
  break;
6564
6789
  case "capture":
6565
6790
  if (!parsed.url) {
6566
- console.log(import_chalk20.default.red(" Missing URL"));
6567
- console.log(import_chalk20.default.dim(" Run: qp --help"));
6791
+ console.log(import_chalk21.default.red(" Missing URL"));
6792
+ console.log(import_chalk21.default.dim(" Run: qp --help"));
6568
6793
  process.exit(1);
6569
6794
  }
6570
6795
  console.log();