@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.
@@ -43,7 +43,7 @@ function getErrorMessage(err) {
43
43
  return err instanceof Error ? err.message : String(err);
44
44
  }
45
45
  function toTitleCase(text) {
46
- return text.replace(/\w+/g, (word, index) => {
46
+ return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
47
47
  if (index === 0) {
48
48
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
49
49
  }
@@ -107,6 +107,9 @@ var init_utils = __esm({
107
107
  });
108
108
 
109
109
  // src/lib/api-client.ts
110
+ function apiKey() {
111
+ return process.env.SPARK_AI_KEY || "";
112
+ }
110
113
  async function getOrRefreshDirectToken() {
111
114
  const now = Date.now();
112
115
  if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
@@ -115,7 +118,7 @@ async function getOrRefreshDirectToken() {
115
118
  try {
116
119
  const response = await fetch(SPARK_AI_TOKEN, {
117
120
  method: "POST",
118
- headers: { "X-Api-Key": API_KEY }
121
+ headers: { "X-Api-Key": apiKey() }
119
122
  });
120
123
  if (!response.ok) {
121
124
  throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
@@ -226,7 +229,7 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
226
229
  return lastRateLimitError || { success: false, error: "Request failed after retries" };
227
230
  }
228
231
  async function callAI(systemPrompt, userPrompt, tier = "free") {
229
- if (!API_KEY) {
232
+ if (!apiKey()) {
230
233
  return {
231
234
  success: false,
232
235
  error: "AI service is not configured. Please report this issue.",
@@ -286,7 +289,7 @@ function normalizeUrl(input) {
286
289
  url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
287
290
  return `https://${url}`;
288
291
  }
289
- var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, API_KEY, cachedDirectToken, cachedRelayToken;
292
+ var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
290
293
  var init_api_client = __esm({
291
294
  "src/lib/api-client.ts"() {
292
295
  "use strict";
@@ -297,7 +300,6 @@ var init_api_client = __esm({
297
300
  RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
298
301
  RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
299
302
  RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
300
- API_KEY = process.env.SPARK_AI_KEY || "";
301
303
  cachedDirectToken = null;
302
304
  cachedRelayToken = null;
303
305
  }
@@ -401,7 +403,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
401
403
  var init_config = __esm({
402
404
  "src/lib/config.ts"() {
403
405
  "use strict";
404
- VERSION = "1.1.1";
406
+ VERSION = "1.2.1";
405
407
  CONFIG_FILE = "quickpeek.config.json";
406
408
  DEFAULT_CONFIG = {
407
409
  plan: { maxSteps: 8 },
@@ -551,7 +553,7 @@ var init_scraping = __esm({
551
553
  });
552
554
 
553
555
  // src/lib/crawl.ts
554
- import { chromium } from "playwright";
556
+ import { chromium as chromium2 } from "playwright";
555
557
  function getActionForType(type) {
556
558
  switch (type) {
557
559
  case "button":
@@ -569,20 +571,29 @@ function getActionForType(type) {
569
571
  return "type";
570
572
  }
571
573
  }
572
- async function crawlPage(url, task2 = noopTask, storageStatePath) {
573
- const cached = crawlCache.get(url);
574
+ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
575
+ const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
576
+ const cached = crawlCache.get(cacheKey);
574
577
  if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
575
578
  task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
576
- return { title: cached.title, elements: [...cached.elements] };
579
+ return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
577
580
  }
578
581
  const browserTask = task2("Launching browser");
579
- const browser = await chromium.launch({ headless: true });
580
- const context = await browser.newContext(storageStatePath ? { storageState: storageStatePath } : {});
582
+ const browser = await chromium2.launch({ headless: true });
583
+ const context = await browser.newContext({
584
+ ...storageStatePath ? { storageState: storageStatePath } : {},
585
+ // Crawl at the size we will record at, or the measurements below describe
586
+ // a page nobody will ever see.
587
+ ...viewport ? { viewport } : {}
588
+ });
581
589
  const page = await context.newPage();
582
590
  browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
583
591
  const crawlTask = task2("Crawling page");
584
592
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
585
593
  const title = await page.title();
594
+ const scrollable = await page.evaluate(
595
+ "Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
596
+ );
586
597
  const rawElements = await page.evaluate(`
587
598
  (() => {
588
599
  const items = [];
@@ -712,12 +723,12 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
712
723
  });
713
724
  await browser.close();
714
725
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
715
- crawlCache.set(url, { timestamp: Date.now(), title, elements: [...elements] });
726
+ crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
716
727
  if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
717
728
  const oldest = crawlCache.keys().next().value;
718
729
  if (oldest !== void 0) crawlCache.delete(oldest);
719
730
  }
720
- return { title, elements };
731
+ return { title, elements, scrollable };
721
732
  }
722
733
  var noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
723
734
  var init_crawl = __esm({
@@ -1289,7 +1300,7 @@ EXAMPLES:
1289
1300
  \u2022 File upload: "target": "input[type=\\"file\\"]"
1290
1301
  \u2022 Slider: "target": "input[type=\\"range\\"]"`;
1291
1302
  }
1292
- function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
1303
+ function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
1293
1304
  const lang = langName ? `
1294
1305
  Language: Write ALL captions in ${langName}.` : "";
1295
1306
  const purpose = description ? `
@@ -1327,8 +1338,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
1327
1338
  ${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
1328
1339
  }
1329
1340
  const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
1341
+ const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
1342
+ const scrolling = cannotScroll ? `
1343
+
1344
+ SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
1345
+ 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" : "";
1330
1346
  return `URL: ${url}
1331
- Title: ${title}${purpose}
1347
+ Title: ${title}${purpose}${scrolling}
1332
1348
 
1333
1349
  AVAILABLE ELEMENTS (grouped by action):
1334
1350
  ${elementSummary}
@@ -1338,7 +1354,7 @@ REQUIREMENTS:
1338
1354
  \u2022 Complete ONE user journey: input \u2192 process \u2192 output
1339
1355
  \u2022 For CLICK: use simplified text target like "extract frame" or "download"
1340
1356
  \u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
1341
- \u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
1357
+ \u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
1342
1358
 
1343
1359
  Return JSON:
1344
1360
  {
@@ -1361,10 +1377,12 @@ TARGET SYNTAX:
1361
1377
  \u2022 Sliders: input[type="range"]
1362
1378
  \u2022 Text inputs: Use selector from list above`;
1363
1379
  }
1380
+ var MIN_USEFUL_SCROLL_PX;
1364
1381
  var init_prompts = __esm({
1365
1382
  "src/lib/prompts.ts"() {
1366
1383
  "use strict";
1367
1384
  init_utils();
1385
+ MIN_USEFUL_SCROLL_PX = 200;
1368
1386
  }
1369
1387
  });
1370
1388
 
@@ -2246,10 +2264,13 @@ function fileResult(output, data, poster) {
2246
2264
  }
2247
2265
 
2248
2266
  // src/mcp/tools.ts
2249
- import { readdirSync as readdirSync4, statSync as statSync2 } from "fs";
2250
- import { join as join22 } from "path";
2267
+ import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
2268
+ import { join as join23 } from "path";
2251
2269
  import { WIDE_VOICES as WIDE_VOICES2 } from "@spark-apps/video-kit";
2252
2270
 
2271
+ // src/browser/auth-session.ts
2272
+ import { chromium } from "playwright";
2273
+
2253
2274
  // src/browser/auth-state.ts
2254
2275
  import { existsSync as existsSync2 } from "fs";
2255
2276
  import { chmod, mkdir, readdir, unlink, writeFile } from "fs/promises";
@@ -2271,12 +2292,52 @@ function findAuthState(url) {
2271
2292
  const path = authStatePath(url);
2272
2293
  return path && existsSync2(path) ? path : void 0;
2273
2294
  }
2295
+ async function saveAuthState(url, state) {
2296
+ const path = authStatePath(url);
2297
+ if (!path) throw new Error(`Not a valid URL: ${url}`);
2298
+ await mkdir(AUTH_DIR, { recursive: true });
2299
+ await writeFile(path, JSON.stringify(state, null, 2));
2300
+ await chmod(path, 384);
2301
+ return path;
2302
+ }
2303
+
2304
+ // src/browser/auth-session.ts
2305
+ var pending = null;
2306
+ function pendingSignIn() {
2307
+ return pending?.url ?? null;
2308
+ }
2309
+ async function openSignIn(url) {
2310
+ await cancelSignIn();
2311
+ const browser = await chromium.launch({ headless: false });
2312
+ const context = await browser.newContext();
2313
+ const page = await context.newPage();
2314
+ pending = { browser, context, url };
2315
+ try {
2316
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
2317
+ } catch {
2318
+ }
2319
+ }
2320
+ async function saveSignIn() {
2321
+ if (!pending) throw new Error("No sign-in window is open. Call start_auth first.");
2322
+ const { context, url } = pending;
2323
+ try {
2324
+ const path = await saveAuthState(url, await context.storageState());
2325
+ return { host: new URL(url).host, path };
2326
+ } finally {
2327
+ await cancelSignIn();
2328
+ }
2329
+ }
2330
+ async function cancelSignIn() {
2331
+ const open = pending;
2332
+ pending = null;
2333
+ await open?.browser.close().catch(() => {
2334
+ });
2335
+ }
2274
2336
 
2275
- // src/commands/record-cmd.ts
2276
- import { existsSync as existsSync17 } from "fs";
2277
- import { mkdir as mkdir8 } from "fs/promises";
2278
- import { isAbsolute as isAbsolute5, join as join20, resolve as resolve9 } from "path";
2279
- import chalk14 from "chalk";
2337
+ // src/commands/edit-cmd.ts
2338
+ import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync3 } from "fs";
2339
+ import { join as join10 } from "path";
2340
+ import chalk7 from "chalk";
2280
2341
 
2281
2342
  // src/help.ts
2282
2343
  init_lib();
@@ -2341,66 +2402,123 @@ function formatUsageLines(status, tier) {
2341
2402
  return lines;
2342
2403
  }
2343
2404
 
2344
- // src/commands/record-cmd.ts
2345
- init_lib();
2346
- init_config_loader();
2405
+ // src/lib/browser-open.ts
2406
+ import { exec as exec2 } from "child_process";
2407
+ function editorBaseUrl() {
2408
+ return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
2409
+ }
2410
+ function openInBrowser(url) {
2411
+ const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
2412
+ const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
2413
+ return new Promise((resolve10) => {
2414
+ exec2(command, () => resolve10());
2415
+ });
2416
+ }
2347
2417
 
2348
- // src/utils/logging.ts
2349
- import { appendFile, writeFile as writeFile5 } from "fs/promises";
2350
- import { join as join8 } from "path";
2351
- var LOG_FILE_NAME = "quickpeek.log";
2352
- var logFileInitialized = false;
2353
- var logFilePath = LOG_FILE_NAME;
2354
- async function writeLogEntry(entry) {
2355
- try {
2356
- if (!logFileInitialized) {
2357
- await writeFile5(logFilePath, entry);
2358
- logFileInitialized = true;
2359
- } else {
2360
- await appendFile(logFilePath, entry);
2418
+ // src/lib/plan-validation.ts
2419
+ var INVALID_TARGETS = [
2420
+ /\.png$/i,
2421
+ /\.jpg$/i,
2422
+ /\.gif$/i,
2423
+ /\.webp$/i,
2424
+ /\.svg$/i,
2425
+ /sprite_sheet/i,
2426
+ /downloaded/i
2427
+ ];
2428
+ var ACTIONS_REQUIRING_TARGET = ["click", "type", "select", "check", "drag"];
2429
+ function isFilePath(value) {
2430
+ if (!value) return false;
2431
+ return /\.\w{2,5}$/.test(value) && (/[/\\]/.test(value) || /^[a-zA-Z]:/.test(value));
2432
+ }
2433
+ function isInvalidTarget(target) {
2434
+ return INVALID_TARGETS.some((pattern) => pattern.test(target));
2435
+ }
2436
+ function validateStep(step) {
2437
+ if (!step?.action) {
2438
+ return "Missing action";
2439
+ }
2440
+ if (step.action === "navigate") {
2441
+ if (!step.target) return "Navigate requires a URL target";
2442
+ if (!step.target.startsWith("http://") && !step.target.startsWith("https://")) {
2443
+ return "Navigate target must be a valid URL";
2361
2444
  }
2362
- } catch {
2363
2445
  }
2446
+ if (step.action === "media") {
2447
+ if (!step.mediaFile && !isFilePath(step.value)) {
2448
+ return "Media step requires a file (image or video)";
2449
+ }
2450
+ }
2451
+ if (step.action === "upload") {
2452
+ if (!step.mediaFile && !isFilePath(step.value)) {
2453
+ return "Upload step requires a file";
2454
+ }
2455
+ }
2456
+ if (ACTIONS_REQUIRING_TARGET.includes(step.action)) {
2457
+ if (!step.target) return `${step.action} requires a target`;
2458
+ if (isInvalidTarget(step.target)) return `Invalid target pattern: ${step.target}`;
2459
+ }
2460
+ return null;
2364
2461
  }
2365
- async function setLogFilePath(outputDir) {
2366
- logFilePath = join8(outputDir, LOG_FILE_NAME);
2367
- const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
2368
- ${"=".repeat(60)}
2369
2462
 
2463
+ // src/lib/plan-file.ts
2464
+ var MAX_HANDOFF_URL_CHARS = 32e3;
2465
+ function serializePlan(plan) {
2466
+ return `${JSON.stringify(plan, null, 2)}
2370
2467
  `;
2468
+ }
2469
+ function parsePlanText(text) {
2470
+ let raw;
2371
2471
  try {
2372
- await writeFile5(logFilePath, header2);
2373
- logFileInitialized = true;
2374
- } catch {
2472
+ raw = JSON.parse(text);
2473
+ } catch (e) {
2474
+ return { ok: false, error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
2375
2475
  }
2476
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2477
+ return { ok: false, error: "A plan must be a JSON object." };
2478
+ }
2479
+ const candidate = raw;
2480
+ if (typeof candidate.url !== "string" || !candidate.url.trim()) {
2481
+ return { ok: false, error: "A plan needs a `url` to record." };
2482
+ }
2483
+ if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
2484
+ return { ok: false, error: "A plan needs at least one step." };
2485
+ }
2486
+ const steps = [];
2487
+ for (const [index, step] of candidate.steps.entries()) {
2488
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
2489
+ return { ok: false, error: `Step ${index + 1} is not an object.` };
2490
+ }
2491
+ const typed = step;
2492
+ if (typeof typed.action !== "string" || !typed.action.trim()) {
2493
+ return { ok: false, error: `Step ${index + 1} has no action.` };
2494
+ }
2495
+ const failure = validateStep(typed);
2496
+ if (failure) return { ok: false, error: `Step ${index + 1}: ${failure}` };
2497
+ steps.push({ ...typed, id: typed.id ?? index + 1 });
2498
+ }
2499
+ return {
2500
+ ok: true,
2501
+ plan: {
2502
+ url: candidate.url,
2503
+ title: candidate.title ?? "",
2504
+ description: candidate.description ?? "",
2505
+ ...candidate.lang ? { lang: candidate.lang } : {},
2506
+ steps
2507
+ }
2508
+ };
2376
2509
  }
2377
- async function logInfo(context, message) {
2378
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
2379
- `);
2380
- }
2381
- async function logDebug(context, message) {
2382
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
2383
- `);
2510
+ function toBase64Url(text) {
2511
+ const bytes = new TextEncoder().encode(text);
2512
+ const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
2513
+ const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(text, "utf8").toString("base64");
2514
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2384
2515
  }
2385
- async function logError(context, error) {
2386
- let errorMsg;
2387
- if (error instanceof Error) {
2388
- errorMsg = `${error.message}
2389
- ${error.stack}`;
2390
- } else if (typeof error === "object" && error !== null) {
2391
- errorMsg = JSON.stringify(error, null, 2);
2392
- } else {
2393
- errorMsg = String(error);
2394
- }
2395
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
2396
- ${errorMsg}
2397
-
2398
- `);
2516
+ function encodePlan(plan) {
2517
+ return toBase64Url(serializePlan(plan));
2399
2518
  }
2400
- async function logStep(stepNum, action, details) {
2401
- const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
2402
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
2403
- `);
2519
+ function planEditorUrl(baseUrl, plan) {
2520
+ const url = `${baseUrl.replace(/\/$/, "")}/edit#plan=${encodePlan(plan)}`;
2521
+ return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
2404
2522
  }
2405
2523
 
2406
2524
  // src/utils/ui.ts
@@ -2446,7 +2564,7 @@ function promptInput(prompt2) {
2446
2564
  var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
2447
2565
  function formatClickablePath(filePath) {
2448
2566
  const absolutePath = resolve4(filePath);
2449
- if (_isWSL) {
2567
+ if (_isWSL || process.platform !== "win32") {
2450
2568
  return absolutePath;
2451
2569
  }
2452
2570
  let formatted = absolutePath;
@@ -2462,15 +2580,15 @@ function formatDuration(ms) {
2462
2580
  return `${mins}:${secs.toString().padStart(2, "0")}`;
2463
2581
  }
2464
2582
 
2465
- // src/commands/phases/auth-phase.ts
2583
+ // src/commands/utils/record-helpers.ts
2584
+ import chalk6 from "chalk";
2466
2585
  init_lib();
2467
- import chalk8 from "chalk";
2468
2586
 
2469
2587
  // src/plan.ts
2470
- import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile6 } from "fs/promises";
2471
- import { join as join9 } from "path";
2588
+ import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile5 } from "fs/promises";
2589
+ import { join as join8 } from "path";
2472
2590
  import chalk5 from "chalk";
2473
- import { chromium as chromium4 } from "playwright";
2591
+ import { chromium as chromium5 } from "playwright";
2474
2592
 
2475
2593
  // src/ai/client.ts
2476
2594
  init_lib();
@@ -2478,19 +2596,11 @@ import { createInterface as createInterface2 } from "readline";
2478
2596
  import chalk4 from "chalk";
2479
2597
 
2480
2598
  // src/utils/browser-auth.ts
2481
- init_SparkStripe_feature_gating();
2482
2599
  import { createServer } from "http";
2483
2600
  import { URL as URL2 } from "url";
2484
2601
  import chalk3 from "chalk";
2602
+ init_SparkStripe_feature_gating();
2485
2603
  var TIMEOUT_MS = 12e4;
2486
- async function openBrowser(url) {
2487
- const { exec: exec2 } = await import("child_process");
2488
- const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
2489
- const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
2490
- return new Promise((resolve10) => {
2491
- exec2(command, () => resolve10());
2492
- });
2493
- }
2494
2604
  async function browserLogin() {
2495
2605
  return new Promise((resolve10) => {
2496
2606
  let server;
@@ -2544,7 +2654,7 @@ async function browserLogin() {
2544
2654
  console.log(chalk3.dim(` ${url}`));
2545
2655
  console.log();
2546
2656
  console.log(chalk3.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
2547
- openBrowser(url);
2657
+ void openInBrowser(url);
2548
2658
  });
2549
2659
  timer = setTimeout(() => {
2550
2660
  console.log();
@@ -2646,13 +2756,13 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
2646
2756
 
2647
2757
  // src/browser/launch.ts
2648
2758
  import { mkdir as mkdir4 } from "fs/promises";
2649
- import { chromium as chromium3 } from "playwright";
2759
+ import { chromium as chromium4 } from "playwright";
2650
2760
 
2651
2761
  // src/browser/wsl.ts
2652
2762
  import { execSync, spawn as spawn2 } from "child_process";
2653
2763
  import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
2654
2764
  import { request } from "http";
2655
- import { chromium as chromium2 } from "playwright";
2765
+ import { chromium as chromium3 } from "playwright";
2656
2766
  function isWSL() {
2657
2767
  if (process.platform !== "linux") return false;
2658
2768
  try {
@@ -2771,7 +2881,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
2771
2881
  const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
2772
2882
  try {
2773
2883
  await waitForChrome(windowsHost, proxyPort);
2774
- const browser = await chromium2.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
2884
+ const browser = await chromium3.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
2775
2885
  return {
2776
2886
  browser,
2777
2887
  cleanup: async () => {
@@ -2812,7 +2922,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
2812
2922
  const contexts = chromeSession.browser.contexts();
2813
2923
  context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
2814
2924
  } catch {
2815
- context = await chromium3.launchPersistentContext(userDataDir, {
2925
+ context = await chromium4.launchPersistentContext(userDataDir, {
2816
2926
  headless: false,
2817
2927
  args: persistentArgs,
2818
2928
  viewport,
@@ -2821,7 +2931,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
2821
2931
  });
2822
2932
  }
2823
2933
  } else {
2824
- context = await chromium3.launchPersistentContext(userDataDir, {
2934
+ context = await chromium4.launchPersistentContext(userDataDir, {
2825
2935
  headless: false,
2826
2936
  args: persistentArgs,
2827
2937
  viewport,
@@ -2830,7 +2940,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
2830
2940
  });
2831
2941
  }
2832
2942
  } else {
2833
- browser = await chromium3.launch({ headless: true });
2943
+ browser = await chromium4.launch({ headless: true });
2834
2944
  context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
2835
2945
  }
2836
2946
  return { context, browser, chromeSession };
@@ -2848,12 +2958,12 @@ async function cleanupBrowserSession(session) {
2848
2958
  init_lib();
2849
2959
  async function crawlExtension(url, extensionPath, workDir, task2) {
2850
2960
  const browserTask = task2("Launching browser with extension");
2851
- const userDataDir = join9(workDir, ".browser-data");
2961
+ const userDataDir = join8(workDir, ".browser-data");
2852
2962
  await mkdir5(userDataDir, { recursive: true });
2853
2963
  let context;
2854
2964
  let chromeSession = null;
2855
2965
  const useWindowsChrome = isWSL() && tryEnableWSLInterop();
2856
- const launchPersistent = () => chromium4.launchPersistentContext(userDataDir, {
2966
+ const launchPersistent = () => chromium5.launchPersistentContext(userDataDir, {
2857
2967
  headless: false,
2858
2968
  args: [
2859
2969
  `--disable-extensions-except=${extensionPath}`,
@@ -2972,12 +3082,12 @@ async function parsePlanJson(content, task2, logError2, userEmail, tier = "free"
2972
3082
  }
2973
3083
  }
2974
3084
  }
2975
- async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free") {
2976
- const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2, findAuthState(url));
3085
+ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
3086
+ const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
2977
3087
  const aiTask = task2("Generating plan with AI");
2978
3088
  const langName = getLanguageName(lang);
2979
3089
  const systemPrompt = buildSystemPrompt(maxSteps);
2980
- const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps);
3090
+ const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
2981
3091
  try {
2982
3092
  const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
2983
3093
  const jsonMatch = content.match(/\{[\s\S]*\}/);
@@ -3006,7 +3116,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
3006
3116
  }
3007
3117
  const renumberTask = task2("Validating plan structure");
3008
3118
  const originalCount = plan.steps.length;
3009
- const INVALID_TARGETS = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
3119
+ const INVALID_TARGETS2 = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
3010
3120
  const isValidStep = (s) => {
3011
3121
  if (!s?.action) return false;
3012
3122
  if (s.action === "navigate") {
@@ -3015,7 +3125,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
3015
3125
  }
3016
3126
  if (["click", "type", "select", "check", "upload", "drag"].includes(s.action)) {
3017
3127
  if (!s.target) return false;
3018
- if (INVALID_TARGETS.some((pattern) => pattern.test(s.target))) return false;
3128
+ if (INVALID_TARGETS2.some((pattern) => pattern.test(s.target))) return false;
3019
3129
  }
3020
3130
  if (s.action === "upload") {
3021
3131
  if (!s.target?.includes("file") && !s.target?.includes("input")) return false;
@@ -3032,7 +3142,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
3032
3142
  renumberTask.fail("No valid steps");
3033
3143
  throw new Error("Plan has no valid steps");
3034
3144
  }
3035
- await writeFile6(planPath, JSON.stringify(plan, null, 2));
3145
+ await writeFile5(planPath, JSON.stringify(plan, null, 2));
3036
3146
  const removed = originalCount - plan.steps.length;
3037
3147
  renumberTask.succeed(
3038
3148
  removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
@@ -3040,67 +3150,68 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
3040
3150
  return plan;
3041
3151
  }
3042
3152
 
3043
- // src/commands/phases/auth-phase.ts
3153
+ // src/commands/utils/record-helpers.ts
3044
3154
  init_config_loader();
3045
3155
 
3046
- // src/commands/utils/auth-helpers.ts
3047
- init_lib();
3048
- import chalk6 from "chalk";
3049
- async function handleRegistrationGate() {
3050
- console.log();
3051
- console.log(chalk6.yellow(" \u{1F389} You've made 5 free videos!"));
3052
- console.log(chalk6.white(" Sign in to continue creating videos."));
3053
- console.log();
3054
- console.log(chalk6.dim(" Benefits of signing in:"));
3055
- console.log(chalk6.dim(" \u2022 20 more videos per month"));
3056
- console.log(chalk6.dim(" \u2022 Keep your video history"));
3057
- console.log(chalk6.dim(" \u2022 Upgrade to Pro anytime"));
3058
- let email = await browserLogin();
3059
- if (!email) {
3060
- console.log();
3061
- email = await promptInput("Email: ");
3062
- }
3063
- if (!email || !email.includes("@")) {
3064
- console.log(chalk6.red(" Invalid email. Run qp again to try again."));
3065
- return null;
3066
- }
3067
- const registerTask = task("Sending verification email");
3068
- const result = await registerFreeUser(email);
3069
- if (!result.success) {
3070
- registerTask.fail(result.error || "Registration failed");
3071
- return null;
3156
+ // src/utils/logging.ts
3157
+ import { appendFile, writeFile as writeFile6 } from "fs/promises";
3158
+ import { join as join9 } from "path";
3159
+ var LOG_FILE_NAME = "quickpeek.log";
3160
+ var logFileInitialized = false;
3161
+ var logFilePath = LOG_FILE_NAME;
3162
+ async function writeLogEntry(entry) {
3163
+ try {
3164
+ if (!logFileInitialized) {
3165
+ await writeFile6(logFilePath, entry);
3166
+ logFileInitialized = true;
3167
+ } else {
3168
+ await appendFile(logFilePath, entry);
3169
+ }
3170
+ } catch {
3072
3171
  }
3073
- if (result.alreadyVerified) {
3074
- registerTask.succeed("Already verified!");
3075
- return { email, verified: true };
3172
+ }
3173
+ async function setLogFilePath(outputDir) {
3174
+ logFilePath = join9(outputDir, LOG_FILE_NAME);
3175
+ const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
3176
+ ${"=".repeat(60)}
3177
+
3178
+ `;
3179
+ try {
3180
+ await writeFile6(logFilePath, header2);
3181
+ logFileInitialized = true;
3182
+ } catch {
3076
3183
  }
3077
- registerTask.succeed("Check your email!");
3078
- console.log();
3079
- console.log(chalk6.cyan(" \u{1F4E7} Verification email sent to:"));
3080
- console.log(chalk6.white(` ${email}`));
3081
- console.log();
3082
- console.log(chalk6.dim(" Click the link in the email, then press Enter to continue..."));
3083
- await promptInput("");
3084
- const verifyTask = task("Checking verification");
3085
- const verified = await isVerified(email);
3086
- if (verified) {
3087
- verifyTask.succeed("Email verified!");
3088
- return { email, verified: true };
3184
+ }
3185
+ async function logInfo(context, message) {
3186
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
3187
+ `);
3188
+ }
3189
+ async function logDebug(context, message) {
3190
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
3191
+ `);
3192
+ }
3193
+ async function logError(context, error) {
3194
+ let errorMsg;
3195
+ if (error instanceof Error) {
3196
+ errorMsg = `${error.message}
3197
+ ${error.stack}`;
3198
+ } else if (typeof error === "object" && error !== null) {
3199
+ errorMsg = JSON.stringify(error, null, 2);
3200
+ } else {
3201
+ errorMsg = String(error);
3089
3202
  }
3090
- verifyTask.fail("Not verified yet");
3091
- console.log();
3092
- console.log(chalk6.yellow(" Email not verified. Check your inbox and spam folder."));
3093
- console.log(chalk6.dim(" Run qp again after clicking the verification link."));
3094
- console.log();
3095
- console.log(chalk6.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
3096
- console.log();
3097
- return { email, verified: false };
3203
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
3204
+ ${errorMsg}
3205
+
3206
+ `);
3207
+ }
3208
+ async function logStep(stepNum, action, details) {
3209
+ const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
3210
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
3211
+ `);
3098
3212
  }
3099
3213
 
3100
3214
  // src/commands/utils/record-helpers.ts
3101
- import chalk7 from "chalk";
3102
- init_lib();
3103
- init_config_loader();
3104
3215
  init_utils();
3105
3216
  var FREE_VIDEO_LIMIT = 5;
3106
3217
  var INDENT2 = " ";
@@ -3124,41 +3235,41 @@ function displayRateLimitInfo() {
3124
3235
  const percentage = Math.round(limits.used / limits.limit * 100);
3125
3236
  if (percentage >= 90) {
3126
3237
  console.log();
3127
- console.log(chalk7.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
3238
+ console.log(chalk6.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
3128
3239
  if (limits.tier === "free") {
3129
- console.log(chalk7.dim(` Upgrade: ${getUpgradeUrl()}`));
3240
+ console.log(chalk6.dim(` Upgrade: ${getUpgradeUrl()}`));
3130
3241
  }
3131
3242
  } else if (percentage >= 50) {
3132
- console.log(chalk7.dim(` ${remaining}/${limits.limit} requests remaining`));
3243
+ console.log(chalk6.dim(` ${remaining}/${limits.limit} requests remaining`));
3133
3244
  }
3134
3245
  }
3135
3246
  async function displayUsageStatus(userEmail, userTier, _localVideoCount = 0) {
3136
3247
  const tier = userTier === "pro" ? "pro" : "free";
3137
- const tierLabel = tier === "pro" ? chalk7.green("Pro") : "Free";
3138
- console.log(chalk7.cyan(` \u2714 ${tierLabel} plan`));
3139
- console.log(chalk7.dim(` ${userEmail}`));
3248
+ const tierLabel = tier === "pro" ? chalk6.green("Pro") : "Free";
3249
+ console.log(chalk6.cyan(` \u2714 ${tierLabel} plan`));
3250
+ console.log(chalk6.dim(` ${userEmail}`));
3140
3251
  if (userEmail && userEmail !== "anonymous") {
3141
3252
  try {
3142
3253
  const status = await getStatusCached(userEmail);
3143
3254
  for (const line of formatUsageLines(status, tier)) {
3144
- console.log(chalk7.dim(` ${line}`));
3255
+ console.log(chalk6.dim(` ${line}`));
3145
3256
  }
3146
3257
  } catch {
3147
3258
  }
3148
3259
  }
3149
3260
  if (userTier === "free") {
3150
3261
  const upgradeUrl = getUpgradeUrl(userEmail !== "anonymous" ? userEmail : void 0);
3151
- console.log(chalk7.dim(` Upgrade: ${upgradeUrl}`));
3262
+ console.log(chalk6.dim(` Upgrade: ${upgradeUrl}`));
3152
3263
  }
3153
3264
  }
3154
3265
  function displayPlanSummary(plan) {
3155
3266
  console.log();
3156
- console.log(chalk7.dim(` ${plan.title || "Untitled"}`));
3157
- console.log(chalk7.dim(` ${plan.steps?.length || 0} steps`));
3267
+ console.log(chalk6.dim(` ${plan.title || "Untitled"}`));
3268
+ console.log(chalk6.dim(` ${plan.steps?.length || 0} steps`));
3158
3269
  for (const step of plan.steps || []) {
3159
3270
  const text = step.caption || step.action || "";
3160
3271
  const caption = text.length > 50 ? `${text.slice(0, 47)}...` : text;
3161
- console.log(chalk7.dim(` ${step.id}. ${caption}`));
3272
+ console.log(chalk6.dim(` ${step.id}. ${caption}`));
3162
3273
  }
3163
3274
  }
3164
3275
  function getOutputDirFromUrl(url) {
@@ -3205,6 +3316,86 @@ function getOutputDirFromUrl(url) {
3205
3316
  }
3206
3317
  }
3207
3318
 
3319
+ // src/commands/edit-cmd.ts
3320
+ var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
3321
+ function findPlanFile(dir, lang) {
3322
+ if (lang) {
3323
+ const exact = join10(dir, `plan-${lang}.json`);
3324
+ return existsSync7(exact) ? exact : null;
3325
+ }
3326
+ if (!existsSync7(dir)) return null;
3327
+ const plans = readdirSync(dir).filter((f) => PLAN_FILE.test(f));
3328
+ const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
3329
+ return preferred ? join10(dir, preferred) : null;
3330
+ }
3331
+
3332
+ // src/commands/record-cmd.ts
3333
+ import { existsSync as existsSync18 } from "fs";
3334
+ import { mkdir as mkdir8 } from "fs/promises";
3335
+ import { isAbsolute as isAbsolute5, join as join21, resolve as resolve9 } from "path";
3336
+ import chalk15 from "chalk";
3337
+ init_lib();
3338
+ init_config_loader();
3339
+
3340
+ // src/commands/phases/auth-phase.ts
3341
+ init_lib();
3342
+ import chalk9 from "chalk";
3343
+ init_config_loader();
3344
+
3345
+ // src/commands/utils/auth-helpers.ts
3346
+ init_lib();
3347
+ import chalk8 from "chalk";
3348
+ async function handleRegistrationGate() {
3349
+ console.log();
3350
+ console.log(chalk8.yellow(" \u{1F389} You've made 5 free videos!"));
3351
+ console.log(chalk8.white(" Sign in to continue creating videos."));
3352
+ console.log();
3353
+ console.log(chalk8.dim(" Benefits of signing in:"));
3354
+ console.log(chalk8.dim(" \u2022 20 more videos per month"));
3355
+ console.log(chalk8.dim(" \u2022 Keep your video history"));
3356
+ console.log(chalk8.dim(" \u2022 Upgrade to Pro anytime"));
3357
+ let email = await browserLogin();
3358
+ if (!email) {
3359
+ console.log();
3360
+ email = await promptInput("Email: ");
3361
+ }
3362
+ if (!email || !email.includes("@")) {
3363
+ console.log(chalk8.red(" Invalid email. Run qp again to try again."));
3364
+ return null;
3365
+ }
3366
+ const registerTask = task("Sending verification email");
3367
+ const result = await registerFreeUser(email);
3368
+ if (!result.success) {
3369
+ registerTask.fail(result.error || "Registration failed");
3370
+ return null;
3371
+ }
3372
+ if (result.alreadyVerified) {
3373
+ registerTask.succeed("Already verified!");
3374
+ return { email, verified: true };
3375
+ }
3376
+ registerTask.succeed("Check your email!");
3377
+ console.log();
3378
+ console.log(chalk8.cyan(" \u{1F4E7} Verification email sent to:"));
3379
+ console.log(chalk8.white(` ${email}`));
3380
+ console.log();
3381
+ console.log(chalk8.dim(" Click the link in the email, then press Enter to continue..."));
3382
+ await promptInput("");
3383
+ const verifyTask = task("Checking verification");
3384
+ const verified = await isVerified(email);
3385
+ if (verified) {
3386
+ verifyTask.succeed("Email verified!");
3387
+ return { email, verified: true };
3388
+ }
3389
+ verifyTask.fail("Not verified yet");
3390
+ console.log();
3391
+ console.log(chalk8.yellow(" Email not verified. Check your inbox and spam folder."));
3392
+ console.log(chalk8.dim(" Run qp again after clicking the verification link."));
3393
+ console.log();
3394
+ console.log(chalk8.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
3395
+ console.log();
3396
+ return { email, verified: false };
3397
+ }
3398
+
3208
3399
  // src/commands/phases/auth-phase.ts
3209
3400
  async function runAuthPhase(input) {
3210
3401
  let { userEmail, userTier, localVideoCount } = input;
@@ -3228,7 +3419,7 @@ async function runAuthPhase(input) {
3228
3419
  } else {
3229
3420
  verifyTask.fail("Email not verified");
3230
3421
  console.log();
3231
- console.log(chalk8.yellow(" Check your inbox for the verification email."));
3422
+ console.log(chalk9.yellow(" Check your inbox for the verification email."));
3232
3423
  console.log();
3233
3424
  const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
3234
3425
  if (input2.toLowerCase() === "exit") {
@@ -3253,8 +3444,8 @@ async function runAuthPhase(input) {
3253
3444
  registerTask.succeed("Check your email!");
3254
3445
  await saveUserData({ email: newEmail, verified: false });
3255
3446
  console.log();
3256
- console.log(chalk8.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
3257
- console.log(chalk8.dim(" Click the link in the email, then run qp again."));
3447
+ console.log(chalk9.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
3448
+ console.log(chalk9.dim(" Click the link in the email, then run qp again."));
3258
3449
  console.log();
3259
3450
  gracefulExit();
3260
3451
  return null;
@@ -3270,7 +3461,7 @@ async function runAuthPhase(input) {
3270
3461
  } else {
3271
3462
  recheckTask.fail("Still not verified");
3272
3463
  console.log();
3273
- console.log(chalk8.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
3464
+ console.log(chalk9.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
3274
3465
  console.log();
3275
3466
  gracefulExit();
3276
3467
  return null;
@@ -3297,8 +3488,8 @@ async function runAuthPhase(input) {
3297
3488
  } else {
3298
3489
  const remaining = FREE_VIDEO_LIMIT - localVideoCount;
3299
3490
  console.log();
3300
- console.log(chalk8.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
3301
- console.log(chalk8.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
3491
+ console.log(chalk9.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
3492
+ console.log(chalk9.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
3302
3493
  userEmail = await setupUserEmail();
3303
3494
  let registeredVerified = false;
3304
3495
  try {
@@ -3317,10 +3508,10 @@ async function runAuthPhase(input) {
3317
3508
  }
3318
3509
 
3319
3510
  // src/commands/phases/plan-phase.ts
3320
- import { existsSync as existsSync7, readdirSync } from "fs";
3511
+ import { existsSync as existsSync8, readdirSync as readdirSync2 } from "fs";
3321
3512
  import { readFile as readFile4 } from "fs/promises";
3322
- import { basename, join as join10 } from "path";
3323
- import chalk9 from "chalk";
3513
+ import { basename, join as join11 } from "path";
3514
+ import chalk10 from "chalk";
3324
3515
 
3325
3516
  // src/ai/fix.ts
3326
3517
  init_lib();
@@ -3473,12 +3664,22 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
3473
3664
  init_config_loader();
3474
3665
  var INDENT3 = " ";
3475
3666
  async function runPlanPhase(params) {
3476
- const { url, config, userEmail, description, planPath, legacyPlanPath, targetLang, userTier = "free" } = params;
3667
+ const {
3668
+ url,
3669
+ config,
3670
+ userEmail,
3671
+ description,
3672
+ planPath,
3673
+ legacyPlanPath,
3674
+ targetLang,
3675
+ userTier = "free",
3676
+ fresh = false
3677
+ } = params;
3477
3678
  const maxSteps = config.plan?.maxSteps ?? 8;
3478
3679
  const extensionPath = config.browser?.extension;
3479
3680
  let existingPlan = null;
3480
3681
  let existingPlanPath = null;
3481
- if (existsSync7(planPath)) {
3682
+ if (existsSync8(planPath)) {
3482
3683
  try {
3483
3684
  const parsed = JSON.parse(await readFile4(planPath, "utf-8"));
3484
3685
  if (parsed.url && parsed.steps?.length > 0) {
@@ -3490,10 +3691,10 @@ async function runPlanPhase(params) {
3490
3691
  }
3491
3692
  if (!existingPlan) {
3492
3693
  try {
3493
- const files = readdirSync(config.output.dir);
3694
+ const files = readdirSync2(config.output.dir);
3494
3695
  const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== basename(planPath));
3495
3696
  for (const file of planFiles) {
3496
- const filePath = join10(config.output.dir, file);
3697
+ const filePath = join11(config.output.dir, file);
3497
3698
  try {
3498
3699
  const parsed = JSON.parse(await readFile4(filePath, "utf-8"));
3499
3700
  if (parsed.url && parsed.steps?.length > 0) {
@@ -3507,7 +3708,7 @@ async function runPlanPhase(params) {
3507
3708
  } catch {
3508
3709
  }
3509
3710
  }
3510
- if (!existingPlan && existsSync7(legacyPlanPath)) {
3711
+ if (!existingPlan && existsSync8(legacyPlanPath)) {
3511
3712
  try {
3512
3713
  const parsed = JSON.parse(await readFile4(legacyPlanPath, "utf-8"));
3513
3714
  if (parsed.url && parsed.steps?.length > 0) {
@@ -3533,25 +3734,25 @@ async function runPlanPhase(params) {
3533
3734
  translateTask.fail(failMsg);
3534
3735
  }
3535
3736
  };
3536
- if (existingPlan && existingPlanPath) {
3737
+ if (existingPlan && existingPlanPath && !fresh) {
3537
3738
  console.log();
3538
- console.log(chalk9.dim(` Found existing plan: `) + chalk9.cyan(formatClickablePath(existingPlanPath)));
3539
- console.log(chalk9.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
3739
+ console.log(chalk10.dim(` Found existing plan: `) + chalk10.cyan(formatClickablePath(existingPlanPath)));
3740
+ console.log(chalk10.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
3540
3741
  if (!urlMatches) {
3541
- console.log(chalk9.yellow(` \u26A0 Plan URL differs from argument:`));
3542
- console.log(chalk9.yellow(` Plan: ${existingPlan.url}`));
3543
- console.log(chalk9.yellow(` Arg: ${url}`));
3742
+ console.log(chalk10.yellow(` \u26A0 Plan URL differs from argument:`));
3743
+ console.log(chalk10.yellow(` Plan: ${existingPlan.url}`));
3744
+ console.log(chalk10.yellow(` Arg: ${url}`));
3544
3745
  }
3545
3746
  const existingLang = existingPlan.lang || "en";
3546
3747
  if (existingLang !== targetLang) {
3547
- console.log(chalk9.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
3748
+ console.log(chalk10.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
3548
3749
  }
3549
3750
  const reuse = await confirm(`${INDENT3}Use existing plan?`);
3550
3751
  if (reuse) {
3551
3752
  plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
3552
3753
  if (existingPlanPath !== planPath) {
3553
3754
  await savePlan(plan, planPath);
3554
- console.log(chalk9.dim(` Migrated to: ${formatClickablePath(planPath)}`));
3755
+ console.log(chalk10.dim(` Migrated to: ${formatClickablePath(planPath)}`));
3555
3756
  }
3556
3757
  if (existingLang !== targetLang) {
3557
3758
  const translate = await confirm(`${INDENT3}Translate to ${targetLang}?`);
@@ -3569,7 +3770,8 @@ async function runPlanPhase(params) {
3569
3770
  userEmail,
3570
3771
  extensionPath,
3571
3772
  config.output.dir,
3572
- userTier
3773
+ userTier,
3774
+ { width: config.video.width, height: config.video.height }
3573
3775
  );
3574
3776
  displayRateLimitInfo();
3575
3777
  if (urlMatches) {
@@ -3586,7 +3788,7 @@ async function runPlanPhase(params) {
3586
3788
  uploadIdx++;
3587
3789
  }
3588
3790
  }
3589
- console.log(chalk9.dim(` Preserved upload paths from previous plan`));
3791
+ console.log(chalk10.dim(` Preserved upload paths from previous plan`));
3590
3792
  }
3591
3793
  await savePlan(plan, planPath);
3592
3794
  plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
@@ -3602,7 +3804,9 @@ async function runPlanPhase(params) {
3602
3804
  logError,
3603
3805
  userEmail,
3604
3806
  extensionPath,
3605
- config.output.dir
3807
+ config.output.dir,
3808
+ void 0,
3809
+ { width: config.video.width, height: config.video.height }
3606
3810
  );
3607
3811
  displayRateLimitInfo();
3608
3812
  await savePlan(plan, planPath);
@@ -3610,8 +3814,8 @@ async function runPlanPhase(params) {
3610
3814
  }
3611
3815
  displayPlanSummary(plan);
3612
3816
  console.log();
3613
- console.log(chalk9.dim(` Plan: ${formatClickablePath(planPath)}`));
3614
- console.log(chalk9.dim(` Enter a prompt to refine, or press Enter to continue.`));
3817
+ console.log(chalk10.dim(` Plan: ${formatClickablePath(planPath)}`));
3818
+ console.log(chalk10.dim(` Enter a prompt to refine, or press Enter to continue.`));
3615
3819
  const MAX_REFINEMENTS = 10;
3616
3820
  let refinementCount = 0;
3617
3821
  while (true) {
@@ -3629,15 +3833,15 @@ async function runPlanPhase(params) {
3629
3833
  refineTask.succeed("Plan refined");
3630
3834
  displayRateLimitInfo();
3631
3835
  if (plan.steps.length > maxSteps) {
3632
- console.log(chalk9.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
3836
+ console.log(chalk10.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
3633
3837
  }
3634
3838
  displayPlanSummary(plan);
3635
3839
  console.log();
3636
- console.log(chalk9.dim(` Plan: ${formatClickablePath(planPath)}`));
3840
+ console.log(chalk10.dim(` Plan: ${formatClickablePath(planPath)}`));
3637
3841
  if (refinementCount >= MAX_REFINEMENTS) {
3638
3842
  console.log();
3639
- console.log(chalk9.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
3640
- console.log(chalk9.dim(` Further refinements may incur additional costs.`));
3843
+ console.log(chalk10.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
3844
+ console.log(chalk10.dim(` Further refinements may incur additional costs.`));
3641
3845
  const continueRefining = await confirm(`${INDENT3}Continue refining?`);
3642
3846
  if (!continueRefining) break;
3643
3847
  }
@@ -3649,12 +3853,12 @@ async function runPlanPhase(params) {
3649
3853
  const planLang = plan.lang || "en";
3650
3854
  if (planLang !== targetLang) {
3651
3855
  let useExistingTranslation = false;
3652
- if (existsSync7(planPath) && existingPlanPath !== planPath) {
3856
+ if (existsSync8(planPath) && existingPlanPath !== planPath) {
3653
3857
  try {
3654
3858
  const translatedPlan = JSON.parse(await readFile4(planPath, "utf-8"));
3655
3859
  if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
3656
3860
  console.log();
3657
- console.log(chalk9.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
3861
+ console.log(chalk10.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
3658
3862
  const useTranslated = await confirm(`${INDENT3}Use ${targetLang} translation?`);
3659
3863
  if (useTranslated) {
3660
3864
  plan = translatedPlan;
@@ -3666,7 +3870,7 @@ async function runPlanPhase(params) {
3666
3870
  }
3667
3871
  if (!useExistingTranslation) {
3668
3872
  console.log();
3669
- console.log(chalk9.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
3873
+ console.log(chalk10.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
3670
3874
  const translate = await confirm(`${INDENT3}Translate captions to ${targetLang}?`);
3671
3875
  if (translate) await doTranslate("Translation failed, using original");
3672
3876
  }
@@ -3676,32 +3880,32 @@ async function runPlanPhase(params) {
3676
3880
 
3677
3881
  // src/commands/phases/validation-phase.ts
3678
3882
  import { readFile as readFile7 } from "fs/promises";
3679
- import chalk11 from "chalk";
3883
+ import chalk12 from "chalk";
3680
3884
 
3681
3885
  // src/record.ts
3682
- import { existsSync as existsSync12 } from "fs";
3886
+ import { existsSync as existsSync13 } from "fs";
3683
3887
  import { mkdir as mkdir6 } from "fs/promises";
3684
- import { join as join15 } from "path";
3685
- import chalk10 from "chalk";
3888
+ import { join as join16 } from "path";
3889
+ import chalk11 from "chalk";
3686
3890
 
3687
3891
  // src/browser/actions.ts
3688
- import { existsSync as existsSync9 } from "fs";
3892
+ import { existsSync as existsSync10 } from "fs";
3689
3893
  import { readFile as readFile5 } from "fs/promises";
3690
3894
  import { isAbsolute as isAbsolute2, resolve as resolve5 } from "path";
3691
3895
 
3692
3896
  // src/browser/effects.ts
3693
3897
  init_lib();
3694
- import { existsSync as existsSync8, readFileSync as readFileSync3 } from "fs";
3695
- import { join as join11 } from "path";
3898
+ import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
3899
+ import { join as join12 } from "path";
3696
3900
  function loadHighlightCSS() {
3697
3901
  const candidates = [
3698
- join11(__dirname, "highlight.css"),
3902
+ join12(__dirname, "highlight.css"),
3699
3903
  // dist/ build
3700
- join11(__dirname, "..", "highlight.css")
3904
+ join12(__dirname, "..", "highlight.css")
3701
3905
  // src/browser/ → src/
3702
3906
  ];
3703
3907
  for (const p of candidates) {
3704
- if (existsSync8(p)) return readFileSync3(p, "utf-8");
3908
+ if (existsSync9(p)) return readFileSync4(p, "utf-8");
3705
3909
  }
3706
3910
  return "";
3707
3911
  }
@@ -4025,7 +4229,14 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
4025
4229
  }
4026
4230
  } else {
4027
4231
  const amount = direction === "up" ? -PAGE_SCROLL_PX : PAGE_SCROLL_PX;
4028
- await page.evaluate((y) => window.scrollBy(0, y), amount);
4232
+ const moved = await page.evaluate((y) => {
4233
+ const before = window.scrollY;
4234
+ window.scrollBy(0, y);
4235
+ return window.scrollY - before;
4236
+ }, amount);
4237
+ if (moved === 0) {
4238
+ throw new Error("scroll moved nothing: the page has no scroll headroom at this viewport");
4239
+ }
4029
4240
  }
4030
4241
  break;
4031
4242
  }
@@ -4043,7 +4254,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
4043
4254
  if (!isPlaceholder) {
4044
4255
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
4045
4256
  const filePath = isAbsolute2(normalizedPath) ? normalizedPath : resolve5(baseDir, normalizedPath);
4046
- if (existsSync9(filePath)) {
4257
+ if (existsSync10(filePath)) {
4047
4258
  await fileInput.setInputFiles(filePath);
4048
4259
  await page.waitForTimeout(2e3);
4049
4260
  break;
@@ -4160,7 +4371,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
4160
4371
  break;
4161
4372
  case "image": {
4162
4373
  const imagePath = step.value ? isAbsolute2(step.value) ? step.value : resolve5(baseDir, step.value) : null;
4163
- if (imagePath && existsSync9(imagePath)) {
4374
+ if (imagePath && existsSync10(imagePath)) {
4164
4375
  const imgStyle = step.target === "stretch" ? `width: ${config.video.width}px; height: ${config.video.height}px; object-fit: cover;` : "max-width: 90%; max-height: 90%;";
4165
4376
  if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
4166
4377
  }
@@ -4190,7 +4401,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
4190
4401
  await pendingDownload;
4191
4402
  pendingDownload = null;
4192
4403
  }
4193
- if (lastDownloadPath && existsSync9(lastDownloadPath)) {
4404
+ if (lastDownloadPath && existsSync10(lastDownloadPath)) {
4194
4405
  const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
4195
4406
  if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
4196
4407
  }
@@ -4235,22 +4446,22 @@ init_lib();
4235
4446
 
4236
4447
  // src/browser/dry-run.ts
4237
4448
  init_utils();
4238
- import { join as join13 } from "path";
4449
+ import { join as join14 } from "path";
4239
4450
 
4240
4451
  // src/browser/step-validator.ts
4241
4452
  init_utils();
4242
- import { existsSync as existsSync10, readdirSync as readdirSync2 } from "fs";
4243
- import { basename as basename2, isAbsolute as isAbsolute3, join as join12, resolve as resolve6 } from "path";
4453
+ import { existsSync as existsSync11, readdirSync as readdirSync3 } from "fs";
4454
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join13, resolve as resolve6 } from "path";
4244
4455
  function findMediaFile(filename, baseDir) {
4245
4456
  const name = basename2(filename);
4246
- const searchDirs = [baseDir, join12(baseDir, "res"), join12(baseDir, "assets"), join12(baseDir, "media")];
4457
+ const searchDirs = [baseDir, join13(baseDir, "res"), join13(baseDir, "assets"), join13(baseDir, "media")];
4247
4458
  for (const dir of searchDirs) {
4248
- if (!existsSync10(dir)) continue;
4459
+ if (!existsSync11(dir)) continue;
4249
4460
  try {
4250
- const files = readdirSync2(dir);
4251
- if (files.includes(name)) return join12(dir, name);
4461
+ const files = readdirSync3(dir);
4462
+ if (files.includes(name)) return join13(dir, name);
4252
4463
  const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
4253
- if (match) return join12(dir, match);
4464
+ if (match) return join13(dir, match);
4254
4465
  } catch {
4255
4466
  }
4256
4467
  }
@@ -4323,7 +4534,7 @@ function resolveMediaStep(step, baseDir, label, startTime) {
4323
4534
  return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
4324
4535
  }
4325
4536
  const specifiedPath = isAbsolute3(step.value) ? step.value : resolve6(baseDir, step.value);
4326
- if (existsSync10(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
4537
+ if (existsSync11(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
4327
4538
  const foundPath = findMediaFile(step.value, baseDir);
4328
4539
  if (foundPath) {
4329
4540
  step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -4445,7 +4656,7 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
4445
4656
  if (!isPlaceholder) {
4446
4657
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
4447
4658
  let filePath = isAbsolute3(normalizedPath) ? normalizedPath : resolve6(baseDir, normalizedPath);
4448
- if (!existsSync10(filePath)) {
4659
+ if (!existsSync11(filePath)) {
4449
4660
  const foundPath = findMediaFile(rawPath, baseDir);
4450
4661
  if (foundPath) {
4451
4662
  const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -4544,7 +4755,7 @@ async function getPageElements(page) {
4544
4755
  });
4545
4756
  }
4546
4757
  async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart, onStepEnd) {
4547
- const userDataDir = join13(baseDir, ".browser-data");
4758
+ const userDataDir = join14(baseDir, ".browser-data");
4548
4759
  const { context, browser, chromeSession } = await launchBrowserContext(
4549
4760
  config,
4550
4761
  userDataDir,
@@ -4609,17 +4820,17 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
4609
4820
  }
4610
4821
 
4611
4822
  // src/browser/meta.ts
4612
- import { existsSync as existsSync11 } from "fs";
4823
+ import { existsSync as existsSync12 } from "fs";
4613
4824
  import { readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
4614
- import { join as join14 } from "path";
4825
+ import { join as join15 } from "path";
4615
4826
  var RECORDING_FILE = "recording.mp4";
4616
4827
  var RECORDING_META_FILE = "recording.meta.json";
4617
4828
  function getRecordingPath(outputDir) {
4618
- return join14(outputDir, RECORDING_FILE);
4829
+ return join15(outputDir, RECORDING_FILE);
4619
4830
  }
4620
4831
  async function loadRecordingMeta(outputDir) {
4621
- const metaPath = join14(outputDir, RECORDING_META_FILE);
4622
- if (!existsSync11(metaPath)) return null;
4832
+ const metaPath = join15(outputDir, RECORDING_META_FILE);
4833
+ if (!existsSync12(metaPath)) return null;
4623
4834
  try {
4624
4835
  const content = await readFile6(metaPath, "utf-8");
4625
4836
  return JSON.parse(content);
@@ -4628,7 +4839,7 @@ async function loadRecordingMeta(outputDir) {
4628
4839
  }
4629
4840
  }
4630
4841
  async function saveRecordingMeta(outputDir, meta) {
4631
- const metaPath = join14(outputDir, RECORDING_META_FILE);
4842
+ const metaPath = join15(outputDir, RECORDING_META_FILE);
4632
4843
  await writeFile7(metaPath, JSON.stringify(meta, null, 2));
4633
4844
  }
4634
4845
  function canReuseRecording(plan, config, stepDurations, existingMeta) {
@@ -4698,14 +4909,14 @@ function formatStepLabel(step) {
4698
4909
  return `${caption.slice(0, 47)}...`;
4699
4910
  }
4700
4911
  async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
4701
- const videoDir = join15(workDir, "video");
4912
+ const videoDir = join16(workDir, "video");
4702
4913
  await mkdir6(videoDir, { recursive: true });
4703
4914
  const getStepDuration = (stepNum) => {
4704
4915
  return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
4705
4916
  };
4706
4917
  const browserTask = task2("Starting browser recording");
4707
4918
  const recordingStartTime = Date.now();
4708
- const userDataDir = join15(workDir, "browser-data");
4919
+ const userDataDir = join16(workDir, "browser-data");
4709
4920
  const recordVideo = { dir: videoDir, size: { width: config.video.width, height: config.video.height } };
4710
4921
  const authState = findAuthState(plan.url);
4711
4922
  const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
@@ -4806,9 +5017,9 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4806
5017
  actualStepDurations.push(actualDuration);
4807
5018
  }
4808
5019
  if (failures.length > 0) {
4809
- console.log(chalk10.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
5020
+ console.log(chalk11.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
4810
5021
  for (const f of failures) {
4811
- console.log(chalk10.yellow(` - ${f}`));
5022
+ console.log(chalk11.yellow(` - ${f}`));
4812
5023
  }
4813
5024
  }
4814
5025
  const saveTask = task2("Saving video");
@@ -4817,7 +5028,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4817
5028
  await page.close();
4818
5029
  const videoPath = await video.path();
4819
5030
  await cleanupBrowserSession({ context, browser, chromeSession });
4820
- if (!videoPath || !existsSync12(videoPath)) {
5031
+ if (!videoPath || !existsSync13(videoPath)) {
4821
5032
  throw new Error("Video file was not created");
4822
5033
  }
4823
5034
  saveTask.succeed("Video saved");
@@ -4876,21 +5087,21 @@ async function runValidationPhase(params) {
4876
5087
  const stepNum = result.stepIndex + 1;
4877
5088
  const step = plan.steps[result.stepIndex];
4878
5089
  console.log();
4879
- console.log(chalk11.red(` \u2717 Step ${stepNum}: ${step.action}`));
5090
+ console.log(chalk12.red(` \u2717 Step ${stepNum}: ${step.action}`));
4880
5091
  const caption = step.caption || "";
4881
- if (caption) console.log(chalk11.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
4882
- console.log(chalk11.dim(` Target: ${step.target || "none"}`));
4883
- if (step.value) console.log(chalk11.dim(` Value: ${step.value}`));
4884
- console.log(chalk11.dim(` Error: ${result.error}`));
5092
+ if (caption) console.log(chalk12.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
5093
+ console.log(chalk12.dim(` Target: ${step.target || "none"}`));
5094
+ if (step.value) console.log(chalk12.dim(` Value: ${step.value}`));
5095
+ console.log(chalk12.dim(` Error: ${result.error}`));
4885
5096
  if (result.suggestion) {
4886
- console.log(chalk11.yellow(` Hint: ${result.suggestion}`));
5097
+ console.log(chalk12.yellow(` Hint: ${result.suggestion}`));
4887
5098
  }
4888
5099
  console.log();
4889
5100
  const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
4890
5101
  if (isUploadMissingFile) {
4891
5102
  const filePath = await promptInput('Enter file path to upload (or "skip"): ');
4892
5103
  if (filePath.toLowerCase() === "skip") {
4893
- console.log(chalk11.yellow(` Skipping step ${stepNum}`));
5104
+ console.log(chalk12.yellow(` Skipping step ${stepNum}`));
4894
5105
  continue;
4895
5106
  }
4896
5107
  if (filePath) {
@@ -4899,13 +5110,13 @@ async function runValidationPhase(params) {
4899
5110
  const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
4900
5111
  const newResult2 = revalidation2.results[result.stepIndex];
4901
5112
  if (newResult2?.success) {
4902
- console.log(chalk11.green(` \u2713 Step ${stepNum} now passes`));
5113
+ console.log(chalk12.green(` \u2713 Step ${stepNum} now passes`));
4903
5114
  validationResult = revalidation2;
4904
5115
  continue;
4905
5116
  }
4906
5117
  result.error = newResult2?.error;
4907
5118
  result.suggestion = newResult2?.suggestion;
4908
- console.log(chalk11.red(` Still failing: ${result.error}`));
5119
+ console.log(chalk12.red(` Still failing: ${result.error}`));
4909
5120
  }
4910
5121
  }
4911
5122
  const fixTask = task(`Fixing step ${stepNum} with AI`);
@@ -4927,8 +5138,8 @@ async function runValidationPhase(params) {
4927
5138
  await savePlan(plan, planPath);
4928
5139
  fixTask.succeed("AI suggested a fix");
4929
5140
  const fixedStep = plan.steps[result.stepIndex];
4930
- console.log(chalk11.dim(` New target: ${fixedStep.target || "none"}`));
4931
- if (fixedStep.value) console.log(chalk11.dim(` New value: ${fixedStep.value}`));
5141
+ console.log(chalk12.dim(` New target: ${fixedStep.target || "none"}`));
5142
+ if (fixedStep.value) console.log(chalk12.dim(` New value: ${fixedStep.value}`));
4932
5143
  } catch {
4933
5144
  fixTask.fail("AI could not fix automatically");
4934
5145
  }
@@ -4944,11 +5155,11 @@ async function runValidationPhase(params) {
4944
5155
  result.error = newResult?.error;
4945
5156
  result.suggestion = newResult?.suggestion;
4946
5157
  console.log();
4947
- console.log(chalk11.yellow(` Edit step ${stepNum} in: `) + chalk11.cyan(formatClickablePath(planPath)));
5158
+ console.log(chalk12.yellow(` Edit step ${stepNum} in: `) + chalk12.cyan(formatClickablePath(planPath)));
4948
5159
  console.log();
4949
5160
  const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
4950
5161
  if (choice.toLowerCase() === "skip") {
4951
- console.log(chalk11.yellow(` Skipping step ${stepNum} - may fail during recording`));
5162
+ console.log(chalk12.yellow(` Skipping step ${stepNum} - may fail during recording`));
4952
5163
  continue;
4953
5164
  }
4954
5165
  plan = JSON.parse(await readFile7(planPath, "utf-8"));
@@ -4976,17 +5187,17 @@ async function runValidationPhase(params) {
4976
5187
 
4977
5188
  // src/commands/phases/video-phase.ts
4978
5189
  init_lib();
4979
- import { existsSync as existsSync14 } from "fs";
5190
+ import { existsSync as existsSync15 } from "fs";
4980
5191
  import { copyFile } from "fs/promises";
4981
- import { join as join17, resolve as resolve8 } from "path";
5192
+ import { join as join18, resolve as resolve8 } from "path";
4982
5193
 
4983
5194
  // src/utils/video-pipeline.ts
4984
5195
  init_lib();
4985
5196
  import { chunkWordsByCount, assTimeFromMs as formatASSTime } from "@spark-apps/video-kit";
4986
- import { existsSync as existsSync13 } from "fs";
5197
+ import { existsSync as existsSync14 } from "fs";
4987
5198
  import { mkdir as mkdir7, writeFile as writeFile8 } from "fs/promises";
4988
- import { isAbsolute as isAbsolute4, join as join16, resolve as resolve7 } from "path";
4989
- import chalk12 from "chalk";
5199
+ import { isAbsolute as isAbsolute4, join as join17, resolve as resolve7 } from "path";
5200
+ import chalk13 from "chalk";
4990
5201
  var INDENT4 = " ";
4991
5202
  async function generateASS(plan, stepAudio, workDir, config, captions) {
4992
5203
  const regularColor = hexToAss(captions.regular.color);
@@ -5033,7 +5244,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
5033
5244
  }
5034
5245
  currentTime += audio.duration;
5035
5246
  }
5036
- const assPath = join16(workDir, "subtitles.ass");
5247
+ const assPath = join17(workDir, "subtitles.ass");
5037
5248
  await writeFile8(assPath, ass);
5038
5249
  return assPath;
5039
5250
  }
@@ -5048,7 +5259,7 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
5048
5259
  const ext = step.value.split(".").pop()?.toLowerCase() || "";
5049
5260
  if (["mp4", "webm", "mov"].includes(ext)) {
5050
5261
  const mediaPath = isAbsolute4(step.value) ? step.value : resolve7(baseDir, step.value);
5051
- if (existsSync13(mediaPath)) {
5262
+ if (existsSync14(mediaPath)) {
5052
5263
  const partial = parsePartial(step.partial);
5053
5264
  overlays.push({
5054
5265
  path: mediaPath,
@@ -5067,11 +5278,11 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
5067
5278
  }
5068
5279
  async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
5069
5280
  header("Compose");
5070
- const configPath = join16(process.cwd(), CONFIG_FILE);
5071
- console.log(`${INDENT4}${chalk12.dim(`Config: ${formatClickablePath(configPath)}`)}`);
5281
+ const configPath = join17(process.cwd(), CONFIG_FILE);
5282
+ console.log(`${INDENT4}${chalk13.dim(`Config: ${formatClickablePath(configPath)}`)}`);
5072
5283
  const proceed = await confirm(`${INDENT4}Review config or continue?`);
5073
5284
  if (!proceed) {
5074
- console.log(chalk12.yellow(`
5285
+ console.log(chalk13.yellow(`
5075
5286
  Edit ${CONFIG_FILE} and run again.
5076
5287
  `));
5077
5288
  process.exit(0);
@@ -5079,11 +5290,11 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
5079
5290
  const composeTask = task("Building final video");
5080
5291
  await mkdir7(config.output.dir, { recursive: true });
5081
5292
  const suffix = config.lang ? `-${config.lang}` : "";
5082
- const finalPath = join16(config.output.dir, `demo${suffix}.${config.video.format}`);
5293
+ const finalPath = join17(config.output.dir, `demo${suffix}.${config.video.format}`);
5083
5294
  const videoDuration = await getMediaDuration(videoPath) / 1e3;
5084
- const hasVoiceover = audioPath && existsSync13(audioPath);
5295
+ const hasVoiceover = audioPath && existsSync14(audioPath);
5085
5296
  const musicPath = config.music?.path ? isAbsolute4(config.music.path) ? config.music.path : resolve7(config.output.dir, config.music.path) : "";
5086
- const hasMusic = musicPath && existsSync13(musicPath);
5297
+ const hasMusic = musicPath && existsSync14(musicPath);
5087
5298
  try {
5088
5299
  await composeVideo({
5089
5300
  videoPath,
@@ -5122,7 +5333,7 @@ async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
5122
5333
  if (duration <= limitSecs * 1e3) return finalPath;
5123
5334
  const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
5124
5335
  console.log();
5125
- console.log(chalk12.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
5336
+ console.log(chalk13.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
5126
5337
  const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
5127
5338
  if (!speedUp) return finalPath;
5128
5339
  const speedTask = task(`Speeding up ${speedFactor}x`);
@@ -5163,7 +5374,7 @@ async function runVideoPhase(params) {
5163
5374
  mediaDuration = (trimEnd - trimStart2) * 1e3;
5164
5375
  } else {
5165
5376
  const mediaPath = resolve8(s.value);
5166
- if (existsSync14(mediaPath)) {
5377
+ if (existsSync15(mediaPath)) {
5167
5378
  try {
5168
5379
  const fullDuration = await getMediaDuration(mediaPath);
5169
5380
  const start = (trimStart2 ?? 0) * 1e3;
@@ -5175,7 +5386,7 @@ async function runVideoPhase(params) {
5175
5386
  }
5176
5387
  } else {
5177
5388
  const mediaPath = resolve8(s.value);
5178
- if (existsSync14(mediaPath)) {
5389
+ if (existsSync15(mediaPath)) {
5179
5390
  try {
5180
5391
  mediaDuration = await getMediaDuration(mediaPath);
5181
5392
  } catch {
@@ -5188,7 +5399,7 @@ async function runVideoPhase(params) {
5188
5399
  })
5189
5400
  );
5190
5401
  const recordingPath = getRecordingPath(config.output.dir);
5191
- const hasExistingRecording = existingRecordingMeta && existsSync14(recordingPath);
5402
+ const hasExistingRecording = existingRecordingMeta && existsSync15(recordingPath);
5192
5403
  let videoPath = recordingPath;
5193
5404
  const firstStep = plan.steps[0];
5194
5405
  const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
@@ -5228,7 +5439,7 @@ async function runVideoPhase(params) {
5228
5439
  if (videoOverlays.length > 0) {
5229
5440
  const spliceTask = task("Splicing videos into recording");
5230
5441
  try {
5231
- const splicedPath = join17(workDir, "spliced.mp4");
5442
+ const splicedPath = join18(workDir, "spliced.mp4");
5232
5443
  await spliceVideos({
5233
5444
  inputPath: videoPath,
5234
5445
  outputPath: splicedPath,
@@ -5248,7 +5459,7 @@ async function runVideoPhase(params) {
5248
5459
  if (stepTransitions.length > 0) {
5249
5460
  const transitionTask = task("Applying transitions");
5250
5461
  try {
5251
- const transitionedPath = join17(workDir, "transitioned.mp4");
5462
+ const transitionedPath = join18(workDir, "transitioned.mp4");
5252
5463
  const videoDuration = alignedStepDurations.reduce((a, b) => a + b, 0) / 1e3;
5253
5464
  await applyTransitions({
5254
5465
  inputVideo: videoPath,
@@ -5269,15 +5480,15 @@ async function runVideoPhase(params) {
5269
5480
  let mainContentDuration;
5270
5481
  if (userTier === "free") {
5271
5482
  const scriptDir = getScriptDir();
5272
- let outroPath = join17(scriptDir, "assets", "qp.wm");
5273
- if (!existsSync14(outroPath)) {
5274
- outroPath = join17(scriptDir, "..", "dist", "assets", "qp.wm");
5483
+ let outroPath = join18(scriptDir, "assets", "qp.wm");
5484
+ if (!existsSync15(outroPath)) {
5485
+ outroPath = join18(scriptDir, "..", "dist", "assets", "qp.wm");
5275
5486
  }
5276
- if (existsSync14(outroPath)) {
5487
+ if (existsSync15(outroPath)) {
5277
5488
  try {
5278
5489
  const mainDuration = await getMediaDuration(videoPath);
5279
5490
  mainContentDuration = mainDuration / 1e3 - trimStart;
5280
- const combinedPath = join17(workDir, "combined-with-outro.mp4");
5491
+ const combinedPath = join18(workDir, "combined-with-outro.mp4");
5281
5492
  await concatVideoWithOutro({
5282
5493
  inputPath: videoPath,
5283
5494
  outroPath,
@@ -5325,16 +5536,16 @@ async function runVideoPhase(params) {
5325
5536
 
5326
5537
  // src/commands/phases/voiceover-phase.ts
5327
5538
  init_lib();
5328
- import { existsSync as existsSync15 } from "fs";
5329
- import { join as join18 } from "path";
5330
- import chalk13 from "chalk";
5539
+ import { existsSync as existsSync16 } from "fs";
5540
+ import { join as join19 } from "path";
5541
+ import chalk14 from "chalk";
5331
5542
  var INDENT6 = " ";
5332
5543
  async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5333
5544
  if (fresh) {
5334
5545
  const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
5335
5546
  for (const file of filesToDelete) {
5336
- const filePath = join18(config.output.dir, file);
5337
- if (existsSync15(filePath)) {
5547
+ const filePath = join19(config.output.dir, file);
5548
+ if (existsSync16(filePath)) {
5338
5549
  const { unlink: unlink3 } = await import("fs/promises");
5339
5550
  await unlink3(filePath).catch(() => {
5340
5551
  });
@@ -5344,8 +5555,8 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5344
5555
  }
5345
5556
  const voice = resolveVoice(config);
5346
5557
  const existingMeta = await loadVoiceoverMeta(config.output.dir);
5347
- const voiceoverFile = join18(config.output.dir, "voiceover.wav");
5348
- const hasExistingVoiceover = existingMeta && existsSync15(voiceoverFile);
5558
+ const voiceoverFile = join19(config.output.dir, "voiceover.wav");
5559
+ const hasExistingVoiceover = existingMeta && existsSync16(voiceoverFile);
5349
5560
  const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
5350
5561
  let voiceoverPath;
5351
5562
  let stepAudio;
@@ -5368,9 +5579,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5368
5579
  const reuseCheck = canReuseVoiceover(plan.steps, voice, existingMeta);
5369
5580
  if (reuseCheck.canReuse) {
5370
5581
  console.log();
5371
- console.log(chalk13.dim(` Found existing voiceover in ${config.output.dir}`));
5582
+ console.log(chalk14.dim(` Found existing voiceover in ${config.output.dir}`));
5372
5583
  if (reuseCheck.reason) {
5373
- console.log(chalk13.dim(` ${reuseCheck.reason}`));
5584
+ console.log(chalk14.dim(` ${reuseCheck.reason}`));
5374
5585
  }
5375
5586
  const reuse = await confirm(`${INDENT6}Reuse existing voiceover?`);
5376
5587
  if (reuse) {
@@ -5402,31 +5613,31 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5402
5613
  }
5403
5614
 
5404
5615
  // src/commands/utils/chrome-extension.ts
5405
- import { existsSync as existsSync16, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "fs";
5616
+ import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "fs";
5406
5617
  import { homedir as homedir3 } from "os";
5407
- import { join as join19 } from "path";
5618
+ import { join as join20 } from "path";
5408
5619
  function resolveChromeExtension(extensionId) {
5409
5620
  const platform = process.platform;
5410
5621
  const possibleDirs = [];
5411
5622
  const getChromeExtDirs = (userDataPath) => {
5412
5623
  const dirs = [];
5413
5624
  try {
5414
- const profiles = readdirSync3(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
5625
+ const profiles = readdirSync4(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
5415
5626
  for (const profile of profiles) {
5416
- dirs.push(join19(userDataPath, profile, "Extensions"));
5627
+ dirs.push(join20(userDataPath, profile, "Extensions"));
5417
5628
  }
5418
5629
  } catch {
5419
- dirs.push(join19(userDataPath, "Default", "Extensions"));
5630
+ dirs.push(join20(userDataPath, "Default", "Extensions"));
5420
5631
  }
5421
5632
  return dirs;
5422
5633
  };
5423
5634
  if (platform === "win32") {
5424
- possibleDirs.push(...getChromeExtDirs(join19(homedir3(), "AppData", "Local", "Google", "Chrome", "User Data")));
5635
+ possibleDirs.push(...getChromeExtDirs(join20(homedir3(), "AppData", "Local", "Google", "Chrome", "User Data")));
5425
5636
  } else if (platform === "darwin") {
5426
- possibleDirs.push(...getChromeExtDirs(join19(homedir3(), "Library", "Application Support", "Google", "Chrome")));
5637
+ possibleDirs.push(...getChromeExtDirs(join20(homedir3(), "Library", "Application Support", "Google", "Chrome")));
5427
5638
  } else {
5428
5639
  try {
5429
- const windowsUsers = readdirSync3("/mnt/c/Users").filter(
5640
+ const windowsUsers = readdirSync4("/mnt/c/Users").filter(
5430
5641
  (u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
5431
5642
  );
5432
5643
  for (const user of windowsUsers) {
@@ -5434,22 +5645,22 @@ function resolveChromeExtension(extensionId) {
5434
5645
  }
5435
5646
  } catch {
5436
5647
  }
5437
- possibleDirs.push(...getChromeExtDirs(join19(homedir3(), ".config", "google-chrome")));
5648
+ possibleDirs.push(...getChromeExtDirs(join20(homedir3(), ".config", "google-chrome")));
5438
5649
  }
5439
5650
  for (const chromeExtDir of possibleDirs) {
5440
- const extDir = join19(chromeExtDir, extensionId);
5441
- if (!existsSync16(extDir)) continue;
5651
+ const extDir = join20(chromeExtDir, extensionId);
5652
+ if (!existsSync17(extDir)) continue;
5442
5653
  try {
5443
- const versions = readdirSync3(extDir).filter((f) => !f.startsWith("."));
5654
+ const versions = readdirSync4(extDir).filter((f) => !f.startsWith("."));
5444
5655
  if (versions.length === 0) continue;
5445
5656
  const latestVersion = versions.sort().pop();
5446
- const extPath = join19(extDir, latestVersion);
5657
+ const extPath = join20(extDir, latestVersion);
5447
5658
  let name = extensionId;
5448
5659
  let popupPath = "popup.html";
5449
- const manifestPath = join19(extPath, "manifest.json");
5450
- if (existsSync16(manifestPath)) {
5660
+ const manifestPath = join20(extPath, "manifest.json");
5661
+ if (existsSync17(manifestPath)) {
5451
5662
  try {
5452
- const manifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
5663
+ const manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
5453
5664
  if (manifest.name && !manifest.name.startsWith("__MSG_")) {
5454
5665
  name = manifest.name;
5455
5666
  }
@@ -5480,16 +5691,16 @@ async function recordCommand(url, options = {}) {
5480
5691
  const extensionId = url.slice(7);
5481
5692
  chromeExtension = resolveChromeExtension(extensionId);
5482
5693
  if (!chromeExtension) {
5483
- console.log(chalk14.red(`
5694
+ console.log(chalk15.red(`
5484
5695
  \u2717 Chrome extension not found: ${extensionId}`));
5485
- console.log(chalk14.dim(` Make sure the extension is installed in Chrome
5696
+ console.log(chalk15.dim(` Make sure the extension is installed in Chrome
5486
5697
  `));
5487
5698
  process.exit(1);
5488
5699
  }
5489
5700
  config.browser = { extension: chromeExtension.path };
5490
5701
  url = chromeExtension.popupUrl;
5491
- console.log(chalk14.dim(` Extension: ${chromeExtension.name}`));
5492
- console.log(chalk14.dim(` Path: ${chromeExtension.path}`));
5702
+ console.log(chalk15.dim(` Extension: ${chromeExtension.name}`));
5703
+ console.log(chalk15.dim(` Path: ${chromeExtension.path}`));
5493
5704
  }
5494
5705
  config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
5495
5706
  const userData = await loadUserData();
@@ -5502,20 +5713,20 @@ async function recordCommand(url, options = {}) {
5502
5713
  if (!authResult) return;
5503
5714
  const { userEmail, userTier, localVideoCount } = authResult;
5504
5715
  if (configCreated) {
5505
- const configPath = join20(process.cwd(), CONFIG_FILE);
5716
+ const configPath = join21(process.cwd(), CONFIG_FILE);
5506
5717
  console.log();
5507
- console.log(chalk14.green(` \u2714 Created ${CONFIG_FILE}`));
5508
- console.log(chalk14.dim(` ${configPath}`));
5718
+ console.log(chalk15.green(` \u2714 Created ${CONFIG_FILE}`));
5719
+ console.log(chalk15.dim(` ${configPath}`));
5509
5720
  console.log();
5510
- console.log(chalk14.cyan(" Review the config file to customize:"));
5511
- console.log(chalk14.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
5512
- console.log(chalk14.dim(" \u2022 video: Resolution, quality, effects"));
5513
- console.log(chalk14.dim(" \u2022 music: Background music path and volume"));
5514
- console.log(chalk14.dim(" \u2022 output: Output directory"));
5721
+ console.log(chalk15.cyan(" Review the config file to customize:"));
5722
+ console.log(chalk15.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
5723
+ console.log(chalk15.dim(" \u2022 video: Resolution, quality, effects"));
5724
+ console.log(chalk15.dim(" \u2022 music: Background music path and volume"));
5725
+ console.log(chalk15.dim(" \u2022 output: Output directory"));
5515
5726
  console.log();
5516
5727
  const proceed = await confirm(`${INDENT2}Proceed with default settings?`);
5517
5728
  if (!proceed) {
5518
- console.log(chalk14.yellow(`
5729
+ console.log(chalk15.yellow(`
5519
5730
  Edit ${CONFIG_FILE} and run again.
5520
5731
  `));
5521
5732
  process.exit(0);
@@ -5524,16 +5735,16 @@ async function recordCommand(url, options = {}) {
5524
5735
  if (langOverride) config.lang = langOverride;
5525
5736
  if (config.music?.path) {
5526
5737
  const musicPath = isAbsolute5(config.music.path) ? config.music.path : resolve9(config.output.dir, config.music.path);
5527
- if (!existsSync17(musicPath)) {
5738
+ if (!existsSync18(musicPath)) {
5528
5739
  console.log();
5529
- console.log(chalk14.yellow(` \u26A0 Music file not found: ${config.music.path}`));
5530
- console.log(chalk14.dim(` Config: ${join20(process.cwd(), CONFIG_FILE)}`));
5740
+ console.log(chalk15.yellow(` \u26A0 Music file not found: ${config.music.path}`));
5741
+ console.log(chalk15.dim(` Config: ${join21(process.cwd(), CONFIG_FILE)}`));
5531
5742
  console.log();
5532
5743
  const proceed = await confirm(`${INDENT2}Continue without background music?`);
5533
5744
  if (!proceed) {
5534
- console.log(chalk14.yellow(`
5745
+ console.log(chalk15.yellow(`
5535
5746
  Edit music.path in config and run again.`));
5536
- console.log(chalk14.dim(` ${join20(process.cwd(), CONFIG_FILE)}
5747
+ console.log(chalk15.dim(` ${join21(process.cwd(), CONFIG_FILE)}
5537
5748
  `));
5538
5749
  process.exit(0);
5539
5750
  }
@@ -5543,8 +5754,8 @@ async function recordCommand(url, options = {}) {
5543
5754
  const captions = await loadCaptions();
5544
5755
  const workDir = await prepareWorkDir();
5545
5756
  const targetLang = config.lang || "en";
5546
- const planPath = join20(config.output.dir, `plan-${targetLang}.json`);
5547
- const legacyPlanPath = join20(config.output.dir, "plan.json");
5757
+ const planPath = join21(config.output.dir, `plan-${targetLang}.json`);
5758
+ const legacyPlanPath = join21(config.output.dir, "plan.json");
5548
5759
  const isExtensionUrl = url.startsWith("chrome-extension://");
5549
5760
  await mkdir8(config.output.dir, { recursive: true });
5550
5761
  await setLogFilePath(config.output.dir);
@@ -5557,7 +5768,8 @@ async function recordCommand(url, options = {}) {
5557
5768
  ...description !== void 0 && { description },
5558
5769
  planPath,
5559
5770
  legacyPlanPath,
5560
- targetLang
5771
+ targetLang,
5772
+ ...fresh !== void 0 && { fresh }
5561
5773
  });
5562
5774
  const validatedPlan = await runValidationPhase({
5563
5775
  plan,
@@ -5582,29 +5794,29 @@ async function recordCommand(url, options = {}) {
5582
5794
  await saveUserData({ localVideoCount: newCount });
5583
5795
  const remaining = FREE_VIDEO_LIMIT - newCount;
5584
5796
  if (remaining > 0) {
5585
- console.log(chalk14.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
5797
+ console.log(chalk15.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
5586
5798
  }
5587
5799
  }
5588
5800
  console.log();
5589
- console.log(chalk14.bold.green(" \u2713 Done!"));
5801
+ console.log(chalk15.bold.green(" \u2713 Done!"));
5590
5802
  console.log();
5591
- console.log(chalk14.cyan(` ${formatClickablePath(finalPath)}`));
5803
+ console.log(chalk15.cyan(` ${formatClickablePath(finalPath)}`));
5592
5804
  console.log();
5593
5805
  return finalPath;
5594
5806
  } catch (err) {
5595
5807
  await logError("Record Command", err);
5596
5808
  if (rethrow) throw err;
5597
5809
  console.log();
5598
- console.log(chalk14.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
5599
- console.log(chalk14.dim(` See ${logFilePath} for details`));
5810
+ console.log(chalk15.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
5811
+ console.log(chalk15.dim(` See ${logFilePath} for details`));
5600
5812
  process.exit(1);
5601
5813
  }
5602
5814
  }
5603
5815
 
5604
5816
  // src/commands/script-cmd.ts
5605
5817
  import { readFile as readFile8 } from "fs/promises";
5606
- import { join as join21 } from "path";
5607
- import chalk15 from "chalk";
5818
+ import { join as join22 } from "path";
5819
+ import chalk16 from "chalk";
5608
5820
 
5609
5821
  // src/ai/narrate.ts
5610
5822
  init_lib();
@@ -5659,7 +5871,7 @@ async function scriptCommand(url, scriptPath, lang) {
5659
5871
  const raw = await readFile8(scriptPath, "utf-8");
5660
5872
  const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
5661
5873
  if (lines.length === 0) {
5662
- console.log(chalk15.red(" Script file is empty or has no non-comment lines"));
5874
+ console.log(chalk16.red(" Script file is empty or has no non-comment lines"));
5663
5875
  process.exit(1);
5664
5876
  }
5665
5877
  info(`${lines.length} captions from script`);
@@ -5671,7 +5883,7 @@ async function scriptCommand(url, scriptPath, lang) {
5671
5883
  const planTask = task("Mapping script to UI actions");
5672
5884
  const appTitle = new URL(url).hostname.replace(/^www\./, "");
5673
5885
  const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
5674
- const planPath = join21(config.output.dir, `plan-${lang ?? "en"}.json`);
5886
+ const planPath = join22(config.output.dir, `plan-${lang ?? "en"}.json`);
5675
5887
  await savePlan(plan, planPath);
5676
5888
  planTask.succeed(`${plan.steps.length} steps planned`);
5677
5889
  header("Voiceover");
@@ -5691,12 +5903,23 @@ async function scriptCommand(url, scriptPath, lang) {
5691
5903
  const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
5692
5904
  const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
5693
5905
  console.log();
5694
- console.log(chalk15.bold.green(" Done!"));
5695
- console.log(chalk15.cyan(` ${finalPath}`));
5906
+ console.log(chalk16.bold.green(" Done!"));
5907
+ console.log(chalk16.cyan(` ${finalPath}`));
5696
5908
  }
5697
5909
 
5698
5910
  // src/mcp/tools.ts
5699
5911
  init_SparkStripe_feature_gating();
5912
+
5913
+ // src/mcp/elicit.ts
5914
+ var elicit = null;
5915
+ function setElicit(fn) {
5916
+ elicit = fn;
5917
+ }
5918
+ async function askUser(message) {
5919
+ return elicit ? elicit(message) : null;
5920
+ }
5921
+
5922
+ // src/mcp/tools.ts
5700
5923
  var URL_PROPERTY = {
5701
5924
  url: {
5702
5925
  type: "string",
@@ -5712,7 +5935,7 @@ var LANG_PROPERTY = {
5712
5935
  var QUICKPEEK_TOOLS = [
5713
5936
  {
5714
5937
  name: "generate_demo",
5715
- description: "Record a narrated demo video of a web app, end to end: an AI plans the walkthrough, Playwright drives a real browser, and the result is composed with voiceover and burned karaoke captions. This is the headline tool. It takes minutes, not seconds, because a real browser session is recorded. Returns the finished MP4 path and a file:// url. Pages behind a login can be demoed after the user runs `qp auth <url>` once in a terminal; the saved session is picked up automatically.",
5938
+ description: "Record a narrated demo video of a web app, end to end: an AI plans the walkthrough, Playwright drives a real browser, and the result is composed with voiceover and burned karaoke captions. This is the headline tool. It takes minutes, not seconds, because a real browser session is recorded. Returns the finished MP4 path and a file:// url. Pages behind a login can be demoed once a session is saved for the host, by start_auth or by the user running `qp auth <url>`; it is picked up automatically. The result says whether this recording was signed in.",
5716
5939
  inputSchema: {
5717
5940
  type: "object",
5718
5941
  properties: {
@@ -5750,6 +5973,29 @@ var QUICKPEEK_TOOLS = [
5750
5973
  required: ["url", "script_path"]
5751
5974
  }
5752
5975
  },
5976
+ {
5977
+ name: "open_in_editor",
5978
+ description: "Open a recorded demo's plan in the browser editor, where the user can reword captions, reorder steps or drop them. The plan travels in the URL fragment, so nothing is uploaded; saving in the editor writes back over the same file and the next recording picks it up. Records nothing itself.",
5979
+ inputSchema: {
5980
+ type: "object",
5981
+ properties: { ...URL_PROPERTY, ...LANG_PROPERTY },
5982
+ required: ["url"]
5983
+ }
5984
+ },
5985
+ {
5986
+ name: "start_auth",
5987
+ description: "Open a visible browser on a site so the user can sign in, once, by hand. Everything recorded afterwards starts from that session, so a demo can cover pages behind the login. QuickPeek never sees the password: only the resulting cookies are saved. On clients that support prompting, this saves the session as soon as the user confirms; otherwise call finish_auth when they say they are done.",
5988
+ inputSchema: {
5989
+ type: "object",
5990
+ properties: { ...URL_PROPERTY },
5991
+ required: ["url"]
5992
+ }
5993
+ },
5994
+ {
5995
+ name: "finish_auth",
5996
+ description: "Save the session from the browser window start_auth opened, and close it. Call this once the user says they have signed in.",
5997
+ inputSchema: { type: "object", properties: {} }
5998
+ },
5753
5999
  {
5754
6000
  name: "list_demos",
5755
6001
  description: "List demo videos already produced for a URL, newest first, with size and modified time. Cheap: reads the output directory, records nothing.",
@@ -5803,7 +6049,7 @@ function authStatus(url) {
5803
6049
  return {
5804
6050
  signedIn: false,
5805
6051
  host,
5806
- note: `No saved session for ${host}, so only pages reachable while logged out were recorded. To cover pages behind the login, ask the user to run \`qp auth ${url}\` in a terminal and sign in once, then re-run with fresh: true.`
6052
+ note: `No saved session for ${host}, so only pages reachable while logged out were recorded. To cover pages behind the login, call start_auth with this url (or have the user run \`qp auth ${url}\`), then re-run with fresh: true.`
5807
6053
  };
5808
6054
  }
5809
6055
  async function handleGenerateDemoFromScript(args) {
@@ -5817,9 +6063,56 @@ async function handleGenerateDemoFromScript(args) {
5817
6063
  return fileResult(newest, { name: newest.split("/").pop(), url_recorded: url, scriptPath });
5818
6064
  });
5819
6065
  }
6066
+ async function handleOpenInEditor(args) {
6067
+ const url = resolveUrl(args.url);
6068
+ return withSilencedStdout(async () => {
6069
+ const dir = getOutputDirFromUrl(url);
6070
+ const planPath = findPlanFile(dir, args.lang);
6071
+ if (!planPath) {
6072
+ return errorContent(`No plan found in ${dir}. Record the demo first with generate_demo.`);
6073
+ }
6074
+ const parsed = parsePlanText(readFileSync6(planPath, "utf8"));
6075
+ if (!parsed.ok) return errorContent(`${planPath} cannot be opened: ${parsed.error}`);
6076
+ const base = editorBaseUrl();
6077
+ const editorUrl = planEditorUrl(base, parsed.plan);
6078
+ await openInBrowser(editorUrl ?? `${base}/edit`);
6079
+ return jsonContent({
6080
+ opened: true,
6081
+ plan_path: planPath,
6082
+ steps: parsed.plan.steps.length,
6083
+ url: editorUrl ?? `${base}/edit`,
6084
+ next_steps: editorUrl ? ["The editor opens with the plan loaded.", "Saving there writes back over plan_path; then re-record."] : [`This plan is too long for a link. Load ${planPath} in the editor by hand.`]
6085
+ });
6086
+ });
6087
+ }
6088
+ async function handleStartAuth(args) {
6089
+ const url = resolveUrl(args.url);
6090
+ return withSilencedStdout(async () => {
6091
+ await openSignIn(url);
6092
+ const confirmed = await askUser(`A browser window is open on ${url}. Sign in there, then confirm.`);
6093
+ if (confirmed === null) {
6094
+ return jsonContent({
6095
+ status: "awaiting_sign_in",
6096
+ url,
6097
+ next: "A browser window is open. Call finish_auth once the user says they are signed in."
6098
+ });
6099
+ }
6100
+ if (!confirmed) {
6101
+ await cancelSignIn();
6102
+ return jsonContent({ status: "cancelled", url, note: "Nothing was saved." });
6103
+ }
6104
+ return jsonContent({ status: "saved", ...await saveSignIn() });
6105
+ });
6106
+ }
6107
+ async function handleFinishAuth() {
6108
+ return withSilencedStdout(async () => {
6109
+ if (!pendingSignIn()) return errorContent("No sign-in window is open. Call start_auth first.");
6110
+ return jsonContent({ status: "saved", ...await saveSignIn() });
6111
+ });
6112
+ }
5820
6113
  function newestVideo(dir) {
5821
6114
  try {
5822
- const videos = readdirSync4(dir).filter((f) => f.toLowerCase().endsWith(".mp4")).map((f) => join22(dir, f)).map((p) => ({ path: p, mtime: statSync2(p).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
6115
+ const videos = readdirSync5(dir).filter((f) => f.toLowerCase().endsWith(".mp4")).map((f) => join23(dir, f)).map((p) => ({ path: p, mtime: statSync2(p).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
5823
6116
  return videos[0]?.path ?? null;
5824
6117
  } catch {
5825
6118
  return null;
@@ -5831,8 +6124,8 @@ async function handleListDemos(args) {
5831
6124
  return withSilencedStdout(async () => {
5832
6125
  let entries = [];
5833
6126
  try {
5834
- entries = readdirSync4(dir).filter((f) => f.toLowerCase().endsWith(".mp4")).map((f) => {
5835
- const s = statSync2(join22(dir, f));
6127
+ entries = readdirSync5(dir).filter((f) => f.toLowerCase().endsWith(".mp4")).map((f) => {
6128
+ const s = statSync2(join23(dir, f));
5836
6129
  return { name: f, bytes: s.size, modified: new Date(s.mtimeMs).toISOString() };
5837
6130
  }).sort((a, b) => b.modified.localeCompare(a.modified));
5838
6131
  } catch {
@@ -5878,6 +6171,9 @@ async function handleCheckAccount() {
5878
6171
  var QUICKPEEK_HANDLERS = {
5879
6172
  generate_demo: handleGenerateDemo,
5880
6173
  generate_demo_from_script: handleGenerateDemoFromScript,
6174
+ open_in_editor: handleOpenInEditor,
6175
+ start_auth: handleStartAuth,
6176
+ finish_auth: handleFinishAuth,
5881
6177
  list_demos: handleListDemos,
5882
6178
  get_demo_config: handleGetDemoConfig,
5883
6179
  list_voices: handleListVoices,
@@ -5905,5 +6201,6 @@ export {
5905
6201
  TOOLS,
5906
6202
  TOOL_HANDLERS,
5907
6203
  errorContent,
5908
- jsonContent
6204
+ jsonContent,
6205
+ setElicit
5909
6206
  };