@spark-apps/quickpeek 1.1.3 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/qp.js CHANGED
@@ -35,14 +35,6 @@ function getLanguageName(lang) {
35
35
  if (!lang || lang === "en") return null;
36
36
  return LANGUAGE_NAMES[lang] || lang.toUpperCase();
37
37
  }
38
- function hexToASS(hex, alpha = 0) {
39
- const clean = hex.replace("#", "");
40
- const r = clean.slice(0, 2);
41
- const g = clean.slice(2, 4);
42
- const b = clean.slice(4, 6);
43
- const a = alpha.toString(16).padStart(2, "0").toUpperCase();
44
- return `&H${a}${b}${g}${r}`.toUpperCase();
45
- }
46
38
  function rgbaToHex(color) {
47
39
  if (color.startsWith("#")) return color;
48
40
  const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
@@ -52,12 +44,6 @@ function rgbaToHex(color) {
52
44
  const b = parseInt(match[3], 10).toString(16).padStart(2, "0");
53
45
  return `#${r}${g}${b}`.toUpperCase();
54
46
  }
55
- function windowsPathToWSL(path) {
56
- const driveMatch = /^([A-Z]):[/\\]/i.exec(path);
57
- if (!driveMatch) return null;
58
- const drive = driveMatch[1].toLowerCase();
59
- return `/mnt/${drive}/${path.slice(3).replace(/\\/g, "/")}`;
60
- }
61
47
  function parsePartial(partial) {
62
48
  if (!partial) return {};
63
49
  const parts = partial.split("-");
@@ -73,7 +59,7 @@ function getErrorMessage(err) {
73
59
  return err instanceof Error ? err.message : String(err);
74
60
  }
75
61
  function toTitleCase(text) {
76
- return text.replace(/\w+/g, (word, index) => {
62
+ return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
77
63
  if (index === 0) {
78
64
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
79
65
  }
@@ -83,10 +69,12 @@ function toTitleCase(text) {
83
69
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
84
70
  });
85
71
  }
86
- var LANGUAGE_NAMES, LOWERCASE_WORDS;
72
+ var import_video_kit, import_video_kit2, LANGUAGE_NAMES, LOWERCASE_WORDS;
87
73
  var init_utils = __esm({
88
74
  "src/lib/utils.ts"() {
89
75
  "use strict";
76
+ import_video_kit = require("@spark-apps/video-kit");
77
+ import_video_kit2 = require("@spark-apps/video-kit");
90
78
  LANGUAGE_NAMES = {
91
79
  ar: "Arabic",
92
80
  es: "Spanish",
@@ -137,21 +125,27 @@ var init_utils = __esm({
137
125
  });
138
126
 
139
127
  // src/lib/api-client.ts
140
- async function getOrRefreshToken() {
128
+ function apiKey() {
129
+ return process.env.SPARK_AI_KEY || "";
130
+ }
131
+ async function getOrRefreshDirectToken() {
141
132
  const now = Date.now();
142
- if (cachedToken && cachedToken.expiresAt > now + 3e5) {
143
- return cachedToken.token;
133
+ if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
134
+ return cachedDirectToken.token;
144
135
  }
145
136
  try {
146
137
  const response = await fetch(SPARK_AI_TOKEN, {
147
138
  method: "POST",
148
- headers: { "X-Api-Key": API_KEY }
139
+ headers: { "X-Api-Key": apiKey() }
149
140
  });
141
+ if (!response.ok) {
142
+ throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
143
+ }
150
144
  const data = await response.json();
151
- if (!response.ok || !data.success || !data.token) {
152
- throw new Error(data.error || `Token exchange failed: ${response.status}`);
145
+ if (!data.success || !data.token) {
146
+ throw new Error(data.error || `Token exchange failed: unexpected response`);
153
147
  }
154
- cachedToken = {
148
+ cachedDirectToken = {
155
149
  token: data.token,
156
150
  expiresAt: data.expiresAt || now + 36e5
157
151
  };
@@ -160,77 +154,130 @@ async function getOrRefreshToken() {
160
154
  throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
161
155
  }
162
156
  }
163
- async function callAI(systemPrompt, userPrompt, tier = "free") {
164
- if (!API_KEY) {
165
- return {
166
- success: false,
167
- error: "AI service is not configured. Please report this issue.",
168
- isAuthError: true
169
- };
157
+ async function getOrRefreshRelayToken(email) {
158
+ const now = Date.now();
159
+ if (cachedRelayToken && cachedRelayToken.email === email && cachedRelayToken.expiresAt > now + 3e5) {
160
+ return cachedRelayToken.token;
170
161
  }
171
162
  try {
172
- const token = await getOrRefreshToken();
173
- let lastRateLimitError = null;
174
- for (let attempt = 1; attempt <= 3; attempt++) {
175
- try {
176
- const response = await fetch(SPARK_AI_CHAT, {
177
- method: "POST",
178
- headers: {
179
- "Content-Type": "application/json",
180
- Authorization: `Bearer ${token}`
181
- },
182
- body: JSON.stringify({
183
- messages: [
184
- { role: "system", content: systemPrompt },
185
- { role: "user", content: userPrompt }
186
- ]
187
- })
188
- });
189
- const data = await response.json();
190
- if (!response.ok) {
191
- const error = data.error || `Spark AI error: ${response.status}`;
192
- if (response.status === 429 && attempt < 3) {
193
- const retryAfter = response.headers.get("Retry-After");
194
- const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * Math.pow(2, attempt - 1);
195
- lastRateLimitError = {
196
- success: false,
197
- error,
198
- isRateLimit: true,
199
- ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
200
- };
201
- await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
202
- continue;
203
- }
204
- return {
163
+ const response = await fetch(RELAY_TOKEN_URL, {
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify({ email })
167
+ });
168
+ const data = await response.json().catch(() => ({}));
169
+ if (!response.ok || !data.success || !data.token) {
170
+ throw new Error(data.error || `Token request failed: ${response.status} ${response.statusText}`);
171
+ }
172
+ cachedRelayToken = {
173
+ token: data.token,
174
+ expiresAt: data.expiresAt || now + 36e5,
175
+ email
176
+ };
177
+ return data.token;
178
+ } catch (err) {
179
+ throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
180
+ }
181
+ }
182
+ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
183
+ let lastRateLimitError = null;
184
+ for (let attempt = 1; attempt <= 3; attempt++) {
185
+ try {
186
+ const response = await fetch(chatUrl, {
187
+ method: "POST",
188
+ headers: {
189
+ "Content-Type": "application/json",
190
+ Authorization: `Bearer ${bearerToken}`
191
+ },
192
+ body: JSON.stringify({
193
+ messages: [
194
+ { role: "system", content: systemPrompt },
195
+ { role: "user", content: userPrompt }
196
+ ]
197
+ })
198
+ });
199
+ let data = {};
200
+ const contentType = response.headers.get("content-type") ?? "";
201
+ if (contentType.includes("application/json")) {
202
+ data = await response.json();
203
+ }
204
+ if (!response.ok) {
205
+ const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
206
+ if (response.status === 429 && attempt < 3) {
207
+ const retryAfter = response.headers.get("Retry-After");
208
+ const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
209
+ lastRateLimitError = {
205
210
  success: false,
206
211
  error,
207
- isRateLimit: response.status === 429,
208
- isAuthError: response.status === 401 || response.status === 403
209
- };
210
- }
211
- if (!data.success || !data.response) {
212
- return {
213
- success: false,
214
- error: data.error || "No response from Spark AI"
212
+ isRateLimit: true,
213
+ ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
215
214
  };
215
+ await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
216
+ continue;
216
217
  }
217
218
  return {
218
- success: true,
219
- content: data.response,
220
- limits: {
221
- used: data.limits?.used ?? 0,
222
- limit: data.limits?.limit ?? 100,
223
- tier,
224
- ...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
225
- }
219
+ success: false,
220
+ error,
221
+ isRateLimit: response.status === 429,
222
+ isAuthError: response.status === 401 || response.status === 403
223
+ };
224
+ }
225
+ if (!data.success || !data.response) {
226
+ return {
227
+ success: false,
228
+ error: data.error || "No response from Spark AI"
226
229
  };
227
- } catch (err) {
228
- if (attempt === 3) throw err;
229
- const delayMs = 1e3 * Math.pow(2, attempt - 1);
230
- await new Promise((resolve9) => setTimeout(resolve9, delayMs));
231
230
  }
231
+ return {
232
+ success: true,
233
+ content: data.response,
234
+ limits: {
235
+ used: data.limits?.used ?? 0,
236
+ limit: data.limits?.limit ?? 100,
237
+ tier,
238
+ ...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
239
+ }
240
+ };
241
+ } catch (err) {
242
+ if (attempt === 3) throw err;
243
+ const delayMs = 1e3 * 2 ** (attempt - 1);
244
+ await new Promise((resolve9) => setTimeout(resolve9, delayMs));
232
245
  }
233
- return lastRateLimitError || { success: false, error: "Request failed after retries" };
246
+ }
247
+ return lastRateLimitError || { success: false, error: "Request failed after retries" };
248
+ }
249
+ async function callAI(systemPrompt, userPrompt, tier = "free") {
250
+ if (!apiKey()) {
251
+ return {
252
+ success: false,
253
+ error: "AI service is not configured. Please report this issue.",
254
+ isAuthError: true
255
+ };
256
+ }
257
+ try {
258
+ const token = await getOrRefreshDirectToken();
259
+ return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
260
+ } catch (err) {
261
+ return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
262
+ }
263
+ }
264
+ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
265
+ if (!email) {
266
+ return {
267
+ success: false,
268
+ error: "Sign in required. Run qp login to connect your account.",
269
+ isAuthError: true
270
+ };
271
+ }
272
+ try {
273
+ let token = await getOrRefreshRelayToken(email);
274
+ let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
275
+ if (!result.success && result.isAuthError) {
276
+ cachedRelayToken = null;
277
+ token = await getOrRefreshRelayToken(email);
278
+ result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
279
+ }
280
+ return result;
234
281
  } catch (err) {
235
282
  return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
236
283
  }
@@ -260,7 +307,7 @@ function normalizeUrl(input) {
260
307
  url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
261
308
  return `https://${url}`;
262
309
  }
263
- var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, API_KEY, cachedToken;
310
+ var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
264
311
  var init_api_client = __esm({
265
312
  "src/lib/api-client.ts"() {
266
313
  "use strict";
@@ -268,14 +315,20 @@ var init_api_client = __esm({
268
315
  SPARK_AI_BASE = "https://sparkbrain.app";
269
316
  SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
270
317
  SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
271
- API_KEY = "spark_ai-8eefdbe9be3069cad2566cb5753dcff9";
272
- cachedToken = null;
318
+ RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
319
+ RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
320
+ RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
321
+ cachedDirectToken = null;
322
+ cachedRelayToken = null;
273
323
  }
274
324
  });
275
325
 
276
326
  // src/lib/SparkStripe-feature-gating.ts
277
327
  async function _fetch(email, init) {
278
- const r = await fetch(`${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`, init);
328
+ const r = await fetch(
329
+ `${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`,
330
+ init
331
+ );
279
332
  if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
280
333
  return r.json();
281
334
  }
@@ -318,24 +371,27 @@ var init_SparkStripe_feature_gating = __esm({
318
371
  APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
319
372
  BASE = "https://sparkstripe.com";
320
373
  TIERS = {
321
- "price_1TADnPEf9opkuWid9NtqOYON": "pro",
322
- "price_1TADnPEf9opkuWidgbvvX9ma": "pro",
323
- "price_1TADm7Ef9opkuWid3I9tquIB": "pro",
324
- "price_1TADm8Ef9opkuWid1RSA2wCa": "pro",
325
- "price_1TCREmEf9opkuWidvhuqzqDM": "pro",
326
- "price_1TCREmEf9opkuWidacf1tiC7": "pro"
374
+ price_1TADnPEf9opkuWid9NtqOYON: "pro",
375
+ price_1TADnPEf9opkuWidgbvvX9ma: "pro",
376
+ price_1TADm7Ef9opkuWid3I9tquIB: "pro",
377
+ price_1TADm8Ef9opkuWid1RSA2wCa: "pro",
378
+ price_1TCREmEf9opkuWidvhuqzqDM: "pro",
379
+ price_1TCREmEf9opkuWidacf1tiC7: "pro"
327
380
  };
328
381
  RANK = {
329
- "free": 0,
330
- "pro": 1
382
+ free: 0,
383
+ pro: 1
384
+ };
385
+ FEATURES = {
386
+ free: [],
387
+ pro: [
388
+ "Everything in Free",
389
+ "No watermark",
390
+ "Full video composition (voiceover + captions + music + transitions)",
391
+ "Unlimited videos",
392
+ "Priority support"
393
+ ]
331
394
  };
332
- FEATURES = { "free": [], "pro": [
333
- "Everything in Free",
334
- "No watermark",
335
- "Full video composition (voiceover + captions + music + transitions)",
336
- "Unlimited videos",
337
- "Priority support"
338
- ] };
339
395
  getStatus = (email) => _fetch(email);
340
396
  _cache = /* @__PURE__ */ new Map();
341
397
  isPaid = (d) => getTier(d) !== "free";
@@ -365,7 +421,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
365
421
  var init_config = __esm({
366
422
  "src/lib/config.ts"() {
367
423
  "use strict";
368
- VERSION = "1.1.1";
424
+ VERSION = "1.2.1";
369
425
  CONFIG_FILE = "quickpeek.config.json";
370
426
  DEFAULT_CONFIG = {
371
427
  plan: { maxSteps: 8 },
@@ -532,14 +588,29 @@ function getActionForType(type) {
532
588
  return "type";
533
589
  }
534
590
  }
535
- async function crawlPage(url, task2 = noopTask) {
591
+ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
592
+ const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
593
+ const cached = crawlCache.get(cacheKey);
594
+ if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
595
+ task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
596
+ return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
597
+ }
536
598
  const browserTask = task2("Launching browser");
537
599
  const browser = await import_playwright.chromium.launch({ headless: true });
538
- const page = await browser.newPage();
539
- browserTask.succeed("Browser launched");
600
+ const context = await browser.newContext({
601
+ ...storageStatePath ? { storageState: storageStatePath } : {},
602
+ // Crawl at the size we will record at, or the measurements below describe
603
+ // a page nobody will ever see.
604
+ ...viewport ? { viewport } : {}
605
+ });
606
+ const page = await context.newPage();
607
+ browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
540
608
  const crawlTask = task2("Crawling page");
541
609
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
542
610
  const title = await page.title();
611
+ const scrollable = await page.evaluate(
612
+ "Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
613
+ );
543
614
  const rawElements = await page.evaluate(`
544
615
  (() => {
545
616
  const items = [];
@@ -669,9 +740,14 @@ async function crawlPage(url, task2 = noopTask) {
669
740
  });
670
741
  await browser.close();
671
742
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
672
- return { title, elements };
743
+ crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
744
+ if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
745
+ const oldest = crawlCache.keys().next().value;
746
+ if (oldest !== void 0) crawlCache.delete(oldest);
747
+ }
748
+ return { title, elements, scrollable };
673
749
  }
674
- var import_playwright, noopTask;
750
+ var import_playwright, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
675
751
  var init_crawl = __esm({
676
752
  "src/lib/crawl.ts"() {
677
753
  "use strict";
@@ -679,6 +755,9 @@ var init_crawl = __esm({
679
755
  init_scraping();
680
756
  noopTask = () => ({ succeed: () => {
681
757
  } });
758
+ CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
759
+ CRAWL_CACHE_MAX_ENTRIES = 50;
760
+ crawlCache = /* @__PURE__ */ new Map();
682
761
  }
683
762
  });
684
763
 
@@ -723,10 +802,10 @@ var init_watermark = __esm({
723
802
  // src/lib/ffmpeg/spawn.ts
724
803
  function toWSLPath(path) {
725
804
  if (!isWindowsRuntime) return path;
726
- return windowsPathToWSL(path) ?? path.replace(/\\/g, "/");
805
+ return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path.replace(/\\/g, "/");
727
806
  }
728
807
  function convertArgToWSL(arg2) {
729
- const wslPath = windowsPathToWSL(arg2);
808
+ const wslPath = (0, import_video_kit2.windowsDrivePathToWsl)(arg2);
730
809
  if (wslPath) return wslPath;
731
810
  if (arg2.includes("\\")) return arg2.replace(/\\/g, "/");
732
811
  return arg2;
@@ -823,20 +902,24 @@ function composeVideo(opts) {
823
902
  `[1:a]apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}[voice]`,
824
903
  // Music: fade in and fade out
825
904
  `[2:a]aloop=loop=-1:size=2e+09,atrim=0:${effectiveDuration},volume=${musicVolume},afade=t=in:st=0:d=${musicFadeIn},afade=t=out:st=${musicFadeOutStart}:d=${musicFadeOut}[music]`,
826
- `[voice][music]amix=inputs=2:duration=longest[aout]`
905
+ // Normalise the MIX and hard-limit its true peak - ported from
906
+ // VidLet, where single-pass loudnorm alone measured +0.9 dBTP on
907
+ // sharp TTS consonants. -14 LUFS is the YouTube/IG/TikTok playback
908
+ // target, so the platform leaves the audio untouched.
909
+ `[voice][music]amix=inputs=2:duration=longest,${import_video_kit3.MASTER_CHAIN}[aout]`
827
910
  ].join(";");
828
911
  args.push("-vf", vfStr);
829
912
  args.push("-filter_complex", complexFilter, "-map", "0:v", "-map", "[aout]");
830
913
  args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
831
914
  args.push("-c:a", "aac", "-b:a", audioBitrate);
832
915
  } else if (hasVoiceover) {
833
- const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}`;
916
+ const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn},${import_video_kit3.MASTER_CHAIN}`;
834
917
  args.push("-vf", vfStr);
835
918
  args.push("-map", "0:v", "-map", "1:a", "-af", af);
836
919
  args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
837
920
  args.push("-c:a", "aac", "-b:a", audioBitrate);
838
921
  } else if (hasMusic) {
839
- const musicFilter = `aloop=loop=-1:size=2e+09,atrim=0:${effectiveDuration},volume=${musicVolume},afade=t=in:st=0:d=${musicFadeIn},afade=t=out:st=${musicFadeOutStart}:d=${musicFadeOut}`;
922
+ const musicFilter = `aloop=loop=-1:size=2e+09,atrim=0:${effectiveDuration},volume=${musicVolume},afade=t=in:st=0:d=${musicFadeIn},afade=t=out:st=${musicFadeOutStart}:d=${musicFadeOut},${import_video_kit3.MASTER_CHAIN}`;
840
923
  args.push("-vf", vfStr);
841
924
  args.push("-map", "0:v", "-map", "1:a", "-af", musicFilter);
842
925
  args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
@@ -849,9 +932,11 @@ function composeVideo(opts) {
849
932
  runFFmpeg(args, "ffmpeg compose failed").then(resolve9, reject);
850
933
  });
851
934
  }
935
+ var import_video_kit3;
852
936
  var init_compose = __esm({
853
937
  "src/lib/ffmpeg/compose.ts"() {
854
938
  "use strict";
939
+ import_video_kit3 = require("@spark-apps/video-kit");
855
940
  init_watermark();
856
941
  init_spawn();
857
942
  }
@@ -1108,14 +1193,13 @@ function applyTransitions(opts) {
1108
1193
  filterParts.push(`[0:v]trim=start=${start.toFixed(3)}:end=${end.toFixed(3)},setpts=PTS-STARTPTS[v${i}]`);
1109
1194
  }
1110
1195
  let currentV = "v0";
1196
+ let chainDuration = boundaries[1] - boundaries[0];
1111
1197
  for (let i = 0; i < sorted.length; i++) {
1112
1198
  const trans = sorted[i];
1113
1199
  const duration = trans.duration ?? 0.3;
1114
1200
  const nextIdx = i + 1;
1115
- const segStart = boundaries[i];
1116
- const segEnd = boundaries[i + 1];
1117
- const segDuration = segEnd - segStart;
1118
- const offset = Math.max(0, segDuration - duration);
1201
+ const offset = Math.max(0, chainDuration - duration);
1202
+ chainDuration += boundaries[nextIdx + 1] - boundaries[nextIdx] - duration;
1119
1203
  const xfadeType = trans.type === "fade" ? "fadeblack" : "fade";
1120
1204
  const outV = i === sorted.length - 1 ? "outv" : `xv${i}`;
1121
1205
  filterParts.push(
@@ -1235,7 +1319,7 @@ EXAMPLES:
1235
1319
  \u2022 File upload: "target": "input[type=\\"file\\"]"
1236
1320
  \u2022 Slider: "target": "input[type=\\"range\\"]"`;
1237
1321
  }
1238
- function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
1322
+ function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
1239
1323
  const lang = langName ? `
1240
1324
  Language: Write ALL captions in ${langName}.` : "";
1241
1325
  const purpose = description ? `
@@ -1273,8 +1357,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
1273
1357
  ${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
1274
1358
  }
1275
1359
  const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
1360
+ const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
1361
+ const scrolling = cannotScroll ? `
1362
+
1363
+ SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
1364
+ const scrollRule = cannotScroll ? "\n\u2022 NEVER use the scroll action: this page does not scroll, so the video would freeze. Use wait and click steps only" : "";
1276
1365
  return `URL: ${url}
1277
- Title: ${title}${purpose}
1366
+ Title: ${title}${purpose}${scrolling}
1278
1367
 
1279
1368
  AVAILABLE ELEMENTS (grouped by action):
1280
1369
  ${elementSummary}
@@ -1284,7 +1373,7 @@ REQUIREMENTS:
1284
1373
  \u2022 Complete ONE user journey: input \u2192 process \u2192 output
1285
1374
  \u2022 For CLICK: use simplified text target like "extract frame" or "download"
1286
1375
  \u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
1287
- \u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
1376
+ \u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
1288
1377
 
1289
1378
  Return JSON:
1290
1379
  {
@@ -1307,33 +1396,31 @@ TARGET SYNTAX:
1307
1396
  \u2022 Sliders: input[type="range"]
1308
1397
  \u2022 Text inputs: Use selector from list above`;
1309
1398
  }
1399
+ var MIN_USEFUL_SCROLL_PX;
1310
1400
  var init_prompts = __esm({
1311
1401
  "src/lib/prompts.ts"() {
1312
1402
  "use strict";
1313
1403
  init_utils();
1404
+ MIN_USEFUL_SCROLL_PX = 200;
1314
1405
  }
1315
1406
  });
1316
1407
 
1317
1408
  // src/lib/styles.ts
1318
1409
  function generateASSHeader(width, height, font, size, primaryColor, secondaryColor, outlineColor, backColor, bold, outlineWidth, shadow, marginBottom) {
1319
- return `[Script Info]
1320
- Title: QuickPeek Demo
1321
- ScriptType: v4.00+
1322
- PlayResX: ${width}
1323
- PlayResY: ${height}
1324
-
1325
- [V4+ Styles]
1326
- Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1327
- Style: Karaoke,${font},${size},${primaryColor},${secondaryColor},${outlineColor},${backColor},${bold ? 1 : 0},0,0,0,100,100,0,0,1,${outlineWidth},${shadow},2,40,40,${marginBottom},1
1328
-
1329
- [Events]
1330
- Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
1331
- `;
1410
+ return (0, import_video_kit4.buildAssHeader)({
1411
+ title: "QuickPeek Demo",
1412
+ width,
1413
+ height,
1414
+ styles: [
1415
+ `Style: Karaoke,${font},${size},${primaryColor},${secondaryColor},${outlineColor},${backColor},${bold ? 1 : 0},0,0,0,100,100,0,0,1,${outlineWidth},${shadow},2,40,40,${marginBottom},1`
1416
+ ]
1417
+ });
1332
1418
  }
1333
- var CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
1419
+ var import_video_kit4, CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
1334
1420
  var init_styles = __esm({
1335
1421
  "src/lib/styles.ts"() {
1336
1422
  "use strict";
1423
+ import_video_kit4 = require("@spark-apps/video-kit");
1337
1424
  init_cursor();
1338
1425
  CURSOR_MOVE_DURATION = 800;
1339
1426
  CURSOR_CSS = `
@@ -1416,7 +1503,7 @@ var init_subscription_client = __esm({
1416
1503
 
1417
1504
  // src/lib/tts/helpers.ts
1418
1505
  function toWSLPathForTTS(path) {
1419
- return windowsPathToWSL(path) ?? path;
1506
+ return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path;
1420
1507
  }
1421
1508
  function estimateActionDuration(step) {
1422
1509
  if (step.action === "drag" && step.value === "around") {
@@ -1446,8 +1533,8 @@ async function getMediaStepDuration(step, baseDir) {
1446
1533
  if (!["mp4", "webm", "mov"].includes(ext)) {
1447
1534
  return 0;
1448
1535
  }
1449
- const mediaPath = (0, import_node_path.isAbsolute)(step.value) ? step.value : (0, import_node_path.resolve)(baseDir, step.value);
1450
- if (!(0, import_node_fs.existsSync)(mediaPath)) {
1536
+ const mediaPath = (0, import_node_path2.isAbsolute)(step.value) ? step.value : (0, import_node_path2.resolve)(baseDir, step.value);
1537
+ if (!(0, import_node_fs2.existsSync)(mediaPath)) {
1451
1538
  return 0;
1452
1539
  }
1453
1540
  try {
@@ -1467,38 +1554,38 @@ async function getMediaStepDuration(step, baseDir) {
1467
1554
  }
1468
1555
  }
1469
1556
  async function loadVoiceoverMeta(outputDir) {
1470
- const metaPath = (0, import_node_path.join)(outputDir, "voiceover.json");
1557
+ const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
1471
1558
  try {
1472
- if (!(0, import_node_fs.existsSync)(metaPath)) return null;
1473
- const content = await (0, import_promises.readFile)(metaPath, "utf-8");
1559
+ if (!(0, import_node_fs2.existsSync)(metaPath)) return null;
1560
+ const content = await (0, import_promises2.readFile)(metaPath, "utf-8");
1474
1561
  return JSON.parse(content);
1475
1562
  } catch {
1476
1563
  return null;
1477
1564
  }
1478
1565
  }
1479
1566
  async function saveVoiceoverMeta(outputDir, meta) {
1480
- const metaPath = (0, import_node_path.join)(outputDir, "voiceover.json");
1481
- await (0, import_promises.writeFile)(metaPath, JSON.stringify(meta, null, 2));
1567
+ const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
1568
+ await (0, import_promises2.writeFile)(metaPath, JSON.stringify(meta, null, 2));
1482
1569
  }
1483
1570
  async function concatenateAudio(inputs, output, workDir) {
1484
1571
  const formatPath = (f) => {
1485
- const resolved = (0, import_node_path.resolve)(f);
1486
- return isWindows ? windowsPathToWSL(resolved) ?? resolved : resolved;
1572
+ const resolved = (0, import_node_path2.resolve)(f);
1573
+ return isWindows ? (0, import_video_kit2.windowsDrivePathToWsl)(resolved) ?? resolved : resolved;
1487
1574
  };
1488
1575
  const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
1489
- const listPath = (0, import_node_path.join)(workDir, "concat.txt");
1490
- await (0, import_promises.writeFile)(listPath, listContent);
1576
+ const listPath = (0, import_node_path2.join)(workDir, "concat.txt");
1577
+ await (0, import_promises2.writeFile)(listPath, listContent);
1491
1578
  await concatMedia(listPath, output);
1492
- await (0, import_promises.unlink)(listPath).catch(() => {
1579
+ await (0, import_promises2.unlink)(listPath).catch(() => {
1493
1580
  });
1494
1581
  }
1495
- var import_node_fs, import_promises, import_node_path, isWindows;
1582
+ var import_node_fs2, import_promises2, import_node_path2, isWindows;
1496
1583
  var init_helpers = __esm({
1497
1584
  "src/lib/tts/helpers.ts"() {
1498
1585
  "use strict";
1499
- import_node_fs = require("fs");
1500
- import_promises = require("fs/promises");
1501
- import_node_path = require("path");
1586
+ import_node_fs2 = require("fs");
1587
+ import_promises2 = require("fs/promises");
1588
+ import_node_path2 = require("path");
1502
1589
  init_ffmpeg2();
1503
1590
  init_utils();
1504
1591
  isWindows = process.platform === "win32";
@@ -1534,7 +1621,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1534
1621
  if (!needsRealignment) {
1535
1622
  return stepAudio;
1536
1623
  }
1537
- const audioDir = (0, import_node_path2.join)(workDir, "audio");
1624
+ const audioDir = (0, import_node_path3.join)(workDir, "audio");
1538
1625
  const segments = [];
1539
1626
  const alignedStepAudio = /* @__PURE__ */ new Map();
1540
1627
  for (let i = 0; i < actualRecordingDurations.length; i++) {
@@ -1543,20 +1630,20 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1543
1630
  const voiceDuration = voiceData?.duration ?? 0;
1544
1631
  const recordingDuration = actualRecordingDurations[i] ?? 0;
1545
1632
  const targetDuration = Math.max(voiceDuration, recordingDuration);
1546
- const audioPath = (0, import_node_path2.join)(audioDir, `step-${stepNum}.mp3`);
1547
- const originalPaddingPath = (0, import_node_path2.join)(audioDir, `step-${stepNum}-padding.mp3`);
1548
- const silencePath = (0, import_node_path2.join)(audioDir, `step-${stepNum}-silence.mp3`);
1549
- if ((0, import_node_fs2.existsSync)(audioPath)) {
1633
+ const audioPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}.mp3`);
1634
+ const originalPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-padding.mp3`);
1635
+ const silencePath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-silence.mp3`);
1636
+ if ((0, import_node_fs3.existsSync)(audioPath)) {
1550
1637
  segments.push(audioPath);
1551
- if ((0, import_node_fs2.existsSync)(originalPaddingPath)) {
1638
+ if ((0, import_node_fs3.existsSync)(originalPaddingPath)) {
1552
1639
  segments.push(originalPaddingPath);
1553
1640
  }
1554
1641
  if (recordingDuration > voiceDuration) {
1555
- const alignPaddingPath = (0, import_node_path2.join)(audioDir, `step-${stepNum}-align-padding.mp3`);
1642
+ const alignPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-align-padding.mp3`);
1556
1643
  await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
1557
1644
  segments.push(alignPaddingPath);
1558
1645
  }
1559
- } else if ((0, import_node_fs2.existsSync)(silencePath)) {
1646
+ } else if ((0, import_node_fs3.existsSync)(silencePath)) {
1560
1647
  if (targetDuration !== voiceDuration) {
1561
1648
  await generateSilence(silencePath, targetDuration);
1562
1649
  }
@@ -1568,7 +1655,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1568
1655
  });
1569
1656
  }
1570
1657
  if (segments.length > 0) {
1571
- const voiceoverPath = (0, import_node_path2.join)(outputDir, "voiceover.wav");
1658
+ const voiceoverPath = (0, import_node_path3.join)(outputDir, "voiceover.wav");
1572
1659
  await concatenateAudio(segments, voiceoverPath, workDir);
1573
1660
  }
1574
1661
  const existingMeta = await loadVoiceoverMeta(outputDir);
@@ -1580,12 +1667,12 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1580
1667
  }
1581
1668
  return alignedStepAudio;
1582
1669
  }
1583
- var import_node_fs2, import_node_path2;
1670
+ var import_node_fs3, import_node_path3;
1584
1671
  var init_timing = __esm({
1585
1672
  "src/lib/tts/timing.ts"() {
1586
1673
  "use strict";
1587
- import_node_fs2 = require("fs");
1588
- import_node_path2 = require("path");
1674
+ import_node_fs3 = require("fs");
1675
+ import_node_path3 = require("path");
1589
1676
  init_ffmpeg2();
1590
1677
  init_helpers();
1591
1678
  }
@@ -1616,7 +1703,7 @@ function canReuseVoiceover(steps, voice, existingMeta) {
1616
1703
  }
1617
1704
  async function loadExistingVoiceover(steps, outputDir, meta, config) {
1618
1705
  const { estimateWordTimings: estimateWordTimings2 } = await Promise.resolve().then(() => (init_timing(), timing_exports));
1619
- const voiceoverPath = (0, import_node_path3.join)(outputDir, "voiceover.wav");
1706
+ const voiceoverPath = (0, import_node_path4.join)(outputDir, "voiceover.wav");
1620
1707
  const stepAudio = /* @__PURE__ */ new Map();
1621
1708
  for (let i = 0; i < steps.length; i++) {
1622
1709
  const step = steps[i];
@@ -1629,97 +1716,29 @@ async function loadExistingVoiceover(steps, outputDir, meta, config) {
1629
1716
  }
1630
1717
  return { voiceoverPath, stepAudio };
1631
1718
  }
1632
- var import_node_path3;
1719
+ var import_node_path4;
1633
1720
  var init_cache = __esm({
1634
1721
  "src/lib/tts/cache.ts"() {
1635
1722
  "use strict";
1636
- import_node_path3 = require("path");
1723
+ import_node_path4 = require("path");
1637
1724
  init_helpers();
1638
1725
  }
1639
1726
  });
1640
1727
 
1641
1728
  // src/lib/voices.ts
1642
- var LANGUAGE_VOICES;
1729
+ var import_video_kit5;
1643
1730
  var init_voices = __esm({
1644
1731
  "src/lib/voices.ts"() {
1645
1732
  "use strict";
1646
- LANGUAGE_VOICES = {
1647
- en: "en-GB-SoniaNeural",
1648
- ar: "ar-LB-LaylaNeural",
1649
- es: "es-ES-ElviraNeural",
1650
- fr: "fr-FR-DeniseNeural",
1651
- de: "de-DE-KatjaNeural",
1652
- it: "it-IT-ElsaNeural",
1653
- pt: "pt-BR-FranciscaNeural",
1654
- ru: "ru-RU-SvetlanaNeural",
1655
- zh: "zh-CN-XiaoxiaoNeural",
1656
- ja: "ja-JP-NanamiNeural",
1657
- ko: "ko-KR-SunHiNeural",
1658
- hi: "hi-IN-SwaraNeural",
1659
- tr: "tr-TR-EmelNeural",
1660
- nl: "nl-NL-ColetteNeural",
1661
- pl: "pl-PL-AgnieszkaNeural",
1662
- sv: "sv-SE-SofieNeural",
1663
- he: "he-IL-HilaNeural",
1664
- th: "th-TH-PremwadeeNeural",
1665
- vi: "vi-VN-HoaiMyNeural",
1666
- id: "id-ID-GadisNeural",
1667
- ms: "ms-MY-YasminNeural",
1668
- fil: "fil-PH-BlessicaNeural",
1669
- uk: "uk-UA-PolinaNeural",
1670
- cs: "cs-CZ-VlastaNeural",
1671
- el: "el-GR-AthinaNeural",
1672
- ro: "ro-RO-AlinaNeural",
1673
- hu: "hu-HU-NoemiNeural",
1674
- da: "da-DK-ChristelNeural",
1675
- fi: "fi-FI-NooraNeural",
1676
- no: "nb-NO-PernilleNeural",
1677
- sk: "sk-SK-ViktoriaNeural",
1678
- bg: "bg-BG-KalinaNeural",
1679
- hr: "hr-HR-GabrijelaNeural",
1680
- sl: "sl-SI-PetraNeural",
1681
- et: "et-EE-AnuNeural",
1682
- lv: "lv-LV-EveritaNeural",
1683
- lt: "lt-LT-OnaNeural",
1684
- bn: "bn-IN-TanishaaNeural",
1685
- ta: "ta-IN-PallaviNeural",
1686
- te: "te-IN-ShrutiNeural",
1687
- mr: "mr-IN-AarohiNeural",
1688
- gu: "gu-IN-DhwaniNeural",
1689
- kn: "kn-IN-SapnaNeural",
1690
- ml: "ml-IN-SobhanaNeural",
1691
- ur: "ur-PK-UzmaNeural",
1692
- fa: "fa-IR-DilaraNeural",
1693
- sw: "sw-KE-ZuriNeural",
1694
- af: "af-ZA-AdriNeural",
1695
- ca: "ca-ES-JoanaNeural",
1696
- cy: "cy-GB-NiaNeural",
1697
- ga: "ga-IE-OrlaNeural",
1698
- mt: "mt-MT-GraceNeural",
1699
- is: "is-IS-GudrunNeural",
1700
- mk: "mk-MK-MarijaNeural",
1701
- sr: "sr-RS-SophieNeural",
1702
- bs: "bs-BA-VesnaNeural",
1703
- sq: "sq-AL-AnilaNeural",
1704
- am: "am-ET-MekdesNeural",
1705
- az: "az-AZ-BabekNeural",
1706
- ka: "ka-GE-EkaNeural",
1707
- kk: "kk-KZ-AigulNeural",
1708
- mn: "mn-MN-YesUINeural",
1709
- ne: "ne-NP-HemkalaNeural",
1710
- ps: "ps-AF-LatifaNeural",
1711
- si: "si-LK-ThiliniNeural",
1712
- so: "so-SO-UbaxNeural",
1713
- zu: "zu-ZA-ThandoNeural"
1714
- };
1733
+ import_video_kit5 = require("@spark-apps/video-kit");
1715
1734
  }
1716
1735
  });
1717
1736
 
1718
1737
  // src/lib/tts/synthesize.ts
1719
1738
  async function synthesizeSpeech(text, outputPath, voice) {
1720
- const tempScriptPath = (0, import_node_path4.join)((0, import_node_path4.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
1721
- const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path4.resolve)(tempScriptPath)) : tempScriptPath;
1722
- const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path4.resolve)(outputPath)) : outputPath;
1739
+ const tempScriptPath = (0, import_node_path5.join)((0, import_node_path5.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
1740
+ const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(tempScriptPath)) : tempScriptPath;
1741
+ const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(outputPath)) : outputPath;
1723
1742
  const pythonScript = `
1724
1743
  import asyncio
1725
1744
  from edge_tts import Communicate
@@ -1734,7 +1753,7 @@ async def main():
1734
1753
  asyncio.run(main())
1735
1754
  `;
1736
1755
  try {
1737
- await (0, import_promises2.writeFile)(tempScriptPath, pythonScript);
1756
+ await (0, import_promises3.writeFile)(tempScriptPath, pythonScript);
1738
1757
  const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
1739
1758
  await execAsync(pythonCmd, { timeout: 3e4 });
1740
1759
  await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
@@ -1762,13 +1781,13 @@ asyncio.run(main())
1762
1781
  }
1763
1782
  }
1764
1783
  }
1765
- var import_node_child_process2, import_promises2, import_node_path4, import_node_util, execAsync;
1784
+ var import_node_child_process2, import_promises3, import_node_path5, import_node_util, execAsync;
1766
1785
  var init_synthesize = __esm({
1767
1786
  "src/lib/tts/synthesize.ts"() {
1768
1787
  "use strict";
1769
1788
  import_node_child_process2 = require("child_process");
1770
- import_promises2 = require("fs/promises");
1771
- import_node_path4 = require("path");
1789
+ import_promises3 = require("fs/promises");
1790
+ import_node_path5 = require("path");
1772
1791
  import_node_util = require("util");
1773
1792
  init_helpers();
1774
1793
  execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
@@ -1782,7 +1801,7 @@ function estimateDurationFromText(text, wordsPerMinute = 150) {
1782
1801
  }
1783
1802
  function resolveVoice(config) {
1784
1803
  if (config.audio?.voice) return config.audio.voice;
1785
- if (config.lang) return LANGUAGE_VOICES[config.lang] || null;
1804
+ if (config.lang) return import_video_kit5.WIDE_VOICES[config.lang] || null;
1786
1805
  return null;
1787
1806
  }
1788
1807
  async function generateVoiceover(steps, config, workDir, outputDir, logError2, task2, info2) {
@@ -1807,8 +1826,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1807
1826
  info2("No voice configured");
1808
1827
  return { voiceoverPath: "", stepAudio: stepAudio2 };
1809
1828
  }
1810
- const audioDir = (0, import_node_path5.join)(workDir, "audio");
1811
- await (0, import_promises3.mkdir)(audioDir, { recursive: true });
1829
+ const audioDir = (0, import_node_path6.join)(workDir, "audio");
1830
+ await (0, import_promises4.mkdir)(audioDir, { recursive: true });
1812
1831
  const segments = [];
1813
1832
  const stepAudio = /* @__PURE__ */ new Map();
1814
1833
  const ttsTask = task2("Generating voiceover");
@@ -1820,13 +1839,13 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1820
1839
  const manualDuration = (step.duration ?? 0) * 1e3;
1821
1840
  const mediaDuration = await getMediaStepDuration(step, outputDir);
1822
1841
  const silenceDuration = Math.max(actionDuration, manualDuration, mediaDuration, config.timing.minStepDuration);
1823
- const silencePath = (0, import_node_path5.join)(audioDir, `step-${stepNum}-silence.mp3`);
1842
+ const silencePath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-silence.mp3`);
1824
1843
  await generateSilence(silencePath, silenceDuration);
1825
1844
  segments.push(silencePath);
1826
1845
  stepAudio.set(stepNum, { duration: silenceDuration, words: [] });
1827
1846
  continue;
1828
1847
  }
1829
- const audioPath = (0, import_node_path5.join)(audioDir, `step-${stepNum}.mp3`);
1848
+ const audioPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}.mp3`);
1830
1849
  if (segments.length > 0) await new Promise((r) => setTimeout(r, 200));
1831
1850
  try {
1832
1851
  await synthesizeSpeech(step.caption, audioPath, voice);
@@ -1843,7 +1862,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1843
1862
  mediaDuration
1844
1863
  );
1845
1864
  if (stepDuration > audioDuration) {
1846
- const paddingPath = (0, import_node_path5.join)(audioDir, `step-${stepNum}-padding.mp3`);
1865
+ const paddingPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-padding.mp3`);
1847
1866
  await generateSilence(paddingPath, stepDuration - audioDuration);
1848
1867
  segments.push(paddingPath);
1849
1868
  }
@@ -1859,8 +1878,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1859
1878
  return { voiceoverPath: "", stepAudio };
1860
1879
  }
1861
1880
  const concatTask = task2("Combining audio");
1862
- await (0, import_promises3.mkdir)(outputDir, { recursive: true });
1863
- const voiceoverPath = (0, import_node_path5.join)(outputDir, "voiceover.wav");
1881
+ await (0, import_promises4.mkdir)(outputDir, { recursive: true });
1882
+ const voiceoverPath = (0, import_node_path6.join)(outputDir, "voiceover.wav");
1864
1883
  await concatenateAudio(segments, voiceoverPath, workDir);
1865
1884
  concatTask.succeed("Done");
1866
1885
  const meta = {
@@ -1873,12 +1892,12 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1873
1892
  await saveVoiceoverMeta(outputDir, meta);
1874
1893
  return { voiceoverPath, stepAudio };
1875
1894
  }
1876
- var import_promises3, import_node_path5;
1895
+ var import_promises4, import_node_path6;
1877
1896
  var init_generate = __esm({
1878
1897
  "src/lib/tts/generate.ts"() {
1879
1898
  "use strict";
1880
- import_promises3 = require("fs/promises");
1881
- import_node_path5 = require("path");
1899
+ import_promises4 = require("fs/promises");
1900
+ import_node_path6 = require("path");
1882
1901
  init_ffmpeg2();
1883
1902
  init_voices();
1884
1903
  init_helpers();
@@ -1933,7 +1952,7 @@ __export(lib_exports, {
1933
1952
  EXCLUDED_LINK_PATTERNS: () => EXCLUDED_LINK_PATTERNS,
1934
1953
  FEATURES: () => FEATURES,
1935
1954
  INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
1936
- LANGUAGE_VOICES: () => LANGUAGE_VOICES,
1955
+ LANGUAGE_VOICES: () => import_video_kit5.WIDE_VOICES,
1937
1956
  VERSION: () => VERSION,
1938
1957
  alignVoiceoverToRecording: () => alignVoiceoverToRecording,
1939
1958
  applyTransitions: () => applyTransitions,
@@ -1943,6 +1962,7 @@ __export(lib_exports, {
1943
1962
  buildUserPrompt: () => buildUserPrompt,
1944
1963
  buildWatermarkFilters: () => buildWatermarkFilters,
1945
1964
  callAI: () => callAI,
1965
+ callAIViaRelay: () => callAIViaRelay,
1946
1966
  canReuseVoiceover: () => canReuseVoiceover,
1947
1967
  composeVideo: () => composeVideo,
1948
1968
  concatMedia: () => concatMedia,
@@ -1961,7 +1981,7 @@ __export(lib_exports, {
1961
1981
  getUpgradeUrl: () => getUpgradeUrl,
1962
1982
  hasAccess: () => hasAccess,
1963
1983
  hasTrialRemaining: () => hasTrialRemaining,
1964
- hexToASS: () => hexToASS,
1984
+ hexToASS: () => import_video_kit.hexToAss,
1965
1985
  isCanceling: () => isCanceling,
1966
1986
  isExcludedLink: () => isExcludedLink,
1967
1987
  isPaid: () => isPaid,
@@ -1983,7 +2003,7 @@ __export(lib_exports, {
1983
2003
  spliceVideos: () => spliceVideos,
1984
2004
  synthesizeSpeech: () => synthesizeSpeech,
1985
2005
  toTitleCase: () => toTitleCase,
1986
- windowsPathToWSL: () => windowsPathToWSL
2006
+ windowsPathToWSL: () => import_video_kit2.windowsDrivePathToWsl
1987
2007
  });
1988
2008
  var init_lib = __esm({
1989
2009
  "src/lib/index.ts"() {
@@ -1995,9 +2015,9 @@ var init_lib = __esm({
1995
2015
  init_cursor();
1996
2016
  init_ffmpeg2();
1997
2017
  init_prompts();
2018
+ init_SparkStripe_feature_gating();
1998
2019
  init_scraping();
1999
2020
  init_styles();
2000
- init_SparkStripe_feature_gating();
2001
2021
  init_subscription_client();
2002
2022
  init_tts2();
2003
2023
  init_types3();
@@ -2009,199 +2029,691 @@ var init_lib = __esm({
2009
2029
 
2010
2030
  // src/qp.ts
2011
2031
  var import_dotenv = __toESM(require("dotenv"));
2012
- var import_chalk18 = __toESM(require("chalk"));
2013
-
2014
- // src/commands/capture-cmd.ts
2015
- var import_node_path19 = require("path");
2016
- var import_chalk9 = __toESM(require("chalk"));
2017
-
2018
- // src/ai/narrate.ts
2019
- init_lib();
2032
+ var import_chalk21 = __toESM(require("chalk"));
2020
2033
 
2021
- // src/ai/client.ts
2022
- var import_node_readline = require("readline");
2023
- var import_chalk2 = __toESM(require("chalk"));
2024
- init_lib();
2034
+ // src/commands/auth-cmd.ts
2035
+ var import_node_readline2 = require("readline");
2036
+ var import_chalk3 = __toESM(require("chalk"));
2037
+ var import_playwright2 = require("playwright");
2025
2038
 
2026
- // src/utils/browser-auth.ts
2027
- var import_node_http = require("http");
2028
- var import_node_url = require("url");
2029
- var import_chalk = __toESM(require("chalk"));
2030
- init_SparkStripe_feature_gating();
2031
- var TIMEOUT_MS = 12e4;
2032
- async function openBrowser(url) {
2033
- const { exec: exec2 } = await import("child_process");
2034
- const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
2035
- const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
2036
- return new Promise((resolve9) => {
2037
- exec2(command, () => resolve9());
2038
- });
2039
+ // src/browser/auth-state.ts
2040
+ var import_node_fs = require("fs");
2041
+ var import_promises = require("fs/promises");
2042
+ var import_node_os = require("os");
2043
+ var import_node_path = require("path");
2044
+ var AUTH_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".quickpeek", "auth");
2045
+ function hostOf(url) {
2046
+ try {
2047
+ return new URL(url).host;
2048
+ } catch {
2049
+ return null;
2050
+ }
2039
2051
  }
2040
- async function browserLogin() {
2041
- return new Promise((resolve9) => {
2042
- let server;
2043
- let timer;
2044
- const cleanup = (email) => {
2045
- clearTimeout(timer);
2046
- server?.close();
2047
- resolve9(email);
2048
- };
2049
- server = (0, import_node_http.createServer)((req, res) => {
2050
- const url = new import_node_url.URL(req.url || "/", `http://localhost`);
2051
- const path = url.pathname;
2052
- if (path === "/callback" || path === "/") {
2053
- const email = url.searchParams.get("email");
2054
- res.writeHead(200, { "Content-Type": "text/html" });
2055
- res.end(`
2056
- <!DOCTYPE html>
2057
- <html>
2058
- <head><title>QuickPeek</title><style>
2059
- body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff; }
2060
- .card { text-align: center; padding: 2rem; }
2061
- h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
2062
- p { color: #888; }
2063
- </style></head>
2064
- <body><div class="card">
2065
- <h1>${email ? "Logged in!" : "Return to your terminal"}</h1>
2066
- <p>${email ? `Signed in as ${email}` : "Complete setup in your terminal."}</p>
2067
- <p style="margin-top: 1rem; font-size: 0.875rem;">You can close this tab.</p>
2068
- </div></body>
2069
- </html>
2070
- `);
2071
- if (email) {
2072
- cleanup(email);
2073
- }
2074
- } else {
2075
- res.writeHead(404);
2076
- res.end();
2077
- }
2078
- });
2079
- server.listen(0, "127.0.0.1", () => {
2080
- const addr = server.address();
2081
- if (!addr || typeof addr === "string") {
2082
- cleanup(null);
2083
- return;
2084
- }
2085
- const port = addr.port;
2086
- const returnUrl = `http://localhost:${port}/callback`;
2087
- const url = pricingUrl({ returnUrl });
2088
- console.log();
2089
- console.log(import_chalk.default.cyan(" Opening browser to sign in..."));
2090
- console.log(import_chalk.default.dim(` ${url}`));
2091
- console.log();
2092
- console.log(import_chalk.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
2093
- openBrowser(url);
2094
- });
2095
- timer = setTimeout(() => {
2096
- console.log();
2097
- console.log(import_chalk.default.yellow(" Sign-in timed out."));
2098
- cleanup(null);
2099
- }, TIMEOUT_MS);
2100
- });
2052
+ function authStatePath(url) {
2053
+ const host = hostOf(url);
2054
+ return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.json`) : null;
2101
2055
  }
2102
-
2103
- // src/ai/client.ts
2104
- var lastRateLimitInfo = null;
2105
- function getLastRateLimitInfo() {
2106
- return lastRateLimitInfo;
2056
+ function findAuthState(url) {
2057
+ const path = authStatePath(url);
2058
+ return path && (0, import_node_fs.existsSync)(path) ? path : void 0;
2107
2059
  }
2108
- function prompt(question) {
2109
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
2110
- return new Promise((resolve9) => {
2111
- rl.question(question, (answer) => {
2112
- rl.close();
2113
- resolve9(answer.trim());
2114
- });
2115
- });
2060
+ async function saveAuthState(url, state) {
2061
+ const path = authStatePath(url);
2062
+ if (!path) throw new Error(`Not a valid URL: ${url}`);
2063
+ await (0, import_promises.mkdir)(AUTH_DIR, { recursive: true });
2064
+ await (0, import_promises.writeFile)(path, JSON.stringify(state, null, 2));
2065
+ await (0, import_promises.chmod)(path, 384);
2066
+ return path;
2116
2067
  }
2117
- async function setupUserEmail() {
2118
- console.log();
2119
- console.log(import_chalk2.default.yellow(" \u2709 Account Setup"));
2120
- console.log(import_chalk2.default.dim(" \u2500".repeat(20)));
2121
- console.log();
2122
- console.log(import_chalk2.default.white(" Sign in with your Google account to track usage"));
2123
- console.log(import_chalk2.default.white(" and unlock Pro features."));
2124
- const browserEmail = await browserLogin();
2125
- if (browserEmail) {
2126
- console.log();
2127
- console.log(import_chalk2.default.green(` \u2713 Signed in as ${browserEmail}`));
2128
- console.log();
2129
- return browserEmail;
2130
- }
2131
- console.log();
2132
- console.log(import_chalk2.default.dim(" Or enter your email manually:"));
2133
- const email = await prompt(import_chalk2.default.white(" Email: "));
2134
- if (!email) {
2135
- console.log();
2136
- console.log(import_chalk2.default.red(" No email provided. Exiting."));
2137
- process.exit(1);
2138
- }
2139
- if (!email.includes("@") || !email.includes(".")) {
2140
- console.log();
2141
- console.log(import_chalk2.default.yellow(" \u26A0 That doesn't look like a valid email address."));
2142
- const proceed = await prompt(import_chalk2.default.white(" Continue anyway? ") + import_chalk2.default.dim("[y/N] "));
2143
- if (proceed.toLowerCase() !== "y") {
2144
- process.exit(1);
2145
- }
2068
+ async function clearAuthState(url) {
2069
+ if (url) {
2070
+ const path = authStatePath(url);
2071
+ if (!path || !(0, import_node_fs.existsSync)(path)) return [];
2072
+ await (0, import_promises.unlink)(path);
2073
+ return [hostOf(url)];
2146
2074
  }
2147
- console.log();
2148
- console.log(import_chalk2.default.green(" \u2713 Email registered"));
2149
- console.log();
2150
- return email;
2075
+ if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
2076
+ const files = (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json"));
2077
+ await Promise.all(files.map((f) => (0, import_promises.unlink)((0, import_node_path.join)(AUTH_DIR, f))));
2078
+ return files.map((f) => f.slice(0, -".json".length));
2151
2079
  }
2152
- async function callGroq(systemPrompt, userPrompt, logError2, _userEmail, tier = "free") {
2153
- const result = await callAI(systemPrompt, userPrompt, tier);
2154
- if (!result.success) {
2155
- await logError2("AI API", new Error(result.error));
2156
- if (result.isRateLimit) {
2157
- console.log();
2158
- console.log(import_chalk2.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
2159
- console.log();
2160
- console.log(import_chalk2.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
2161
- console.log(import_chalk2.default.dim(" Options:"));
2162
- if (result.retryAfterSeconds) {
2163
- const minutes = Math.ceil(result.retryAfterSeconds / 60);
2164
- console.log(import_chalk2.default.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
2165
- } else {
2166
- console.log(import_chalk2.default.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
2167
- }
2168
- console.log(import_chalk2.default.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
2169
- console.log(import_chalk2.default.dim(" \u2022 Upgrade to a higher tier for more requests"));
2170
- console.log();
2171
- process.exit(1);
2172
- }
2173
- if (result.isAuthError) {
2174
- console.log();
2175
- console.log(import_chalk2.default.red(" \u26A0 Authentication Error"));
2176
- console.log();
2177
- console.log(import_chalk2.default.dim(` ${result.error}`));
2178
- console.log();
2179
- process.exit(1);
2180
- }
2181
- console.log();
2182
- console.log(import_chalk2.default.red(" \u26A0 AI Error"));
2183
- console.log();
2184
- const firstLine = result.error.split("\n")[0] ?? result.error;
2185
- console.log(import_chalk2.default.dim(` ${firstLine.slice(0, 80)}`));
2186
- console.log();
2187
- process.exit(1);
2188
- }
2189
- lastRateLimitInfo = result.limits;
2190
- return result.content;
2080
+ async function listAuthStates() {
2081
+ if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
2082
+ return (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length));
2191
2083
  }
2192
2084
 
2193
- // src/ai/narrate.ts
2194
- async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
2195
- const langName = getLanguageName(lang);
2196
- const stepList = steps.map((s, i) => {
2197
- const parts = [`${i + 1}. ${s.action}`];
2198
- if (s.target) parts.push(`on "${s.target}"`);
2199
- if (s.value) parts.push(`= "${s.value}"`);
2200
- return parts.join(" ");
2201
- }).join("\n");
2202
- const systemPrompt = `You write professional demo video captions in ${langName}.
2203
- Given UI steps, return a JSON array of captions \u2014 one per step, in order.
2204
-
2085
+ // src/help.ts
2086
+ var import_chalk = __toESM(require("chalk"));
2087
+ init_lib();
2088
+
2089
+ // src/logo.ts
2090
+ var import_figlet = __toESM(require("figlet"));
2091
+ var import_gradient_string = __toESM(require("gradient-string"));
2092
+ var DEFAULT_LOGO_CONFIG = {
2093
+ text: "QuickPeek",
2094
+ font: "Slant",
2095
+ horizontalLayout: "default",
2096
+ gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
2097
+ suffix: " \u{1F440}"
2098
+ };
2099
+ function renderLogo(config = {}) {
2100
+ const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
2101
+ const ascii = import_figlet.default.textSync(resolved.text, {
2102
+ font: resolved.font,
2103
+ horizontalLayout: resolved.horizontalLayout
2104
+ });
2105
+ const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
2106
+ return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
2107
+ }
2108
+
2109
+ // src/utils/config-loader.ts
2110
+ var import_node_fs4 = require("fs");
2111
+ var import_promises5 = require("fs/promises");
2112
+ var import_node_os2 = require("os");
2113
+ var import_node_path7 = require("path");
2114
+ init_lib();
2115
+ async function loadConfig(_url, langOverride) {
2116
+ const configPath = (0, import_node_path7.join)(process.cwd(), CONFIG_FILE);
2117
+ if ((0, import_node_fs4.existsSync)(configPath)) {
2118
+ try {
2119
+ const content = await (0, import_promises5.readFile)(configPath, "utf-8");
2120
+ const userConfig = JSON.parse(content);
2121
+ const config = {
2122
+ lang: langOverride || userConfig.lang || "en",
2123
+ plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
2124
+ video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
2125
+ audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
2126
+ music: userConfig.music,
2127
+ timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
2128
+ output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
2129
+ };
2130
+ return { config, created: false };
2131
+ } catch {
2132
+ }
2133
+ }
2134
+ const fullDefaultConfig = {
2135
+ lang: "en",
2136
+ plan: { maxSteps: 8 },
2137
+ video: {
2138
+ width: 1080,
2139
+ height: 1920,
2140
+ fps: 30,
2141
+ quality: 0.5,
2142
+ preset: "fast",
2143
+ cursor: true,
2144
+ highlight: false,
2145
+ fadeIn: 500,
2146
+ // ms
2147
+ fadeOut: 500,
2148
+ // ms
2149
+ contrast: 0.25
2150
+ },
2151
+ audio: {
2152
+ // Voice is derived from lang setting via LANGUAGE_VOICES mapping
2153
+ // To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
2154
+ speed: 1
2155
+ },
2156
+ music: {
2157
+ path: "",
2158
+ volume: 0.3
2159
+ },
2160
+ output: { dir: "./demo" }
2161
+ };
2162
+ await (0, import_promises5.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
2163
+ return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
2164
+ }
2165
+ var WORK_DIR = ".quickpeek";
2166
+ async function prepareWorkDir() {
2167
+ const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
2168
+ if ((0, import_node_fs4.existsSync)(workDir)) {
2169
+ await (0, import_promises5.rm)(workDir, { recursive: true });
2170
+ }
2171
+ await (0, import_promises5.mkdir)(workDir, { recursive: true });
2172
+ return workDir;
2173
+ }
2174
+ var USER_DATA_DIR = (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".quickpeek");
2175
+ var USER_DATA_FILE = (0, import_node_path7.join)(USER_DATA_DIR, "user.json");
2176
+ async function loadUserData() {
2177
+ try {
2178
+ if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
2179
+ return JSON.parse(await (0, import_promises5.readFile)(USER_DATA_FILE, "utf-8"));
2180
+ }
2181
+ } catch {
2182
+ }
2183
+ return {};
2184
+ }
2185
+ async function saveUserData(userData) {
2186
+ if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
2187
+ await (0, import_promises5.mkdir)(USER_DATA_DIR, { recursive: true });
2188
+ }
2189
+ const existing = await loadUserData();
2190
+ const merged = { ...existing, ...userData };
2191
+ await (0, import_promises5.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
2192
+ }
2193
+ var DEFAULT_CAPTIONS_CSS = `.caption {
2194
+ font-family: Arial, sans-serif;
2195
+ -webkit-text-stroke: 3px #000000;
2196
+ }
2197
+ .caption .regular {
2198
+ font-size: 56px;
2199
+ color: #FFFFFF;
2200
+ }
2201
+ .caption .highlight {
2202
+ font-size: 64px;
2203
+ color: #FFFF00;
2204
+ }
2205
+ `;
2206
+ function parseCaptionCSS(css) {
2207
+ const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
2208
+ const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
2209
+ const regularBlock = css.match(/\.caption\s+\.regular\s*\{([^}]+)\}/)?.[1] || "";
2210
+ const highlightBlock = css.match(/\.caption\s+\.highlight\s*\{([^}]+)\}/)?.[1] || "";
2211
+ const fontMatch = captionBlock.match(/font-family:\s*"?([^";,]+)/);
2212
+ if (fontMatch) style.font = fontMatch[1].trim();
2213
+ const strokeMatch = captionBlock.match(/-webkit-text-stroke:\s*(\d+)px\s*(rgba?\([^)]+\)|#[A-Fa-f0-9]{6})/);
2214
+ if (strokeMatch) {
2215
+ style.outlineWidth = parseInt(strokeMatch[1], 10);
2216
+ style.outline = rgbaToHex(strokeMatch[2]);
2217
+ }
2218
+ const shadowMatch = captionBlock.match(/text-shadow:\s*(\d+)px/);
2219
+ if (shadowMatch) style.shadow = parseInt(shadowMatch[1], 10);
2220
+ const marginMatch = captionBlock.match(/margin-bottom:\s*(\d+)/);
2221
+ if (marginMatch) style.marginBottom = parseInt(marginMatch[1], 10);
2222
+ const parseWordStyle = (block, target) => {
2223
+ const colorMatch = block.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
2224
+ if (colorMatch) target.color = colorMatch[1];
2225
+ const sizeMatch = block.match(/font-size:\s*(\d+)/);
2226
+ if (sizeMatch) target.size = parseInt(sizeMatch[1], 10);
2227
+ const weightMatch = block.match(/font-weight:\s*(bold|normal|\d+)/);
2228
+ if (weightMatch) target.bold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
2229
+ };
2230
+ parseWordStyle(regularBlock, style.regular);
2231
+ parseWordStyle(highlightBlock, style.highlight);
2232
+ if (!regularBlock && !highlightBlock) {
2233
+ const colorMatch = captionBlock.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
2234
+ if (colorMatch) style.regular.color = colorMatch[1];
2235
+ const sizeMatch = captionBlock.match(/font-size:\s*(\d+)/);
2236
+ if (sizeMatch) {
2237
+ style.regular.size = parseInt(sizeMatch[1], 10);
2238
+ style.highlight.size = Math.round(style.regular.size * 1.15);
2239
+ }
2240
+ const highlightMatch = captionBlock.match(/-qp-highlight:\s*(#[A-Fa-f0-9]{6})/);
2241
+ if (highlightMatch) style.highlight.color = highlightMatch[1];
2242
+ const weightMatch = captionBlock.match(/font-weight:\s*(bold|normal|\d+)/);
2243
+ if (weightMatch) {
2244
+ const isBold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
2245
+ style.regular.bold = isBold;
2246
+ style.highlight.bold = isBold;
2247
+ }
2248
+ }
2249
+ return style;
2250
+ }
2251
+ async function loadCaptions() {
2252
+ const captionsPath = (0, import_node_path7.join)(process.cwd(), "captions.css");
2253
+ if (!(0, import_node_fs4.existsSync)(captionsPath)) {
2254
+ await (0, import_promises5.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
2255
+ return DEFAULT_CAPTIONS;
2256
+ }
2257
+ try {
2258
+ const css = await (0, import_promises5.readFile)(captionsPath, "utf-8");
2259
+ return parseCaptionCSS(css);
2260
+ } catch {
2261
+ return DEFAULT_CAPTIONS;
2262
+ }
2263
+ }
2264
+ async function savePlan(plan, planPath) {
2265
+ const cleanPlan = {
2266
+ ...plan,
2267
+ steps: plan.steps.map(({ duration, ...step }) => step)
2268
+ };
2269
+ await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(planPath), { recursive: true });
2270
+ await (0, import_promises5.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
2271
+ }
2272
+
2273
+ // src/help.ts
2274
+ var cmd = import_chalk.default.cyan;
2275
+ var arg = import_chalk.default.green;
2276
+ var opt = import_chalk.default.magenta;
2277
+ var desc = import_chalk.default.white;
2278
+ var ex = import_chalk.default.dim;
2279
+ function pad(colored, visible, width) {
2280
+ return colored + " ".repeat(Math.max(0, width - visible.length));
2281
+ }
2282
+ function showLogo() {
2283
+ const logo = renderLogo();
2284
+ console.log(logo);
2285
+ console.log(import_chalk.default.dim(` v${VERSION} - Auto-generate demo videos with AI`));
2286
+ console.log(import_chalk.default.italic.dim(" Because Tutorial Videos are Overrated!\n"));
2287
+ }
2288
+ function formatUsageLines(status, tier) {
2289
+ const lines = [];
2290
+ lines.push(`Videos generated: ${status.usage_count}`);
2291
+ if (tier !== "pro" && status.trial) {
2292
+ const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
2293
+ const used = max_uses - uses_remaining;
2294
+ lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
2295
+ if (!is_expired && days_remaining > 0) {
2296
+ lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
2297
+ }
2298
+ }
2299
+ if (tier === "pro" && status.subscription) {
2300
+ const end = status.subscription.current_period_end;
2301
+ if (status.subscription.is_lifetime) {
2302
+ lines.push("Lifetime access");
2303
+ } else if (isCanceling(status)) {
2304
+ const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
2305
+ lines.push(`Cancels: ${date}`);
2306
+ } else if (end) {
2307
+ const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
2308
+ lines.push(`Renews: ${date}`);
2309
+ }
2310
+ }
2311
+ return lines;
2312
+ }
2313
+ async function showAccount() {
2314
+ const userData = await loadUserData();
2315
+ if (!userData.email) {
2316
+ console.log(import_chalk.default.dim(" Not signed in. Run ") + cmd("qp login") + import_chalk.default.dim(" to sign in."));
2317
+ console.log();
2318
+ return;
2319
+ }
2320
+ const email = userData.email;
2321
+ const tier = userData.tier === "pro" ? "pro" : "free";
2322
+ const tierLabel = tier === "pro" ? import_chalk.default.green("Pro") : "Free";
2323
+ console.log(` ${tierLabel} plan ${import_chalk.default.dim("\xB7")} ${import_chalk.default.dim(email)}`);
2324
+ try {
2325
+ const status = await getStatusCached(email);
2326
+ for (const line of formatUsageLines(status, tier)) {
2327
+ console.log(import_chalk.default.dim(` ${line}`));
2328
+ }
2329
+ } catch {
2330
+ }
2331
+ console.log();
2332
+ }
2333
+ async function showHelp() {
2334
+ showLogo();
2335
+ await showAccount();
2336
+ const W = 28;
2337
+ const usage = [
2338
+ [`${cmd("qp")} ${arg("<url>")}`, `qp <url>`, "AI-generated demo video"],
2339
+ [
2340
+ `${cmd("qp")} ${arg("<url>")} ${opt("--script")} ${arg("<file>")}`,
2341
+ `qp <url> --script <file>`,
2342
+ "Use your own narration script"
2343
+ ],
2344
+ [`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
2345
+ [`${cmd("qp edit")} ${arg("<url>")}`, `qp edit <url>`, "Edit the demo plan in the browser"],
2346
+ [`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
2347
+ [`${cmd("qp logout")}`, `qp logout`, "Sign out"],
2348
+ [`${cmd("qp auth")} ${arg("<url>")}`, `qp auth <url>`, "Save a signed-in session for a site you demo"],
2349
+ [`${cmd("qp auth clear")} ${arg("[url]")}`, `qp auth clear [url]`, "Forget saved sessions"],
2350
+ [`${cmd("qp init")}`, `qp init`, "Create config file"],
2351
+ [`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
2352
+ ];
2353
+ const options = [
2354
+ [`${opt("lang=")}${arg("xx")}`, `lang=xx`, "Voice language (en, es, ar, zh, fr, de, ja...)"],
2355
+ [
2356
+ `${opt("-d")}, ${opt("--description")} ${arg('"..."')}`,
2357
+ `-d, --description "..."`,
2358
+ "Guide AI with an app description"
2359
+ ],
2360
+ [
2361
+ `${opt("-s")}, ${opt("--script")} ${arg("<file>")}`,
2362
+ `-s, --script <file>`,
2363
+ "Narration script (one caption per line)"
2364
+ ],
2365
+ [`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
2366
+ [`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
2367
+ [`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"]
2368
+ ];
2369
+ const examples = [
2370
+ `${ex("$")} ${cmd("qp")} ${arg("localhost:3000")}`,
2371
+ `${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("lang=")}${arg("es")} ${opt("-d")} ${arg('"E-commerce checkout flow"')}`,
2372
+ `${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("--script")} ${arg("narration.txt")}`,
2373
+ `${ex("$")} ${cmd("qp capture")} ${arg("localhost:3000")}`
2374
+ ];
2375
+ console.log();
2376
+ console.log(import_chalk.default.yellow.bold("USAGE"));
2377
+ for (const [colored, plain, d] of usage) {
2378
+ console.log(` ${pad(colored, plain, W)}${desc(d)}`);
2379
+ }
2380
+ console.log();
2381
+ console.log(import_chalk.default.yellow.bold("OPTIONS"));
2382
+ for (const [colored, plain, d] of options) {
2383
+ console.log(` ${pad(colored, plain, W)}${desc(d)}`);
2384
+ }
2385
+ console.log();
2386
+ console.log(import_chalk.default.yellow.bold("EXAMPLES"));
2387
+ for (const e of examples) {
2388
+ console.log(` ${e}`);
2389
+ }
2390
+ console.log();
2391
+ }
2392
+
2393
+ // src/utils/ui.ts
2394
+ var import_node_path8 = require("path");
2395
+ var import_node_readline = require("readline");
2396
+ var import_chalk2 = __toESM(require("chalk"));
2397
+ var import_ora = __toESM(require("ora"));
2398
+ var INDENT = " ";
2399
+ var autoYes = false;
2400
+ function setAutoYes(value) {
2401
+ autoYes = value;
2402
+ }
2403
+ function task(text) {
2404
+ return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
2405
+ }
2406
+ function header(text) {
2407
+ console.log();
2408
+ console.log(import_chalk2.default.bold.cyan(`\u25A0 ${text}`));
2409
+ }
2410
+ function info(text) {
2411
+ console.log(`${INDENT}${import_chalk2.default.dim("\u2502")} ${import_chalk2.default.dim(text)}`);
2412
+ }
2413
+ function confirm(prompt2) {
2414
+ if (autoYes) return Promise.resolve(true);
2415
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
2416
+ return new Promise((res) => {
2417
+ rl.question(import_chalk2.default.yellow(`${prompt2} [Y/n] `), (answer) => {
2418
+ rl.close();
2419
+ res(answer.toLowerCase() !== "n");
2420
+ });
2421
+ });
2422
+ }
2423
+ function promptInput(prompt2) {
2424
+ if (autoYes) return Promise.resolve("");
2425
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
2426
+ return new Promise((res) => {
2427
+ rl.question(import_chalk2.default.cyan(`${INDENT}${prompt2}`), (answer) => {
2428
+ rl.close();
2429
+ res(answer.trim());
2430
+ });
2431
+ });
2432
+ }
2433
+ var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
2434
+ function formatClickablePath(filePath) {
2435
+ const absolutePath = (0, import_node_path8.resolve)(filePath);
2436
+ if (_isWSL || process.platform !== "win32") {
2437
+ return absolutePath;
2438
+ }
2439
+ let formatted = absolutePath;
2440
+ formatted = formatted.replace(/\//g, "\\");
2441
+ if (formatted.includes(" ") && !formatted.startsWith('"')) {
2442
+ formatted = `"${formatted}"`;
2443
+ }
2444
+ return formatted;
2445
+ }
2446
+ function formatDuration(ms) {
2447
+ const mins = Math.floor(ms / 6e4);
2448
+ const secs = Math.floor(ms % 6e4 / 1e3);
2449
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
2450
+ }
2451
+
2452
+ // src/commands/auth-cmd.ts
2453
+ function waitForEnter(prompt2) {
2454
+ return new Promise((resolve9) => {
2455
+ const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
2456
+ rl.question(prompt2, () => {
2457
+ rl.close();
2458
+ resolve9();
2459
+ });
2460
+ });
2461
+ }
2462
+ async function authCommand(url) {
2463
+ console.log();
2464
+ showLogo();
2465
+ if (!authStatePath(url)) {
2466
+ console.log(import_chalk3.default.red(` Not a valid URL: ${url}`));
2467
+ return;
2468
+ }
2469
+ console.log(import_chalk3.default.white(" A browser will open. Sign in to the site, then come back here."));
2470
+ console.log();
2471
+ const browser = await import_playwright2.chromium.launch({ headless: false });
2472
+ const context = await browser.newContext();
2473
+ const page = await context.newPage();
2474
+ try {
2475
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
2476
+ } catch {
2477
+ }
2478
+ await waitForEnter(import_chalk3.default.cyan(" Press Enter when you are signed in... "));
2479
+ try {
2480
+ const state = await context.storageState();
2481
+ const path = await saveAuthState(url, state);
2482
+ console.log();
2483
+ console.log(import_chalk3.default.green(` \u2713 Session saved for ${new URL(url).host}`));
2484
+ console.log(import_chalk3.default.dim(` ${formatClickablePath(path)}`));
2485
+ console.log(import_chalk3.default.dim(" Crawls and recordings of this site now start signed in."));
2486
+ console.log(import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan(`qp auth clear ${new URL(url).host}`) + import_chalk3.default.dim(" to forget it."));
2487
+ } finally {
2488
+ await browser.close().catch(() => {
2489
+ });
2490
+ }
2491
+ console.log();
2492
+ }
2493
+ async function authClearCommand(url) {
2494
+ console.log();
2495
+ const removed = await clearAuthState(url);
2496
+ if (removed.length === 0) {
2497
+ console.log(import_chalk3.default.dim(url ? ` No saved session for ${url}` : " No saved sessions."));
2498
+ } else {
2499
+ for (const host of removed) {
2500
+ console.log(import_chalk3.default.green(` \u2713 Forgot session for ${host}`));
2501
+ }
2502
+ }
2503
+ console.log();
2504
+ }
2505
+ async function authListCommand() {
2506
+ console.log();
2507
+ const hosts = await listAuthStates();
2508
+ if (hosts.length === 0) {
2509
+ console.log(import_chalk3.default.dim(" No saved sessions. Run ") + import_chalk3.default.cyan("qp auth <url>") + import_chalk3.default.dim(" to add one."));
2510
+ } else {
2511
+ console.log(import_chalk3.default.white(" Saved sessions:"));
2512
+ for (const host of hosts) {
2513
+ console.log(` ${import_chalk3.default.green("\u25CF")} ${host}`);
2514
+ }
2515
+ }
2516
+ console.log();
2517
+ }
2518
+
2519
+ // src/commands/capture-cmd.ts
2520
+ var import_node_path20 = require("path");
2521
+ var import_chalk11 = __toESM(require("chalk"));
2522
+
2523
+ // src/ai/narrate.ts
2524
+ init_lib();
2525
+
2526
+ // src/ai/client.ts
2527
+ var import_node_readline3 = require("readline");
2528
+ var import_chalk5 = __toESM(require("chalk"));
2529
+ init_lib();
2530
+
2531
+ // src/utils/browser-auth.ts
2532
+ var import_node_http = require("http");
2533
+ var import_node_url = require("url");
2534
+ var import_chalk4 = __toESM(require("chalk"));
2535
+
2536
+ // src/lib/browser-open.ts
2537
+ var import_node_child_process3 = require("child_process");
2538
+ function editorBaseUrl() {
2539
+ return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
2540
+ }
2541
+ function openInBrowser(url) {
2542
+ const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
2543
+ const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
2544
+ return new Promise((resolve9) => {
2545
+ (0, import_node_child_process3.exec)(command, () => resolve9());
2546
+ });
2547
+ }
2548
+
2549
+ // src/utils/browser-auth.ts
2550
+ init_SparkStripe_feature_gating();
2551
+ var TIMEOUT_MS = 12e4;
2552
+ async function browserLogin() {
2553
+ return new Promise((resolve9) => {
2554
+ let server;
2555
+ let timer;
2556
+ const cleanup = (email) => {
2557
+ clearTimeout(timer);
2558
+ server?.close();
2559
+ resolve9(email);
2560
+ };
2561
+ server = (0, import_node_http.createServer)((req, res) => {
2562
+ const url = new import_node_url.URL(req.url || "/", `http://localhost`);
2563
+ const path = url.pathname;
2564
+ if (path === "/callback" || path === "/") {
2565
+ const email = url.searchParams.get("email");
2566
+ res.writeHead(200, { "Content-Type": "text/html" });
2567
+ res.end(`
2568
+ <!DOCTYPE html>
2569
+ <html>
2570
+ <head><title>QuickPeek</title><style>
2571
+ body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff; }
2572
+ .card { text-align: center; padding: 2rem; }
2573
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
2574
+ p { color: #888; }
2575
+ </style></head>
2576
+ <body><div class="card">
2577
+ <h1>${email ? "Logged in!" : "Return to your terminal"}</h1>
2578
+ <p>${email ? `Signed in as ${email}` : "Complete setup in your terminal."}</p>
2579
+ <p style="margin-top: 1rem; font-size: 0.875rem;">You can close this tab.</p>
2580
+ </div></body>
2581
+ </html>
2582
+ `);
2583
+ if (email) {
2584
+ cleanup(email);
2585
+ }
2586
+ } else {
2587
+ res.writeHead(404);
2588
+ res.end();
2589
+ }
2590
+ });
2591
+ server.listen(0, "127.0.0.1", () => {
2592
+ const addr = server.address();
2593
+ if (!addr || typeof addr === "string") {
2594
+ cleanup(null);
2595
+ return;
2596
+ }
2597
+ const port = addr.port;
2598
+ const returnUrl = `http://localhost:${port}/callback`;
2599
+ const url = pricingUrl({ returnUrl });
2600
+ console.log();
2601
+ console.log(import_chalk4.default.cyan(" Opening browser to sign in..."));
2602
+ console.log(import_chalk4.default.dim(` ${url}`));
2603
+ console.log();
2604
+ console.log(import_chalk4.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
2605
+ void openInBrowser(url);
2606
+ });
2607
+ timer = setTimeout(() => {
2608
+ console.log();
2609
+ console.log(import_chalk4.default.yellow(" Sign-in timed out."));
2610
+ cleanup(null);
2611
+ }, TIMEOUT_MS);
2612
+ });
2613
+ }
2614
+
2615
+ // src/ai/client.ts
2616
+ var lastRateLimitInfo = null;
2617
+ function getLastRateLimitInfo() {
2618
+ return lastRateLimitInfo;
2619
+ }
2620
+ function prompt(question) {
2621
+ const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
2622
+ return new Promise((resolve9) => {
2623
+ rl.question(question, (answer) => {
2624
+ rl.close();
2625
+ resolve9(answer.trim());
2626
+ });
2627
+ });
2628
+ }
2629
+ async function setupUserEmail() {
2630
+ console.log();
2631
+ console.log(import_chalk5.default.yellow(" \u2709 Account Setup"));
2632
+ console.log(import_chalk5.default.dim(" \u2500".repeat(20)));
2633
+ console.log();
2634
+ console.log(import_chalk5.default.white(" Sign in with your Google account to track usage"));
2635
+ console.log(import_chalk5.default.white(" and unlock Pro features."));
2636
+ const browserEmail = await browserLogin();
2637
+ if (browserEmail) {
2638
+ console.log();
2639
+ console.log(import_chalk5.default.green(` \u2713 Signed in as ${browserEmail}`));
2640
+ console.log();
2641
+ return browserEmail;
2642
+ }
2643
+ console.log();
2644
+ console.log(import_chalk5.default.dim(" Or enter your email manually:"));
2645
+ const email = await prompt(import_chalk5.default.white(" Email: "));
2646
+ if (!email) {
2647
+ console.log();
2648
+ console.log(import_chalk5.default.red(" No email provided. Exiting."));
2649
+ process.exit(1);
2650
+ }
2651
+ if (!email.includes("@") || !email.includes(".")) {
2652
+ console.log();
2653
+ console.log(import_chalk5.default.yellow(" \u26A0 That doesn't look like a valid email address."));
2654
+ const proceed = await prompt(import_chalk5.default.white(" Continue anyway? ") + import_chalk5.default.dim("[y/N] "));
2655
+ if (proceed.toLowerCase() !== "y") {
2656
+ process.exit(1);
2657
+ }
2658
+ }
2659
+ console.log();
2660
+ console.log(import_chalk5.default.green(" \u2713 Email registered"));
2661
+ console.log();
2662
+ return email;
2663
+ }
2664
+ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
2665
+ const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier);
2666
+ if (!result.success) {
2667
+ await logError2("AI API", new Error(result.error));
2668
+ if (result.isRateLimit) {
2669
+ console.log();
2670
+ console.log(import_chalk5.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
2671
+ console.log();
2672
+ console.log(import_chalk5.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
2673
+ console.log(import_chalk5.default.dim(" Options:"));
2674
+ if (result.retryAfterSeconds) {
2675
+ const minutes = Math.ceil(result.retryAfterSeconds / 60);
2676
+ console.log(import_chalk5.default.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
2677
+ } else {
2678
+ console.log(import_chalk5.default.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
2679
+ }
2680
+ console.log(import_chalk5.default.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
2681
+ console.log(import_chalk5.default.dim(" \u2022 Upgrade to a higher tier for more requests"));
2682
+ console.log();
2683
+ process.exit(1);
2684
+ }
2685
+ if (result.isAuthError) {
2686
+ console.log();
2687
+ console.log(import_chalk5.default.red(" \u26A0 Authentication Error"));
2688
+ console.log();
2689
+ console.log(import_chalk5.default.dim(` ${result.error}`));
2690
+ console.log();
2691
+ process.exit(1);
2692
+ }
2693
+ console.log();
2694
+ console.log(import_chalk5.default.red(" \u26A0 AI Error"));
2695
+ console.log();
2696
+ const firstLine = result.error.split("\n")[0] ?? result.error;
2697
+ console.log(import_chalk5.default.dim(` ${firstLine.slice(0, 80)}`));
2698
+ console.log();
2699
+ process.exit(1);
2700
+ }
2701
+ lastRateLimitInfo = result.limits;
2702
+ return result.content;
2703
+ }
2704
+
2705
+ // src/ai/narrate.ts
2706
+ async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
2707
+ const langName = getLanguageName(lang);
2708
+ const stepList = steps.map((s, i) => {
2709
+ const parts = [`${i + 1}. ${s.action}`];
2710
+ if (s.target) parts.push(`on "${s.target}"`);
2711
+ if (s.value) parts.push(`= "${s.value}"`);
2712
+ return parts.join(" ");
2713
+ }).join("\n");
2714
+ const systemPrompt = `You write professional demo video captions in ${langName}.
2715
+ Given UI steps, return a JSON array of captions \u2014 one per step, in order.
2716
+
2205
2717
  RULES:
2206
2718
  - Return ONLY a valid JSON array of strings, no explanation
2207
2719
  - Each caption: 1-2 short conversational sentences, present tense ("We click...", "Here we type...")
@@ -2372,27 +2884,27 @@ function normalizeUrl2(url) {
2372
2884
  }
2373
2885
 
2374
2886
  // src/capture.ts
2375
- var import_node_child_process4 = require("child_process");
2887
+ var import_node_child_process5 = require("child_process");
2376
2888
  var import_node_http3 = require("http");
2377
- var import_node_readline2 = require("readline");
2378
- var import_playwright3 = require("playwright");
2889
+ var import_node_readline4 = require("readline");
2890
+ var import_playwright4 = require("playwright");
2379
2891
 
2380
2892
  // src/browser/wsl.ts
2381
- var import_node_child_process3 = require("child_process");
2382
- var import_node_fs3 = require("fs");
2893
+ var import_node_child_process4 = require("child_process");
2894
+ var import_node_fs5 = require("fs");
2383
2895
  var import_node_http2 = require("http");
2384
- var import_playwright2 = require("playwright");
2896
+ var import_playwright3 = require("playwright");
2385
2897
  function isWSL() {
2386
2898
  if (process.platform !== "linux") return false;
2387
2899
  try {
2388
- const r = (0, import_node_child_process3.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
2900
+ const r = (0, import_node_child_process4.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
2389
2901
  return r.includes("microsoft") || r.includes("wsl");
2390
2902
  } catch {
2391
2903
  return false;
2392
2904
  }
2393
2905
  }
2394
2906
  function tryEnableWSLInterop() {
2395
- return (0, import_node_fs3.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
2907
+ return (0, import_node_fs5.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
2396
2908
  }
2397
2909
  function wslToWindowsPath(wslPath) {
2398
2910
  if (!wslPath) return wslPath;
@@ -2401,9 +2913,9 @@ function wslToWindowsPath(wslPath) {
2401
2913
  return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
2402
2914
  }
2403
2915
  function findExe(whereExeQuery, ...hardcoded) {
2404
- for (const p of hardcoded) if ((0, import_node_fs3.existsSync)(p)) return p;
2916
+ for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
2405
2917
  try {
2406
- const line = (0, import_node_child_process3.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
2918
+ const line = (0, import_node_child_process4.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
2407
2919
  if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
2408
2920
  } catch {
2409
2921
  }
@@ -2417,14 +2929,14 @@ var findWindowsChrome = () => findExe(
2417
2929
  var findWindowsNode = () => findExe("node.exe");
2418
2930
  function getWindowsHostIP() {
2419
2931
  try {
2420
- const m = (0, import_node_child_process3.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
2932
+ const m = (0, import_node_child_process4.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
2421
2933
  /default via (\d+\.\d+\.\d+\.\d+)/
2422
2934
  );
2423
2935
  if (m?.[1]) return m[1];
2424
2936
  } catch {
2425
2937
  }
2426
2938
  try {
2427
- const m = (0, import_node_fs3.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
2939
+ const m = (0, import_node_fs5.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
2428
2940
  if (m?.[1]) return m[1];
2429
2941
  } catch {
2430
2942
  }
@@ -2433,7 +2945,7 @@ function getWindowsHostIP() {
2433
2945
  function startRelay(nodePath, windowsHost, targetPort) {
2434
2946
  const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
2435
2947
  return new Promise((resolve9, reject) => {
2436
- const proc = (0, import_node_child_process3.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
2948
+ const proc = (0, import_node_child_process4.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
2437
2949
  const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
2438
2950
  let buf = "";
2439
2951
  proc.stdout.on("data", (d) => {
@@ -2480,7 +2992,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
2480
2992
  if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
2481
2993
  const winUserData = wslToWindowsPath(userDataDir);
2482
2994
  const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
2483
- const chromeProc = (0, import_node_child_process3.spawn)(
2995
+ const chromeProc = (0, import_node_child_process4.spawn)(
2484
2996
  chromePath,
2485
2997
  [
2486
2998
  `--remote-debugging-port=${debugPort}`,
@@ -2500,7 +3012,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
2500
3012
  const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
2501
3013
  try {
2502
3014
  await waitForChrome(windowsHost, proxyPort);
2503
- const browser = await import_playwright2.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
3015
+ const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
2504
3016
  return {
2505
3017
  browser,
2506
3018
  cleanup: async () => {
@@ -2629,9 +3141,9 @@ var EVENT_TRACKER_SCRIPT = `
2629
3141
  }, { passive: true, capture: true });
2630
3142
  }());
2631
3143
  `;
2632
- function waitForEnter() {
3144
+ function waitForEnter2() {
2633
3145
  return new Promise((resolve9) => {
2634
- const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
3146
+ const rl = (0, import_node_readline4.createInterface)({ input: process.stdin, output: process.stdout });
2635
3147
  process.stdout.write(" Press Enter when done...\n");
2636
3148
  rl.once("line", () => {
2637
3149
  rl.close();
@@ -2660,13 +3172,13 @@ async function launchCaptureChrome() {
2660
3172
  req.end();
2661
3173
  });
2662
3174
  if (alreadyUp) {
2663
- const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${probe.port}`, { timeout: 5e3 });
3175
+ const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${probe.port}`, { timeout: 5e3 });
2664
3176
  return { browser, relay: probe.proc };
2665
3177
  }
2666
3178
  probe.proc.kill();
2667
- const winTemp = (0, import_node_child_process4.execSync)("cmd.exe /c echo %TEMP%", { encoding: "utf-8" }).trim().replace(/\r$/, "");
3179
+ const winTemp = (0, import_node_child_process5.execSync)("cmd.exe /c echo %TEMP%", { encoding: "utf-8" }).trim().replace(/\r$/, "");
2668
3180
  const userDataDir = `${winTemp}\\qp-capture`;
2669
- (0, import_node_child_process4.spawn)(
3181
+ (0, import_node_child_process5.spawn)(
2670
3182
  chromePath,
2671
3183
  [
2672
3184
  `--remote-debugging-port=${debugPort}`,
@@ -2680,7 +3192,7 @@ async function launchCaptureChrome() {
2680
3192
  const relay = await startRelay(nodePath, windowsHost, debugPort);
2681
3193
  try {
2682
3194
  await waitForChrome(windowsHost, relay.port);
2683
- const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${relay.port}`, { timeout: 5e3 });
3195
+ const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${relay.port}`, { timeout: 5e3 });
2684
3196
  return { browser, relay: relay.proc };
2685
3197
  } catch (err) {
2686
3198
  relay.proc.kill();
@@ -2695,7 +3207,7 @@ async function captureUserSession(url, _workDir) {
2695
3207
  if (isWSL()) {
2696
3208
  ({ browser, relay } = await launchCaptureChrome());
2697
3209
  } else {
2698
- browser = await import_playwright3.chromium.launch({
3210
+ browser = await import_playwright4.chromium.launch({
2699
3211
  headless: false,
2700
3212
  args: ["--start-maximized", "--no-first-run"]
2701
3213
  });
@@ -2737,7 +3249,7 @@ async function captureUserSession(url, _workDir) {
2737
3249
  return [title, parts.join(" | ")];
2738
3250
  }).catch(() => ["", ""]);
2739
3251
  startTime = Date.now();
2740
- await waitForEnter();
3252
+ await waitForEnter2();
2741
3253
  const durationMs = Date.now() - startTime;
2742
3254
  if (relay) {
2743
3255
  try {
@@ -2752,195 +3264,31 @@ async function captureUserSession(url, _workDir) {
2752
3264
  return { events, durationMs, pageTitle, pageContext };
2753
3265
  }
2754
3266
 
2755
- // src/utils/config-loader.ts
2756
- var import_node_fs4 = require("fs");
2757
- var import_promises4 = require("fs/promises");
2758
- var import_node_os = require("os");
2759
- var import_node_path6 = require("path");
2760
- init_lib();
2761
- async function loadConfig(_url, langOverride) {
2762
- const configPath = (0, import_node_path6.join)(process.cwd(), CONFIG_FILE);
2763
- if ((0, import_node_fs4.existsSync)(configPath)) {
2764
- try {
2765
- const content = await (0, import_promises4.readFile)(configPath, "utf-8");
2766
- const userConfig = JSON.parse(content);
2767
- const config = {
2768
- lang: langOverride || userConfig.lang || "en",
2769
- plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
2770
- video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
2771
- audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
2772
- music: userConfig.music,
2773
- timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
2774
- output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
2775
- };
2776
- return { config, created: false };
2777
- } catch {
2778
- }
2779
- }
2780
- const fullDefaultConfig = {
2781
- lang: "en",
2782
- plan: { maxSteps: 8 },
2783
- video: {
2784
- width: 1080,
2785
- height: 1920,
2786
- fps: 30,
2787
- quality: 0.5,
2788
- preset: "fast",
2789
- cursor: true,
2790
- highlight: false,
2791
- fadeIn: 500,
2792
- // ms
2793
- fadeOut: 500,
2794
- // ms
2795
- contrast: 0.25
2796
- },
2797
- audio: {
2798
- // Voice is derived from lang setting via LANGUAGE_VOICES mapping
2799
- // To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
2800
- speed: 1
2801
- },
2802
- music: {
2803
- path: "",
2804
- volume: 0.3
2805
- },
2806
- output: { dir: "./demo" }
2807
- };
2808
- await (0, import_promises4.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
2809
- return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
2810
- }
2811
- var WORK_DIR = ".quickpeek";
2812
- async function prepareWorkDir() {
2813
- const workDir = (0, import_node_path6.join)(process.cwd(), WORK_DIR);
2814
- if ((0, import_node_fs4.existsSync)(workDir)) {
2815
- await (0, import_promises4.rm)(workDir, { recursive: true });
2816
- }
2817
- await (0, import_promises4.mkdir)(workDir, { recursive: true });
2818
- return workDir;
2819
- }
2820
- var USER_DATA_DIR = (0, import_node_path6.join)((0, import_node_os.homedir)(), ".quickpeek");
2821
- var USER_DATA_FILE = (0, import_node_path6.join)(USER_DATA_DIR, "user.json");
2822
- async function loadUserData() {
2823
- try {
2824
- if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
2825
- return JSON.parse(await (0, import_promises4.readFile)(USER_DATA_FILE, "utf-8"));
2826
- }
2827
- } catch {
2828
- }
2829
- return {};
2830
- }
2831
- async function saveUserData(userData) {
2832
- if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
2833
- await (0, import_promises4.mkdir)(USER_DATA_DIR, { recursive: true });
2834
- }
2835
- const existing = await loadUserData();
2836
- const merged = { ...existing, ...userData };
2837
- await (0, import_promises4.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
2838
- }
2839
- var DEFAULT_CAPTIONS_CSS = `.caption {
2840
- font-family: Arial, sans-serif;
2841
- -webkit-text-stroke: 3px #000000;
2842
- }
2843
- .caption .regular {
2844
- font-size: 56px;
2845
- color: #FFFFFF;
2846
- }
2847
- .caption .highlight {
2848
- font-size: 64px;
2849
- color: #FFFF00;
2850
- }
2851
- `;
2852
- function parseCaptionCSS(css) {
2853
- const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
2854
- const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
2855
- const regularBlock = css.match(/\.caption\s+\.regular\s*\{([^}]+)\}/)?.[1] || "";
2856
- const highlightBlock = css.match(/\.caption\s+\.highlight\s*\{([^}]+)\}/)?.[1] || "";
2857
- const fontMatch = captionBlock.match(/font-family:\s*"?([^";,]+)/);
2858
- if (fontMatch) style.font = fontMatch[1].trim();
2859
- const strokeMatch = captionBlock.match(/-webkit-text-stroke:\s*(\d+)px\s*(rgba?\([^)]+\)|#[A-Fa-f0-9]{6})/);
2860
- if (strokeMatch) {
2861
- style.outlineWidth = parseInt(strokeMatch[1], 10);
2862
- style.outline = rgbaToHex(strokeMatch[2]);
2863
- }
2864
- const shadowMatch = captionBlock.match(/text-shadow:\s*(\d+)px/);
2865
- if (shadowMatch) style.shadow = parseInt(shadowMatch[1], 10);
2866
- const marginMatch = captionBlock.match(/margin-bottom:\s*(\d+)/);
2867
- if (marginMatch) style.marginBottom = parseInt(marginMatch[1], 10);
2868
- const parseWordStyle = (block, target) => {
2869
- const colorMatch = block.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
2870
- if (colorMatch) target.color = colorMatch[1];
2871
- const sizeMatch = block.match(/font-size:\s*(\d+)/);
2872
- if (sizeMatch) target.size = parseInt(sizeMatch[1], 10);
2873
- const weightMatch = block.match(/font-weight:\s*(bold|normal|\d+)/);
2874
- if (weightMatch) target.bold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
2875
- };
2876
- parseWordStyle(regularBlock, style.regular);
2877
- parseWordStyle(highlightBlock, style.highlight);
2878
- if (!regularBlock && !highlightBlock) {
2879
- const colorMatch = captionBlock.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
2880
- if (colorMatch) style.regular.color = colorMatch[1];
2881
- const sizeMatch = captionBlock.match(/font-size:\s*(\d+)/);
2882
- if (sizeMatch) {
2883
- style.regular.size = parseInt(sizeMatch[1], 10);
2884
- style.highlight.size = Math.round(style.regular.size * 1.15);
2885
- }
2886
- const highlightMatch = captionBlock.match(/-qp-highlight:\s*(#[A-Fa-f0-9]{6})/);
2887
- if (highlightMatch) style.highlight.color = highlightMatch[1];
2888
- const weightMatch = captionBlock.match(/font-weight:\s*(bold|normal|\d+)/);
2889
- if (weightMatch) {
2890
- const isBold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
2891
- style.regular.bold = isBold;
2892
- style.highlight.bold = isBold;
2893
- }
2894
- }
2895
- return style;
2896
- }
2897
- async function loadCaptions() {
2898
- const captionsPath = (0, import_node_path6.join)(process.cwd(), "captions.css");
2899
- if (!(0, import_node_fs4.existsSync)(captionsPath)) {
2900
- await (0, import_promises4.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
2901
- return DEFAULT_CAPTIONS;
2902
- }
2903
- try {
2904
- const css = await (0, import_promises4.readFile)(captionsPath, "utf-8");
2905
- return parseCaptionCSS(css);
2906
- } catch {
2907
- return DEFAULT_CAPTIONS;
2908
- }
2909
- }
2910
- async function savePlan(plan, planPath) {
2911
- const cleanPlan = {
2912
- ...plan,
2913
- steps: plan.steps.map(({ duration, ...step }) => step)
2914
- };
2915
- await (0, import_promises4.mkdir)((0, import_node_path6.dirname)(planPath), { recursive: true });
2916
- await (0, import_promises4.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
2917
- }
2918
-
2919
3267
  // src/utils/logging.ts
2920
- var import_promises5 = require("fs/promises");
2921
- var import_node_path7 = require("path");
3268
+ var import_promises6 = require("fs/promises");
3269
+ var import_node_path9 = require("path");
2922
3270
  var LOG_FILE_NAME = "quickpeek.log";
2923
3271
  var logFileInitialized = false;
2924
3272
  var logFilePath = LOG_FILE_NAME;
2925
3273
  async function writeLogEntry(entry) {
2926
3274
  try {
2927
3275
  if (!logFileInitialized) {
2928
- await (0, import_promises5.writeFile)(logFilePath, entry);
3276
+ await (0, import_promises6.writeFile)(logFilePath, entry);
2929
3277
  logFileInitialized = true;
2930
3278
  } else {
2931
- await (0, import_promises5.appendFile)(logFilePath, entry);
3279
+ await (0, import_promises6.appendFile)(logFilePath, entry);
2932
3280
  }
2933
3281
  } catch {
2934
3282
  }
2935
3283
  }
2936
3284
  async function setLogFilePath(outputDir) {
2937
- logFilePath = (0, import_node_path7.join)(outputDir, LOG_FILE_NAME);
3285
+ logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
2938
3286
  const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
2939
3287
  ${"=".repeat(60)}
2940
3288
 
2941
3289
  `;
2942
3290
  try {
2943
- await (0, import_promises5.writeFile)(logFilePath, header2);
3291
+ await (0, import_promises6.writeFile)(logFilePath, header2);
2944
3292
  logFileInitialized = true;
2945
3293
  } catch {
2946
3294
  }
@@ -2954,115 +3302,56 @@ async function logDebug(context, message) {
2954
3302
  `);
2955
3303
  }
2956
3304
  async function logError(context, error) {
2957
- let errorMsg;
2958
- if (error instanceof Error) {
2959
- errorMsg = `${error.message}
2960
- ${error.stack}`;
2961
- } else if (typeof error === "object" && error !== null) {
2962
- errorMsg = JSON.stringify(error, null, 2);
2963
- } else {
2964
- errorMsg = String(error);
2965
- }
2966
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
2967
- ${errorMsg}
2968
-
2969
- `);
2970
- }
2971
- async function logStep(stepNum, action, details) {
2972
- const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
2973
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
2974
- `);
2975
- }
2976
-
2977
- // src/utils/ui.ts
2978
- var import_node_path8 = require("path");
2979
- var import_node_readline3 = require("readline");
2980
- var import_chalk3 = __toESM(require("chalk"));
2981
- var import_ora = __toESM(require("ora"));
2982
- var INDENT = " ";
2983
- var autoYes = false;
2984
- function setAutoYes(value) {
2985
- autoYes = value;
2986
- }
2987
- function task(text) {
2988
- return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
2989
- }
2990
- function header(text) {
2991
- console.log();
2992
- console.log(import_chalk3.default.bold.cyan(`\u25A0 ${text}`));
2993
- }
2994
- function info(text) {
2995
- console.log(`${INDENT}${import_chalk3.default.dim("\u2502")} ${import_chalk3.default.dim(text)}`);
2996
- }
2997
- function confirm(prompt2) {
2998
- if (autoYes) return Promise.resolve(true);
2999
- const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
3000
- return new Promise((res) => {
3001
- rl.question(import_chalk3.default.yellow(`${prompt2} [Y/n] `), (answer) => {
3002
- rl.close();
3003
- res(answer.toLowerCase() !== "n");
3004
- });
3005
- });
3006
- }
3007
- function promptInput(prompt2) {
3008
- if (autoYes) return Promise.resolve("");
3009
- const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
3010
- return new Promise((res) => {
3011
- rl.question(import_chalk3.default.cyan(`${INDENT}${prompt2}`), (answer) => {
3012
- rl.close();
3013
- res(answer.trim());
3014
- });
3015
- });
3016
- }
3017
- var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
3018
- function formatClickablePath(filePath) {
3019
- const absolutePath = (0, import_node_path8.resolve)(filePath);
3020
- if (_isWSL) {
3021
- return absolutePath;
3022
- }
3023
- let formatted = absolutePath;
3024
- formatted = formatted.replace(/\//g, "\\");
3025
- if (formatted.includes(" ") && !formatted.startsWith('"')) {
3026
- formatted = `"${formatted}"`;
3305
+ let errorMsg;
3306
+ if (error instanceof Error) {
3307
+ errorMsg = `${error.message}
3308
+ ${error.stack}`;
3309
+ } else if (typeof error === "object" && error !== null) {
3310
+ errorMsg = JSON.stringify(error, null, 2);
3311
+ } else {
3312
+ errorMsg = String(error);
3027
3313
  }
3028
- return formatted;
3314
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
3315
+ ${errorMsg}
3316
+
3317
+ `);
3029
3318
  }
3030
- function formatDuration(ms) {
3031
- const mins = Math.floor(ms / 6e4);
3032
- const secs = Math.floor(ms % 6e4 / 1e3);
3033
- return `${mins}:${secs.toString().padStart(2, "0")}`;
3319
+ async function logStep(stepNum, action, details) {
3320
+ const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
3321
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
3322
+ `);
3034
3323
  }
3035
3324
 
3036
3325
  // src/commands/phases/video-phase.ts
3037
- var import_node_fs11 = require("fs");
3038
- var import_promises12 = require("fs/promises");
3039
- var import_node_path17 = require("path");
3326
+ var import_node_fs12 = require("fs");
3327
+ var import_promises13 = require("fs/promises");
3328
+ var import_node_path18 = require("path");
3040
3329
  init_lib();
3041
3330
 
3042
3331
  // src/record.ts
3043
- var import_node_fs9 = require("fs");
3044
- var import_promises9 = require("fs/promises");
3045
- var import_node_path14 = require("path");
3046
- var import_chalk4 = __toESM(require("chalk"));
3332
+ var import_node_fs10 = require("fs");
3333
+ var import_promises10 = require("fs/promises");
3334
+ var import_node_path15 = require("path");
3335
+ var import_chalk6 = __toESM(require("chalk"));
3047
3336
 
3048
3337
  // src/browser/actions.ts
3049
- var import_node_fs6 = require("fs");
3050
- var import_promises6 = require("fs/promises");
3051
- var import_node_path10 = require("path");
3338
+ var import_node_fs7 = require("fs");
3339
+ var import_promises7 = require("fs/promises");
3340
+ var import_node_path11 = require("path");
3052
3341
 
3053
3342
  // src/browser/effects.ts
3054
- var import_node_fs5 = require("fs");
3055
- var import_node_path9 = require("path");
3343
+ var import_node_fs6 = require("fs");
3344
+ var import_node_path10 = require("path");
3056
3345
  init_lib();
3057
3346
  function loadHighlightCSS() {
3058
3347
  const candidates = [
3059
- (0, import_node_path9.join)(__dirname, "highlight.css"),
3348
+ (0, import_node_path10.join)(__dirname, "highlight.css"),
3060
3349
  // dist/ build
3061
- (0, import_node_path9.join)(__dirname, "..", "highlight.css")
3350
+ (0, import_node_path10.join)(__dirname, "..", "highlight.css")
3062
3351
  // src/browser/ → src/
3063
3352
  ];
3064
3353
  for (const p of candidates) {
3065
- if ((0, import_node_fs5.existsSync)(p)) return (0, import_node_fs5.readFileSync)(p, "utf-8");
3354
+ if ((0, import_node_fs6.existsSync)(p)) return (0, import_node_fs6.readFileSync)(p, "utf-8");
3066
3355
  }
3067
3356
  return "";
3068
3357
  }
@@ -3386,7 +3675,14 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3386
3675
  }
3387
3676
  } else {
3388
3677
  const amount = direction === "up" ? -PAGE_SCROLL_PX : PAGE_SCROLL_PX;
3389
- await page.evaluate((y) => window.scrollBy(0, y), amount);
3678
+ const moved = await page.evaluate((y) => {
3679
+ const before = window.scrollY;
3680
+ window.scrollBy(0, y);
3681
+ return window.scrollY - before;
3682
+ }, amount);
3683
+ if (moved === 0) {
3684
+ throw new Error("scroll moved nothing: the page has no scroll headroom at this viewport");
3685
+ }
3390
3686
  }
3391
3687
  break;
3392
3688
  }
@@ -3403,8 +3699,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3403
3699
  const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
3404
3700
  if (!isPlaceholder) {
3405
3701
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
3406
- const filePath = (0, import_node_path10.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path10.resolve)(baseDir, normalizedPath);
3407
- if ((0, import_node_fs6.existsSync)(filePath)) {
3702
+ const filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
3703
+ if ((0, import_node_fs7.existsSync)(filePath)) {
3408
3704
  await fileInput.setInputFiles(filePath);
3409
3705
  await page.waitForTimeout(2e3);
3410
3706
  break;
@@ -3520,8 +3816,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3520
3816
  case "wait":
3521
3817
  break;
3522
3818
  case "image": {
3523
- const imagePath = step.value ? (0, import_node_path10.isAbsolute)(step.value) ? step.value : (0, import_node_path10.resolve)(baseDir, step.value) : null;
3524
- if (imagePath && (0, import_node_fs6.existsSync)(imagePath)) {
3819
+ const imagePath = step.value ? (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value) : null;
3820
+ if (imagePath && (0, import_node_fs7.existsSync)(imagePath)) {
3525
3821
  const imgStyle = step.target === "stretch" ? `width: ${config.video.width}px; height: ${config.video.height}px; object-fit: cover;` : "max-width: 90%; max-height: 90%;";
3526
3822
  if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
3527
3823
  }
@@ -3551,7 +3847,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3551
3847
  await pendingDownload;
3552
3848
  pendingDownload = null;
3553
3849
  }
3554
- if (lastDownloadPath && (0, import_node_fs6.existsSync)(lastDownloadPath)) {
3850
+ if (lastDownloadPath && (0, import_node_fs7.existsSync)(lastDownloadPath)) {
3555
3851
  const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
3556
3852
  if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
3557
3853
  }
@@ -3562,7 +3858,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3562
3858
  async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
3563
3859
  const { width, height } = config.video;
3564
3860
  const ext = filePath.split(".").pop()?.toLowerCase() || "";
3565
- const fileBuffer = await (0, import_promises6.readFile)(filePath);
3861
+ const fileBuffer = await (0, import_promises7.readFile)(filePath);
3566
3862
  const base64 = fileBuffer.toString("base64");
3567
3863
  const mimeTypes = {
3568
3864
  png: "image/png",
@@ -3592,17 +3888,18 @@ async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
3592
3888
  }
3593
3889
 
3594
3890
  // src/browser/launch.ts
3595
- var import_promises7 = require("fs/promises");
3596
- var import_playwright4 = require("playwright");
3597
- async function launchBrowserContext(config, userDataDir, recordVideo) {
3891
+ var import_promises8 = require("fs/promises");
3892
+ var import_playwright5 = require("playwright");
3893
+ async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
3598
3894
  const extensionPath = config.browser?.extension;
3599
3895
  const viewport = { width: config.video.width, height: config.video.height };
3600
3896
  const videoOpts = recordVideo ? { recordVideo } : {};
3897
+ const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
3601
3898
  let context;
3602
3899
  let browser = null;
3603
3900
  let chromeSession = null;
3604
3901
  if (extensionPath) {
3605
- await (0, import_promises7.mkdir)(userDataDir, { recursive: true });
3902
+ await (0, import_promises8.mkdir)(userDataDir, { recursive: true });
3606
3903
  const persistentArgs = [
3607
3904
  `--disable-extensions-except=${extensionPath}`,
3608
3905
  `--load-extension=${extensionPath}`,
@@ -3616,7 +3913,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
3616
3913
  const contexts = chromeSession.browser.contexts();
3617
3914
  context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
3618
3915
  } catch {
3619
- context = await import_playwright4.chromium.launchPersistentContext(userDataDir, {
3916
+ context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
3620
3917
  headless: false,
3621
3918
  args: persistentArgs,
3622
3919
  viewport,
@@ -3625,7 +3922,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
3625
3922
  });
3626
3923
  }
3627
3924
  } else {
3628
- context = await import_playwright4.chromium.launchPersistentContext(userDataDir, {
3925
+ context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
3629
3926
  headless: false,
3630
3927
  args: persistentArgs,
3631
3928
  viewport,
@@ -3634,8 +3931,8 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
3634
3931
  });
3635
3932
  }
3636
3933
  } else {
3637
- browser = await import_playwright4.chromium.launch({ headless: true });
3638
- context = await browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
3934
+ browser = await import_playwright5.chromium.launch({ headless: true });
3935
+ context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
3639
3936
  }
3640
3937
  return { context, browser, chromeSession };
3641
3938
  }
@@ -3652,23 +3949,23 @@ async function cleanupBrowserSession(session) {
3652
3949
  init_lib();
3653
3950
 
3654
3951
  // src/browser/dry-run.ts
3655
- var import_node_path12 = require("path");
3952
+ var import_node_path13 = require("path");
3656
3953
  init_utils();
3657
3954
 
3658
3955
  // src/browser/step-validator.ts
3659
- var import_node_fs7 = require("fs");
3660
- var import_node_path11 = require("path");
3956
+ var import_node_fs8 = require("fs");
3957
+ var import_node_path12 = require("path");
3661
3958
  init_utils();
3662
3959
  function findMediaFile(filename, baseDir) {
3663
- const name = (0, import_node_path11.basename)(filename);
3664
- const searchDirs = [baseDir, (0, import_node_path11.join)(baseDir, "res"), (0, import_node_path11.join)(baseDir, "assets"), (0, import_node_path11.join)(baseDir, "media")];
3960
+ const name = (0, import_node_path12.basename)(filename);
3961
+ const searchDirs = [baseDir, (0, import_node_path12.join)(baseDir, "res"), (0, import_node_path12.join)(baseDir, "assets"), (0, import_node_path12.join)(baseDir, "media")];
3665
3962
  for (const dir of searchDirs) {
3666
- if (!(0, import_node_fs7.existsSync)(dir)) continue;
3963
+ if (!(0, import_node_fs8.existsSync)(dir)) continue;
3667
3964
  try {
3668
- const files = (0, import_node_fs7.readdirSync)(dir);
3669
- if (files.includes(name)) return (0, import_node_path11.join)(dir, name);
3965
+ const files = (0, import_node_fs8.readdirSync)(dir);
3966
+ if (files.includes(name)) return (0, import_node_path12.join)(dir, name);
3670
3967
  const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
3671
- if (match) return (0, import_node_path11.join)(dir, match);
3968
+ if (match) return (0, import_node_path12.join)(dir, match);
3672
3969
  } catch {
3673
3970
  }
3674
3971
  }
@@ -3740,8 +4037,8 @@ function resolveMediaStep(step, baseDir, label, startTime) {
3740
4037
  if (!step.value) {
3741
4038
  return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
3742
4039
  }
3743
- const specifiedPath = (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value);
3744
- if ((0, import_node_fs7.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
4040
+ const specifiedPath = (0, import_node_path12.isAbsolute)(step.value) ? step.value : (0, import_node_path12.resolve)(baseDir, step.value);
4041
+ if ((0, import_node_fs8.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
3745
4042
  const foundPath = findMediaFile(step.value, baseDir);
3746
4043
  if (foundPath) {
3747
4044
  step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -3862,8 +4159,8 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
3862
4159
  const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
3863
4160
  if (!isPlaceholder) {
3864
4161
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
3865
- let filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
3866
- if (!(0, import_node_fs7.existsSync)(filePath)) {
4162
+ let filePath = (0, import_node_path12.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path12.resolve)(baseDir, normalizedPath);
4163
+ if (!(0, import_node_fs8.existsSync)(filePath)) {
3867
4164
  const foundPath = findMediaFile(rawPath, baseDir);
3868
4165
  if (foundPath) {
3869
4166
  const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -3962,8 +4259,13 @@ async function getPageElements(page) {
3962
4259
  });
3963
4260
  }
3964
4261
  async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart, onStepEnd) {
3965
- const userDataDir = (0, import_node_path12.join)(baseDir, ".browser-data");
3966
- const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir);
4262
+ const userDataDir = (0, import_node_path13.join)(baseDir, ".browser-data");
4263
+ const { context, browser, chromeSession } = await launchBrowserContext(
4264
+ config,
4265
+ userDataDir,
4266
+ void 0,
4267
+ findAuthState(plan.url)
4268
+ );
3967
4269
  const page = context.pages()[0] || await context.newPage();
3968
4270
  setupDownloadTracking(page);
3969
4271
  const results = [];
@@ -4022,27 +4324,27 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
4022
4324
  }
4023
4325
 
4024
4326
  // src/browser/meta.ts
4025
- var import_node_fs8 = require("fs");
4026
- var import_promises8 = require("fs/promises");
4027
- var import_node_path13 = require("path");
4327
+ var import_node_fs9 = require("fs");
4328
+ var import_promises9 = require("fs/promises");
4329
+ var import_node_path14 = require("path");
4028
4330
  var RECORDING_FILE = "recording.mp4";
4029
4331
  var RECORDING_META_FILE = "recording.meta.json";
4030
4332
  function getRecordingPath(outputDir) {
4031
- return (0, import_node_path13.join)(outputDir, RECORDING_FILE);
4333
+ return (0, import_node_path14.join)(outputDir, RECORDING_FILE);
4032
4334
  }
4033
4335
  async function loadRecordingMeta(outputDir) {
4034
- const metaPath = (0, import_node_path13.join)(outputDir, RECORDING_META_FILE);
4035
- if (!(0, import_node_fs8.existsSync)(metaPath)) return null;
4336
+ const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
4337
+ if (!(0, import_node_fs9.existsSync)(metaPath)) return null;
4036
4338
  try {
4037
- const content = await (0, import_promises8.readFile)(metaPath, "utf-8");
4339
+ const content = await (0, import_promises9.readFile)(metaPath, "utf-8");
4038
4340
  return JSON.parse(content);
4039
4341
  } catch {
4040
4342
  return null;
4041
4343
  }
4042
4344
  }
4043
4345
  async function saveRecordingMeta(outputDir, meta) {
4044
- const metaPath = (0, import_node_path13.join)(outputDir, RECORDING_META_FILE);
4045
- await (0, import_promises8.writeFile)(metaPath, JSON.stringify(meta, null, 2));
4346
+ const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
4347
+ await (0, import_promises9.writeFile)(metaPath, JSON.stringify(meta, null, 2));
4046
4348
  }
4047
4349
  function canReuseRecording(plan, config, stepDurations, existingMeta) {
4048
4350
  if (!existingMeta) {
@@ -4111,16 +4413,17 @@ function formatStepLabel(step) {
4111
4413
  return `${caption.slice(0, 47)}...`;
4112
4414
  }
4113
4415
  async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
4114
- const videoDir = (0, import_node_path14.join)(workDir, "video");
4115
- await (0, import_promises9.mkdir)(videoDir, { recursive: true });
4416
+ const videoDir = (0, import_node_path15.join)(workDir, "video");
4417
+ await (0, import_promises10.mkdir)(videoDir, { recursive: true });
4116
4418
  const getStepDuration = (stepNum) => {
4117
4419
  return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
4118
4420
  };
4119
4421
  const browserTask = task2("Starting browser recording");
4120
4422
  const recordingStartTime = Date.now();
4121
- const userDataDir = (0, import_node_path14.join)(workDir, "browser-data");
4423
+ const userDataDir = (0, import_node_path15.join)(workDir, "browser-data");
4122
4424
  const recordVideo = { dir: videoDir, size: { width: config.video.width, height: config.video.height } };
4123
- const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo);
4425
+ const authState = findAuthState(plan.url);
4426
+ const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
4124
4427
  const page = context.pages()[0] || await context.newPage();
4125
4428
  setupDownloadTracking(page);
4126
4429
  context.on("page", (p) => setupDownloadTracking(p));
@@ -4218,9 +4521,9 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4218
4521
  actualStepDurations.push(actualDuration);
4219
4522
  }
4220
4523
  if (failures.length > 0) {
4221
- console.log(import_chalk4.default.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
4524
+ console.log(import_chalk6.default.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
4222
4525
  for (const f of failures) {
4223
- console.log(import_chalk4.default.yellow(` - ${f}`));
4526
+ console.log(import_chalk6.default.yellow(` - ${f}`));
4224
4527
  }
4225
4528
  }
4226
4529
  const saveTask = task2("Saving video");
@@ -4229,7 +4532,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4229
4532
  await page.close();
4230
4533
  const videoPath = await video.path();
4231
4534
  await cleanupBrowserSession({ context, browser, chromeSession });
4232
- if (!videoPath || !(0, import_node_fs9.existsSync)(videoPath)) {
4535
+ if (!videoPath || !(0, import_node_fs10.existsSync)(videoPath)) {
4233
4536
  throw new Error("Video file was not created");
4234
4537
  }
4235
4538
  saveTask.succeed("Video saved");
@@ -4237,33 +4540,18 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4237
4540
  }
4238
4541
 
4239
4542
  // src/utils/video-pipeline.ts
4240
- var import_node_fs10 = require("fs");
4241
- var import_promises10 = require("fs/promises");
4242
- var import_node_path15 = require("path");
4543
+ var import_video_kit6 = require("@spark-apps/video-kit");
4544
+ var import_node_fs11 = require("fs");
4545
+ var import_promises11 = require("fs/promises");
4546
+ var import_node_path16 = require("path");
4547
+ var import_chalk7 = __toESM(require("chalk"));
4243
4548
  init_lib();
4244
4549
  var INDENT2 = " ";
4245
- function chunkWords(words, maxWords) {
4246
- const chunks = [];
4247
- for (let i = 0; i < words.length; i += maxWords) {
4248
- chunks.push(words.slice(i, i + maxWords));
4249
- }
4250
- return chunks;
4251
- }
4252
- function formatASSTime(ms) {
4253
- const hours = Math.floor(ms / 36e5);
4254
- const minutes = Math.floor(ms % 36e5 / 6e4);
4255
- const seconds = Math.floor(ms % 6e4 / 1e3);
4256
- const centis = Math.floor(ms % 1e3 / 10);
4257
- return `${hours}:${pad(minutes)}:${pad(seconds)}.${pad(centis)}`;
4258
- }
4259
- function pad(n, len = 2) {
4260
- return n.toString().padStart(len, "0");
4261
- }
4262
4550
  async function generateASS(plan, stepAudio, workDir, config, captions) {
4263
- const regularColor = hexToASS(captions.regular.color);
4264
- const highlightColor = hexToASS(captions.highlight.color);
4265
- const outline = hexToASS(captions.outline);
4266
- const back = hexToASS(captions.outline, 128);
4551
+ const regularColor = (0, import_video_kit.hexToAss)(captions.regular.color);
4552
+ const highlightColor = (0, import_video_kit.hexToAss)(captions.highlight.color);
4553
+ const outline = (0, import_video_kit.hexToAss)(captions.outline);
4554
+ const back = (0, import_video_kit.hexToAss)(captions.outline, 128);
4267
4555
  const highlightScale = Math.round(captions.highlight.size / captions.regular.size * 100);
4268
4556
  let ass = generateASSHeader(
4269
4557
  config.video.width,
@@ -4288,7 +4576,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
4288
4576
  continue;
4289
4577
  }
4290
4578
  const words = audio.words.length > 0 ? audio.words : estimateWordTimings(step.caption || "", audio.duration);
4291
- const chunks = chunkWords(words, 3);
4579
+ const chunks = (0, import_video_kit6.chunkWordsByCount)(words, 3);
4292
4580
  for (const chunk of chunks) {
4293
4581
  if (chunk.length === 0) continue;
4294
4582
  const chunkStart = currentTime + chunk[0].start;
@@ -4299,13 +4587,13 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
4299
4587
  const duration = Math.round((word.end - word.start) / 10);
4300
4588
  karaokeText += `{\\kf${duration}\\fscx${highlightScale}\\fscy${highlightScale}}${word.word}{\\fscx100\\fscy100} `;
4301
4589
  }
4302
- ass += `Dialogue: 0,${formatASSTime(chunkStart)},${formatASSTime(chunkEnd)},Karaoke,,0,0,0,,${karaokeText.trim()}
4590
+ ass += `Dialogue: 0,${(0, import_video_kit6.assTimeFromMs)(chunkStart)},${(0, import_video_kit6.assTimeFromMs)(chunkEnd)},Karaoke,,0,0,0,,${karaokeText.trim()}
4303
4591
  `;
4304
4592
  }
4305
4593
  currentTime += audio.duration;
4306
4594
  }
4307
- const assPath = (0, import_node_path15.join)(workDir, "subtitles.ass");
4308
- await (0, import_promises10.writeFile)(assPath, ass);
4595
+ const assPath = (0, import_node_path16.join)(workDir, "subtitles.ass");
4596
+ await (0, import_promises11.writeFile)(assPath, ass);
4309
4597
  return assPath;
4310
4598
  }
4311
4599
  function buildVideoOverlays(steps, stepDurations, baseDir) {
@@ -4318,250 +4606,120 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
4318
4606
  if ((step.action === "video" || step.action === "media") && step.value) {
4319
4607
  const ext = step.value.split(".").pop()?.toLowerCase() || "";
4320
4608
  if (["mp4", "webm", "mov"].includes(ext)) {
4321
- const mediaPath = (0, import_node_path15.isAbsolute)(step.value) ? step.value : (0, import_node_path15.resolve)(baseDir, step.value);
4322
- if ((0, import_node_fs10.existsSync)(mediaPath)) {
4609
+ const mediaPath = (0, import_node_path16.isAbsolute)(step.value) ? step.value : (0, import_node_path16.resolve)(baseDir, step.value);
4610
+ if ((0, import_node_fs11.existsSync)(mediaPath)) {
4323
4611
  const partial = parsePartial(step.partial);
4324
4612
  overlays.push({
4325
4613
  path: mediaPath,
4326
4614
  startTime: cumulativeTime,
4327
4615
  duration: durationSec,
4328
4616
  mode: step.target === "stretch" ? "stretch" : "center",
4329
- ...partial.trimStart !== void 0 ? { trimStart: partial.trimStart } : {},
4330
- ...partial.trimEnd !== void 0 ? { trimEnd: partial.trimEnd } : {}
4331
- });
4332
- }
4333
- }
4334
- }
4335
- cumulativeTime += durationSec;
4336
- }
4337
- return overlays;
4338
- }
4339
- async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
4340
- header("Compose");
4341
- const configPath = (0, import_node_path15.join)(process.cwd(), CONFIG_FILE);
4342
- console.log(`${INDENT2}${require("chalk").dim(`Config: ${formatClickablePath(configPath)}`)}`);
4343
- const proceed = await confirm(`${INDENT2}Review config or continue?`);
4344
- if (!proceed) {
4345
- console.log(require("chalk").yellow(`
4346
- Edit ${CONFIG_FILE} and run again.
4347
- `));
4348
- process.exit(0);
4349
- }
4350
- const composeTask = task("Building final video");
4351
- await (0, import_promises10.mkdir)(config.output.dir, { recursive: true });
4352
- const suffix = config.lang ? `-${config.lang}` : "";
4353
- const finalPath = (0, import_node_path15.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
4354
- const videoDuration = await getMediaDuration(videoPath) / 1e3;
4355
- const hasVoiceover = audioPath && (0, import_node_fs10.existsSync)(audioPath);
4356
- const musicPath = config.music?.path ? (0, import_node_path15.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path15.resolve)(config.output.dir, config.music.path) : "";
4357
- const hasMusic = musicPath && (0, import_node_fs10.existsSync)(musicPath);
4358
- try {
4359
- await composeVideo({
4360
- videoPath,
4361
- assPath,
4362
- outputPath: finalPath,
4363
- voiceoverPath: hasVoiceover ? audioPath : void 0,
4364
- musicPath: hasMusic ? musicPath : void 0,
4365
- musicVolume: config.music?.volume,
4366
- musicFadeIn: config.video.fadeIn / 1e3,
4367
- musicFadeOut: config.video.fadeOut / 1e3,
4368
- width: config.video.width,
4369
- height: config.video.height,
4370
- fps: config.video.fps,
4371
- preset: config.video.preset,
4372
- quality: config.video.quality,
4373
- fadeIn: config.video.fadeIn / 1e3,
4374
- fadeOut: config.video.fadeOut / 1e3,
4375
- videoDuration,
4376
- audioBitrate: config.audio?.bitrate,
4377
- contrast: config.video.contrast,
4378
- trimStart,
4379
- userTier,
4380
- mainContentDuration,
4381
- stepTransitions
4382
- });
4383
- composeTask.succeed("Video composed");
4384
- return finalPath;
4385
- } catch (err) {
4386
- await logError("Video Compose", err);
4387
- composeTask.fail(err.message);
4388
- throw err;
4389
- }
4390
- }
4391
- async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
4392
- const duration = await getMediaDuration(finalPath);
4393
- if (duration <= limitSecs * 1e3) return finalPath;
4394
- const chalk19 = require("chalk");
4395
- const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
4396
- console.log();
4397
- console.log(chalk19.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
4398
- const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
4399
- if (!speedUp) return finalPath;
4400
- const speedTask = task(`Speeding up ${speedFactor}x`);
4401
- const outputPath = finalPath.replace(".mp4", "-fast.mp4");
4402
- try {
4403
- await speedUpVideo({
4404
- inputPath: finalPath,
4405
- outputPath,
4406
- factor: speedFactor,
4407
- preset: config.video.preset
4408
- });
4409
- speedTask.succeed(`Saved: ${formatClickablePath(outputPath)}`);
4410
- return outputPath;
4411
- } catch (err) {
4412
- await logError("Video SpeedUp", err);
4413
- speedTask.fail(err.message);
4414
- throw err;
4415
- }
4416
- }
4417
-
4418
- // src/commands/utils/record-helpers.ts
4419
- var import_chalk7 = __toESM(require("chalk"));
4420
- init_lib();
4421
-
4422
- // src/help.ts
4423
- var import_chalk5 = __toESM(require("chalk"));
4424
- init_lib();
4425
-
4426
- // src/logo.ts
4427
- var import_figlet = __toESM(require("figlet"));
4428
- var import_gradient_string = __toESM(require("gradient-string"));
4429
- var DEFAULT_LOGO_CONFIG = {
4430
- text: "QuickPeek",
4431
- font: "Slant",
4432
- horizontalLayout: "default",
4433
- gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
4434
- suffix: " \u{1F440}"
4435
- };
4436
- function renderLogo(config = {}) {
4437
- const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
4438
- const ascii = import_figlet.default.textSync(resolved.text, {
4439
- font: resolved.font,
4440
- horizontalLayout: resolved.horizontalLayout
4441
- });
4442
- const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
4443
- return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
4444
- }
4445
-
4446
- // src/help.ts
4447
- var cmd = import_chalk5.default.cyan;
4448
- var arg = import_chalk5.default.green;
4449
- var opt = import_chalk5.default.magenta;
4450
- var desc = import_chalk5.default.white;
4451
- var ex = import_chalk5.default.dim;
4452
- function pad2(colored, visible, width) {
4453
- return colored + " ".repeat(Math.max(0, width - visible.length));
4454
- }
4455
- function showLogo() {
4456
- const logo = renderLogo();
4457
- console.log(logo);
4458
- console.log(import_chalk5.default.dim(` v${VERSION} - Auto-generate demo videos with AI`));
4459
- console.log(import_chalk5.default.italic.dim(" Because Tutorial Videos are Overrated!\n"));
4460
- }
4461
- function formatUsageLines(status, tier) {
4462
- const lines = [];
4463
- lines.push(`Videos generated: ${status.usage_count}`);
4464
- if (tier !== "pro" && status.trial) {
4465
- const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
4466
- const used = max_uses - uses_remaining;
4467
- lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
4468
- if (!is_expired && days_remaining > 0) {
4469
- lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
4470
- }
4471
- }
4472
- if (tier === "pro" && status.subscription) {
4473
- const end = status.subscription.current_period_end;
4474
- if (status.subscription.is_lifetime) {
4475
- lines.push("Lifetime access");
4476
- } else if (isCanceling(status)) {
4477
- const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
4478
- lines.push(`Cancels: ${date}`);
4479
- } else if (end) {
4480
- const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
4481
- lines.push(`Renews: ${date}`);
4482
- }
4483
- }
4484
- return lines;
4485
- }
4486
- async function showAccount() {
4487
- const userData = await loadUserData();
4488
- if (!userData.email) {
4489
- console.log(import_chalk5.default.dim(" Not signed in. Run ") + cmd("qp login") + import_chalk5.default.dim(" to sign in."));
4490
- console.log();
4491
- return;
4492
- }
4493
- const email = userData.email;
4494
- const tier = userData.tier === "pro" ? "pro" : "free";
4495
- const tierLabel = tier === "pro" ? import_chalk5.default.green("Pro") : "Free";
4496
- console.log(` ${tierLabel} plan ${import_chalk5.default.dim("\xB7")} ${import_chalk5.default.dim(email)}`);
4497
- try {
4498
- const status = await getStatusCached(email);
4499
- for (const line of formatUsageLines(status, tier)) {
4500
- console.log(import_chalk5.default.dim(` ${line}`));
4501
- }
4502
- } catch {
4503
- }
4504
- console.log();
4505
- }
4506
- async function showHelp() {
4507
- showLogo();
4508
- await showAccount();
4509
- const W = 28;
4510
- const usage = [
4511
- [`${cmd("qp")} ${arg("<url>")}`, `qp <url>`, "AI-generated demo video"],
4512
- [`${cmd("qp")} ${arg("<url>")} ${opt("--script")} ${arg("<file>")}`, `qp <url> --script <file>`, "Use your own narration script"],
4513
- [`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
4514
- [`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
4515
- [`${cmd("qp logout")}`, `qp logout`, "Sign out"],
4516
- [`${cmd("qp init")}`, `qp init`, "Create config file"],
4517
- [`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
4518
- ];
4519
- const options = [
4520
- [`${opt("lang=")}${arg("xx")}`, `lang=xx`, "Voice language (en, es, ar, zh, fr, de, ja...)"],
4521
- [`${opt("-d")}, ${opt("--description")} ${arg('"..."')}`, `-d, --description "..."`, "Guide AI with an app description"],
4522
- [`${opt("-s")}, ${opt("--script")} ${arg("<file>")}`, `-s, --script <file>`, "Narration script (one caption per line)"],
4523
- [`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
4524
- [`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
4525
- [`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"]
4526
- ];
4527
- const examples = [
4528
- `${ex("$")} ${cmd("qp")} ${arg("localhost:3000")}`,
4529
- `${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("lang=")}${arg("es")} ${opt("-d")} ${arg('"E-commerce checkout flow"')}`,
4530
- `${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("--script")} ${arg("narration.txt")}`,
4531
- `${ex("$")} ${cmd("qp capture")} ${arg("localhost:3000")}`
4532
- ];
4533
- console.log();
4534
- console.log(import_chalk5.default.yellow.bold("USAGE"));
4535
- for (const [colored, plain, d] of usage) {
4536
- console.log(` ${pad2(colored, plain, W)}${desc(d)}`);
4617
+ ...partial.trimStart !== void 0 ? { trimStart: partial.trimStart } : {},
4618
+ ...partial.trimEnd !== void 0 ? { trimEnd: partial.trimEnd } : {}
4619
+ });
4620
+ }
4621
+ }
4622
+ }
4623
+ cumulativeTime += durationSec;
4537
4624
  }
4538
- console.log();
4539
- console.log(import_chalk5.default.yellow.bold("OPTIONS"));
4540
- for (const [colored, plain, d] of options) {
4541
- console.log(` ${pad2(colored, plain, W)}${desc(d)}`);
4625
+ return overlays;
4626
+ }
4627
+ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
4628
+ header("Compose");
4629
+ const configPath = (0, import_node_path16.join)(process.cwd(), CONFIG_FILE);
4630
+ console.log(`${INDENT2}${import_chalk7.default.dim(`Config: ${formatClickablePath(configPath)}`)}`);
4631
+ const proceed = await confirm(`${INDENT2}Review config or continue?`);
4632
+ if (!proceed) {
4633
+ console.log(import_chalk7.default.yellow(`
4634
+ Edit ${CONFIG_FILE} and run again.
4635
+ `));
4636
+ process.exit(0);
4542
4637
  }
4543
- console.log();
4544
- console.log(import_chalk5.default.yellow.bold("EXAMPLES"));
4545
- for (const e of examples) {
4546
- console.log(` ${e}`);
4638
+ const composeTask = task("Building final video");
4639
+ await (0, import_promises11.mkdir)(config.output.dir, { recursive: true });
4640
+ const suffix = config.lang ? `-${config.lang}` : "";
4641
+ const finalPath = (0, import_node_path16.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
4642
+ const videoDuration = await getMediaDuration(videoPath) / 1e3;
4643
+ const hasVoiceover = audioPath && (0, import_node_fs11.existsSync)(audioPath);
4644
+ const musicPath = config.music?.path ? (0, import_node_path16.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path16.resolve)(config.output.dir, config.music.path) : "";
4645
+ const hasMusic = musicPath && (0, import_node_fs11.existsSync)(musicPath);
4646
+ try {
4647
+ await composeVideo({
4648
+ videoPath,
4649
+ assPath,
4650
+ outputPath: finalPath,
4651
+ voiceoverPath: hasVoiceover ? audioPath : void 0,
4652
+ musicPath: hasMusic ? musicPath : void 0,
4653
+ musicVolume: config.music?.volume,
4654
+ musicFadeIn: config.video.fadeIn / 1e3,
4655
+ musicFadeOut: config.video.fadeOut / 1e3,
4656
+ width: config.video.width,
4657
+ height: config.video.height,
4658
+ fps: config.video.fps,
4659
+ preset: config.video.preset,
4660
+ quality: config.video.quality,
4661
+ fadeIn: config.video.fadeIn / 1e3,
4662
+ fadeOut: config.video.fadeOut / 1e3,
4663
+ videoDuration,
4664
+ audioBitrate: config.audio?.bitrate,
4665
+ contrast: config.video.contrast,
4666
+ trimStart,
4667
+ userTier,
4668
+ mainContentDuration,
4669
+ stepTransitions
4670
+ });
4671
+ composeTask.succeed("Video composed");
4672
+ return finalPath;
4673
+ } catch (err) {
4674
+ await logError("Video Compose", err);
4675
+ composeTask.fail(err.message);
4676
+ throw err;
4547
4677
  }
4678
+ }
4679
+ async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
4680
+ const duration = await getMediaDuration(finalPath);
4681
+ if (duration <= limitSecs * 1e3) return finalPath;
4682
+ const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
4548
4683
  console.log();
4684
+ console.log(import_chalk7.default.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
4685
+ const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
4686
+ if (!speedUp) return finalPath;
4687
+ const speedTask = task(`Speeding up ${speedFactor}x`);
4688
+ const outputPath = finalPath.replace(".mp4", "-fast.mp4");
4689
+ try {
4690
+ await speedUpVideo({
4691
+ inputPath: finalPath,
4692
+ outputPath,
4693
+ factor: speedFactor,
4694
+ preset: config.video.preset
4695
+ });
4696
+ speedTask.succeed(`Saved: ${formatClickablePath(outputPath)}`);
4697
+ return outputPath;
4698
+ } catch (err) {
4699
+ await logError("Video SpeedUp", err);
4700
+ speedTask.fail(err.message);
4701
+ throw err;
4702
+ }
4549
4703
  }
4550
4704
 
4705
+ // src/commands/utils/record-helpers.ts
4706
+ var import_chalk9 = __toESM(require("chalk"));
4707
+ init_lib();
4708
+
4551
4709
  // src/plan.ts
4552
- var import_promises11 = require("fs/promises");
4553
- var import_node_path16 = require("path");
4554
- var import_chalk6 = __toESM(require("chalk"));
4555
- var import_playwright5 = require("playwright");
4710
+ var import_promises12 = require("fs/promises");
4711
+ var import_node_path17 = require("path");
4712
+ var import_chalk8 = __toESM(require("chalk"));
4713
+ var import_playwright6 = require("playwright");
4556
4714
  init_lib();
4557
4715
  async function crawlExtension(url, extensionPath, workDir, task2) {
4558
4716
  const browserTask = task2("Launching browser with extension");
4559
- const userDataDir = (0, import_node_path16.join)(workDir, ".browser-data");
4560
- await (0, import_promises11.mkdir)(userDataDir, { recursive: true });
4717
+ const userDataDir = (0, import_node_path17.join)(workDir, ".browser-data");
4718
+ await (0, import_promises12.mkdir)(userDataDir, { recursive: true });
4561
4719
  let context;
4562
4720
  let chromeSession = null;
4563
4721
  const useWindowsChrome = isWSL() && tryEnableWSLInterop();
4564
- const launchPersistent = () => import_playwright5.chromium.launchPersistentContext(userDataDir, {
4722
+ const launchPersistent = () => import_playwright6.chromium.launchPersistentContext(userDataDir, {
4565
4723
  headless: false,
4566
4724
  args: [
4567
4725
  `--disable-extensions-except=${extensionPath}`,
@@ -4577,7 +4735,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
4577
4735
  const contexts = chromeSession.browser.contexts();
4578
4736
  context = contexts[0] || await chromeSession.browser.newContext({ viewport: { width: 400, height: 600 } });
4579
4737
  } catch (_err) {
4580
- console.log(import_chalk6.default.yellow(" \u26A0 Windows Chrome unavailable, using Chromium"));
4738
+ console.log(import_chalk8.default.yellow(" \u26A0 Windows Chrome unavailable, using Chromium"));
4581
4739
  context = await launchPersistent();
4582
4740
  }
4583
4741
  } else {
@@ -4654,17 +4812,43 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
4654
4812
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
4655
4813
  return { title, elements };
4656
4814
  }
4657
- async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free") {
4658
- const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2);
4815
+ var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
4816
+ var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
4817
+ The JSON should have: url, title, description, steps array.
4818
+ Each step has: id (number), action, caption, and optionally target, value.
4819
+
4820
+ Malformed content:
4821
+ ${content}`;
4822
+ async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
4823
+ try {
4824
+ return JSON.parse(content);
4825
+ } catch {
4826
+ const fixTask = task2("Fixing malformed JSON");
4827
+ try {
4828
+ const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
4829
+ const jsonMatch = fixed.match(/\{[\s\S]*\}/);
4830
+ if (!jsonMatch) throw new Error("No JSON in fix");
4831
+ const plan = JSON.parse(jsonMatch[0]);
4832
+ fixTask.succeed("JSON fixed by AI");
4833
+ return plan;
4834
+ } catch (err) {
4835
+ await logError2("JSON Fix", err);
4836
+ fixTask.fail("Could not fix JSON");
4837
+ throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
4838
+ }
4839
+ }
4840
+ }
4841
+ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
4842
+ const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
4659
4843
  const aiTask = task2("Generating plan with AI");
4660
4844
  const langName = getLanguageName(lang);
4661
4845
  const systemPrompt = buildSystemPrompt(maxSteps);
4662
- const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps);
4846
+ const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
4663
4847
  try {
4664
4848
  const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
4665
4849
  const jsonMatch = content.match(/\{[\s\S]*\}/);
4666
4850
  if (!jsonMatch) throw new Error("No JSON found in response");
4667
- const plan = JSON.parse(jsonMatch[0]);
4851
+ const plan = await parsePlanJson(jsonMatch[0], task2, logError2, userEmail, tier);
4668
4852
  if (lang !== void 0) plan.lang = lang;
4669
4853
  plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
4670
4854
  ...s,
@@ -4681,41 +4865,14 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
4681
4865
  }
4682
4866
  }
4683
4867
  async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier = "free") {
4684
- const content = await (0, import_promises11.readFile)(planPath, "utf-8");
4685
- let plan;
4686
- try {
4687
- plan = JSON.parse(content);
4688
- } catch (_parseError) {
4689
- const fixTask = task2("Fixing malformed JSON");
4690
- try {
4691
- const fixed = await callGroq(
4692
- "You fix malformed JSON. Return ONLY valid JSON, no explanation.",
4693
- `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
4694
- The JSON should have: url, title, description, steps array.
4695
- Each step has: id (number), action, caption, and optionally target, value.
4696
-
4697
- Malformed content:
4698
- ${content}`,
4699
- logError2,
4700
- userEmail,
4701
- tier
4702
- );
4703
- const jsonMatch = fixed.match(/\{[\s\S]*\}/);
4704
- if (!jsonMatch) throw new Error("No JSON in fix");
4705
- plan = JSON.parse(jsonMatch[0]);
4706
- fixTask.succeed("JSON fixed by AI");
4707
- } catch (err) {
4708
- await logError2("JSON Fix", err);
4709
- fixTask.fail("Could not fix JSON");
4710
- throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
4711
- }
4712
- }
4868
+ const content = await (0, import_promises12.readFile)(planPath, "utf-8");
4869
+ const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
4713
4870
  if (!plan.url || !plan.steps || !Array.isArray(plan.steps)) {
4714
4871
  throw new Error("Invalid plan: missing url or steps");
4715
4872
  }
4716
4873
  const renumberTask = task2("Validating plan structure");
4717
4874
  const originalCount = plan.steps.length;
4718
- const INVALID_TARGETS = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
4875
+ const INVALID_TARGETS2 = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
4719
4876
  const isValidStep = (s) => {
4720
4877
  if (!s?.action) return false;
4721
4878
  if (s.action === "navigate") {
@@ -4724,7 +4881,7 @@ ${content}`,
4724
4881
  }
4725
4882
  if (["click", "type", "select", "check", "upload", "drag"].includes(s.action)) {
4726
4883
  if (!s.target) return false;
4727
- if (INVALID_TARGETS.some((pattern) => pattern.test(s.target))) return false;
4884
+ if (INVALID_TARGETS2.some((pattern) => pattern.test(s.target))) return false;
4728
4885
  }
4729
4886
  if (s.action === "upload") {
4730
4887
  if (!s.target?.includes("file") && !s.target?.includes("input")) return false;
@@ -4741,7 +4898,7 @@ ${content}`,
4741
4898
  renumberTask.fail("No valid steps");
4742
4899
  throw new Error("Plan has no valid steps");
4743
4900
  }
4744
- await (0, import_promises11.writeFile)(planPath, JSON.stringify(plan, null, 2));
4901
+ await (0, import_promises12.writeFile)(planPath, JSON.stringify(plan, null, 2));
4745
4902
  const removed = originalCount - plan.steps.length;
4746
4903
  renumberTask.succeed(
4747
4904
  removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
@@ -4773,41 +4930,41 @@ function displayRateLimitInfo() {
4773
4930
  const percentage = Math.round(limits.used / limits.limit * 100);
4774
4931
  if (percentage >= 90) {
4775
4932
  console.log();
4776
- console.log(import_chalk7.default.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
4933
+ console.log(import_chalk9.default.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
4777
4934
  if (limits.tier === "free") {
4778
- console.log(import_chalk7.default.dim(` Upgrade: ${getUpgradeUrl()}`));
4935
+ console.log(import_chalk9.default.dim(` Upgrade: ${getUpgradeUrl()}`));
4779
4936
  }
4780
4937
  } else if (percentage >= 50) {
4781
- console.log(import_chalk7.default.dim(` ${remaining}/${limits.limit} requests remaining`));
4938
+ console.log(import_chalk9.default.dim(` ${remaining}/${limits.limit} requests remaining`));
4782
4939
  }
4783
4940
  }
4784
4941
  async function displayUsageStatus(userEmail, userTier, _localVideoCount = 0) {
4785
4942
  const tier = userTier === "pro" ? "pro" : "free";
4786
- const tierLabel = tier === "pro" ? import_chalk7.default.green("Pro") : "Free";
4787
- console.log(import_chalk7.default.cyan(` \u2714 ${tierLabel} plan`));
4788
- console.log(import_chalk7.default.dim(` ${userEmail}`));
4943
+ const tierLabel = tier === "pro" ? import_chalk9.default.green("Pro") : "Free";
4944
+ console.log(import_chalk9.default.cyan(` \u2714 ${tierLabel} plan`));
4945
+ console.log(import_chalk9.default.dim(` ${userEmail}`));
4789
4946
  if (userEmail && userEmail !== "anonymous") {
4790
4947
  try {
4791
4948
  const status = await getStatusCached(userEmail);
4792
4949
  for (const line of formatUsageLines(status, tier)) {
4793
- console.log(import_chalk7.default.dim(` ${line}`));
4950
+ console.log(import_chalk9.default.dim(` ${line}`));
4794
4951
  }
4795
4952
  } catch {
4796
4953
  }
4797
4954
  }
4798
4955
  if (userTier === "free") {
4799
4956
  const upgradeUrl = getUpgradeUrl(userEmail !== "anonymous" ? userEmail : void 0);
4800
- console.log(import_chalk7.default.dim(` Upgrade: ${upgradeUrl}`));
4957
+ console.log(import_chalk9.default.dim(` Upgrade: ${upgradeUrl}`));
4801
4958
  }
4802
4959
  }
4803
4960
  function displayPlanSummary(plan) {
4804
4961
  console.log();
4805
- console.log(import_chalk7.default.dim(` ${plan.title || "Untitled"}`));
4806
- console.log(import_chalk7.default.dim(` ${plan.steps?.length || 0} steps`));
4962
+ console.log(import_chalk9.default.dim(` ${plan.title || "Untitled"}`));
4963
+ console.log(import_chalk9.default.dim(` ${plan.steps?.length || 0} steps`));
4807
4964
  for (const step of plan.steps || []) {
4808
4965
  const text = step.caption || step.action || "";
4809
4966
  const caption = text.length > 50 ? `${text.slice(0, 47)}...` : text;
4810
- console.log(import_chalk7.default.dim(` ${step.id}. ${caption}`));
4967
+ console.log(import_chalk9.default.dim(` ${step.id}. ${caption}`));
4811
4968
  }
4812
4969
  }
4813
4970
  function getOutputDirFromUrl(url) {
@@ -4873,8 +5030,8 @@ async function runVideoPhase(params) {
4873
5030
  if (trimStart2 !== void 0 && trimEnd !== void 0) {
4874
5031
  mediaDuration = (trimEnd - trimStart2) * 1e3;
4875
5032
  } else {
4876
- const mediaPath = (0, import_node_path17.resolve)(s.value);
4877
- if ((0, import_node_fs11.existsSync)(mediaPath)) {
5033
+ const mediaPath = (0, import_node_path18.resolve)(s.value);
5034
+ if ((0, import_node_fs12.existsSync)(mediaPath)) {
4878
5035
  try {
4879
5036
  const fullDuration = await getMediaDuration(mediaPath);
4880
5037
  const start = (trimStart2 ?? 0) * 1e3;
@@ -4885,8 +5042,8 @@ async function runVideoPhase(params) {
4885
5042
  }
4886
5043
  }
4887
5044
  } else {
4888
- const mediaPath = (0, import_node_path17.resolve)(s.value);
4889
- if ((0, import_node_fs11.existsSync)(mediaPath)) {
5045
+ const mediaPath = (0, import_node_path18.resolve)(s.value);
5046
+ if ((0, import_node_fs12.existsSync)(mediaPath)) {
4890
5047
  try {
4891
5048
  mediaDuration = await getMediaDuration(mediaPath);
4892
5049
  } catch {
@@ -4899,7 +5056,7 @@ async function runVideoPhase(params) {
4899
5056
  })
4900
5057
  );
4901
5058
  const recordingPath = getRecordingPath(config.output.dir);
4902
- const hasExistingRecording = existingRecordingMeta && (0, import_node_fs11.existsSync)(recordingPath);
5059
+ const hasExistingRecording = existingRecordingMeta && (0, import_node_fs12.existsSync)(recordingPath);
4903
5060
  let videoPath = recordingPath;
4904
5061
  const firstStep = plan.steps[0];
4905
5062
  const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
@@ -4908,7 +5065,7 @@ async function runVideoPhase(params) {
4908
5065
  header("Record");
4909
5066
  const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
4910
5067
  stepAudio = await alignVoiceoverToRecording(stepAudio, result.actualStepDurations, workDir, config.output.dir);
4911
- await (0, import_promises12.copyFile)(result.videoPath, recordingPath);
5068
+ await (0, import_promises13.copyFile)(result.videoPath, recordingPath);
4912
5069
  const meta = buildRecordingMeta(plan, config, result.actualStepDurations);
4913
5070
  meta.pageLoadTime = result.pageLoadTime;
4914
5071
  await saveRecordingMeta(config.output.dir, meta);
@@ -4939,7 +5096,7 @@ async function runVideoPhase(params) {
4939
5096
  if (videoOverlays.length > 0) {
4940
5097
  const spliceTask = task("Splicing videos into recording");
4941
5098
  try {
4942
- const splicedPath = (0, import_node_path17.join)(workDir, "spliced.mp4");
5099
+ const splicedPath = (0, import_node_path18.join)(workDir, "spliced.mp4");
4943
5100
  await spliceVideos({
4944
5101
  inputPath: videoPath,
4945
5102
  outputPath: splicedPath,
@@ -4959,7 +5116,7 @@ async function runVideoPhase(params) {
4959
5116
  if (stepTransitions.length > 0) {
4960
5117
  const transitionTask = task("Applying transitions");
4961
5118
  try {
4962
- const transitionedPath = (0, import_node_path17.join)(workDir, "transitioned.mp4");
5119
+ const transitionedPath = (0, import_node_path18.join)(workDir, "transitioned.mp4");
4963
5120
  const videoDuration = alignedStepDurations.reduce((a, b) => a + b, 0) / 1e3;
4964
5121
  await applyTransitions({
4965
5122
  inputVideo: videoPath,
@@ -4980,15 +5137,15 @@ async function runVideoPhase(params) {
4980
5137
  let mainContentDuration;
4981
5138
  if (userTier === "free") {
4982
5139
  const scriptDir = getScriptDir();
4983
- let outroPath = (0, import_node_path17.join)(scriptDir, "assets", "qp.wm");
4984
- if (!(0, import_node_fs11.existsSync)(outroPath)) {
4985
- outroPath = (0, import_node_path17.join)(scriptDir, "..", "dist", "assets", "qp.wm");
5140
+ let outroPath = (0, import_node_path18.join)(scriptDir, "assets", "qp.wm");
5141
+ if (!(0, import_node_fs12.existsSync)(outroPath)) {
5142
+ outroPath = (0, import_node_path18.join)(scriptDir, "..", "dist", "assets", "qp.wm");
4986
5143
  }
4987
- if ((0, import_node_fs11.existsSync)(outroPath)) {
5144
+ if ((0, import_node_fs12.existsSync)(outroPath)) {
4988
5145
  try {
4989
5146
  const mainDuration = await getMediaDuration(videoPath);
4990
5147
  mainContentDuration = mainDuration / 1e3 - trimStart;
4991
- const combinedPath = (0, import_node_path17.join)(workDir, "combined-with-outro.mp4");
5148
+ const combinedPath = (0, import_node_path18.join)(workDir, "combined-with-outro.mp4");
4992
5149
  await concatVideoWithOutro({
4993
5150
  inputPath: videoPath,
4994
5151
  outroPath,
@@ -5035,19 +5192,19 @@ async function runVideoPhase(params) {
5035
5192
  }
5036
5193
 
5037
5194
  // src/commands/phases/voiceover-phase.ts
5038
- var import_node_fs12 = require("fs");
5039
- var import_node_path18 = require("path");
5040
- var import_chalk8 = __toESM(require("chalk"));
5195
+ var import_node_fs13 = require("fs");
5196
+ var import_node_path19 = require("path");
5197
+ var import_chalk10 = __toESM(require("chalk"));
5041
5198
  init_lib();
5042
5199
  var INDENT5 = " ";
5043
5200
  async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5044
5201
  if (fresh) {
5045
5202
  const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
5046
5203
  for (const file of filesToDelete) {
5047
- const filePath = (0, import_node_path18.join)(config.output.dir, file);
5048
- if ((0, import_node_fs12.existsSync)(filePath)) {
5049
- const { unlink: unlink2 } = await import("fs/promises");
5050
- await unlink2(filePath).catch(() => {
5204
+ const filePath = (0, import_node_path19.join)(config.output.dir, file);
5205
+ if ((0, import_node_fs13.existsSync)(filePath)) {
5206
+ const { unlink: unlink3 } = await import("fs/promises");
5207
+ await unlink3(filePath).catch(() => {
5051
5208
  });
5052
5209
  }
5053
5210
  }
@@ -5055,8 +5212,8 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5055
5212
  }
5056
5213
  const voice = resolveVoice(config);
5057
5214
  const existingMeta = await loadVoiceoverMeta(config.output.dir);
5058
- const voiceoverFile = (0, import_node_path18.join)(config.output.dir, "voiceover.wav");
5059
- const hasExistingVoiceover = existingMeta && (0, import_node_fs12.existsSync)(voiceoverFile);
5215
+ const voiceoverFile = (0, import_node_path19.join)(config.output.dir, "voiceover.wav");
5216
+ const hasExistingVoiceover = existingMeta && (0, import_node_fs13.existsSync)(voiceoverFile);
5060
5217
  const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
5061
5218
  let voiceoverPath;
5062
5219
  let stepAudio;
@@ -5079,9 +5236,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
5079
5236
  const reuseCheck = canReuseVoiceover(plan.steps, voice, existingMeta);
5080
5237
  if (reuseCheck.canReuse) {
5081
5238
  console.log();
5082
- console.log(import_chalk8.default.dim(` Found existing voiceover in ${config.output.dir}`));
5239
+ console.log(import_chalk10.default.dim(` Found existing voiceover in ${config.output.dir}`));
5083
5240
  if (reuseCheck.reason) {
5084
- console.log(import_chalk8.default.dim(` ${reuseCheck.reason}`));
5241
+ console.log(import_chalk10.default.dim(` ${reuseCheck.reason}`));
5085
5242
  }
5086
5243
  const reuse = await confirm(`${INDENT5}Reuse existing voiceover?`);
5087
5244
  if (reuse) {
@@ -5121,7 +5278,7 @@ async function captureCommand(url, lang) {
5121
5278
  const captureResult = await captureUserSession(url, workDir);
5122
5279
  info(`Captured ${captureResult.events.length} interactions over ${(captureResult.durationMs / 1e3).toFixed(0)}s`);
5123
5280
  if (captureResult.events.length === 0) {
5124
- console.log(import_chalk9.default.yellow(" No interactions captured. Exiting."));
5281
+ console.log(import_chalk11.default.yellow(" No interactions captured. Exiting."));
5125
5282
  return;
5126
5283
  }
5127
5284
  header("Convert");
@@ -5129,7 +5286,7 @@ async function captureCommand(url, lang) {
5129
5286
  const rawSteps = eventsToSteps(captureResult.events, url);
5130
5287
  if (rawSteps.length === 0) {
5131
5288
  convertTask.fail("No actionable steps found");
5132
- console.log(import_chalk9.default.yellow(" No actionable steps could be derived from the captured events."));
5289
+ console.log(import_chalk11.default.yellow(" No actionable steps could be derived from the captured events."));
5133
5290
  return;
5134
5291
  }
5135
5292
  convertTask.succeed(`${rawSteps.length} steps extracted`);
@@ -5161,16 +5318,16 @@ async function captureCommand(url, lang) {
5161
5318
  ...lang !== void 0 ? { lang } : {},
5162
5319
  steps
5163
5320
  };
5164
- const planPath = (0, import_node_path19.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
5321
+ const planPath = (0, import_node_path20.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
5165
5322
  await savePlan(plan, planPath);
5166
5323
  console.log();
5167
- console.log(import_chalk9.default.bold.green(` Plan saved: ${planPath}`));
5168
- console.log(import_chalk9.default.dim(` Run: qp ${url} to replay and create a polished demo video`));
5324
+ console.log(import_chalk11.default.bold.green(` Plan saved: ${planPath}`));
5325
+ console.log(import_chalk11.default.dim(` Run: qp ${url} to replay and create a polished demo video`));
5169
5326
  console.log();
5170
5327
  const makeVideo = await confirm(" Create polished demo video now?");
5171
5328
  if (!makeVideo) {
5172
5329
  console.log();
5173
- console.log(import_chalk9.default.dim(` Run: qp ${url} anytime to create a video from this plan.`));
5330
+ console.log(import_chalk11.default.dim(` Run: qp ${url} anytime to create a video from this plan.`));
5174
5331
  return;
5175
5332
  }
5176
5333
  header("Voiceover");
@@ -5186,20 +5343,176 @@ async function captureCommand(url, lang) {
5186
5343
  userTier
5187
5344
  });
5188
5345
  console.log();
5189
- console.log(import_chalk9.default.bold.green(" Done!"));
5190
- console.log(import_chalk9.default.cyan(` ${finalPath}`));
5346
+ console.log(import_chalk11.default.bold.green(" Done!"));
5347
+ console.log(import_chalk11.default.cyan(` ${finalPath}`));
5348
+ }
5349
+
5350
+ // src/commands/edit-cmd.ts
5351
+ var import_node_fs14 = require("fs");
5352
+ var import_node_path21 = require("path");
5353
+ var import_chalk12 = __toESM(require("chalk"));
5354
+
5355
+ // src/lib/plan-validation.ts
5356
+ var INVALID_TARGETS = [
5357
+ /\.png$/i,
5358
+ /\.jpg$/i,
5359
+ /\.gif$/i,
5360
+ /\.webp$/i,
5361
+ /\.svg$/i,
5362
+ /sprite_sheet/i,
5363
+ /downloaded/i
5364
+ ];
5365
+ var ACTIONS_REQUIRING_TARGET = ["click", "type", "select", "check", "drag"];
5366
+ function isFilePath(value) {
5367
+ if (!value) return false;
5368
+ return /\.\w{2,5}$/.test(value) && (/[/\\]/.test(value) || /^[a-zA-Z]:/.test(value));
5369
+ }
5370
+ function isInvalidTarget(target) {
5371
+ return INVALID_TARGETS.some((pattern) => pattern.test(target));
5372
+ }
5373
+ function validateStep(step) {
5374
+ if (!step?.action) {
5375
+ return "Missing action";
5376
+ }
5377
+ if (step.action === "navigate") {
5378
+ if (!step.target) return "Navigate requires a URL target";
5379
+ if (!step.target.startsWith("http://") && !step.target.startsWith("https://")) {
5380
+ return "Navigate target must be a valid URL";
5381
+ }
5382
+ }
5383
+ if (step.action === "media") {
5384
+ if (!step.mediaFile && !isFilePath(step.value)) {
5385
+ return "Media step requires a file (image or video)";
5386
+ }
5387
+ }
5388
+ if (step.action === "upload") {
5389
+ if (!step.mediaFile && !isFilePath(step.value)) {
5390
+ return "Upload step requires a file";
5391
+ }
5392
+ }
5393
+ if (ACTIONS_REQUIRING_TARGET.includes(step.action)) {
5394
+ if (!step.target) return `${step.action} requires a target`;
5395
+ if (isInvalidTarget(step.target)) return `Invalid target pattern: ${step.target}`;
5396
+ }
5397
+ return null;
5398
+ }
5399
+
5400
+ // src/lib/plan-file.ts
5401
+ var MAX_HANDOFF_URL_CHARS = 32e3;
5402
+ function serializePlan(plan) {
5403
+ return `${JSON.stringify(plan, null, 2)}
5404
+ `;
5405
+ }
5406
+ function parsePlanText(text) {
5407
+ let raw;
5408
+ try {
5409
+ raw = JSON.parse(text);
5410
+ } catch (e) {
5411
+ return { ok: false, error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
5412
+ }
5413
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5414
+ return { ok: false, error: "A plan must be a JSON object." };
5415
+ }
5416
+ const candidate = raw;
5417
+ if (typeof candidate.url !== "string" || !candidate.url.trim()) {
5418
+ return { ok: false, error: "A plan needs a `url` to record." };
5419
+ }
5420
+ if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
5421
+ return { ok: false, error: "A plan needs at least one step." };
5422
+ }
5423
+ const steps = [];
5424
+ for (const [index, step] of candidate.steps.entries()) {
5425
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
5426
+ return { ok: false, error: `Step ${index + 1} is not an object.` };
5427
+ }
5428
+ const typed = step;
5429
+ if (typeof typed.action !== "string" || !typed.action.trim()) {
5430
+ return { ok: false, error: `Step ${index + 1} has no action.` };
5431
+ }
5432
+ const failure = validateStep(typed);
5433
+ if (failure) return { ok: false, error: `Step ${index + 1}: ${failure}` };
5434
+ steps.push({ ...typed, id: typed.id ?? index + 1 });
5435
+ }
5436
+ return {
5437
+ ok: true,
5438
+ plan: {
5439
+ url: candidate.url,
5440
+ title: candidate.title ?? "",
5441
+ description: candidate.description ?? "",
5442
+ ...candidate.lang ? { lang: candidate.lang } : {},
5443
+ steps
5444
+ }
5445
+ };
5446
+ }
5447
+ function toBase64Url(text) {
5448
+ const bytes = new TextEncoder().encode(text);
5449
+ const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
5450
+ const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(text, "utf8").toString("base64");
5451
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5452
+ }
5453
+ function encodePlan(plan) {
5454
+ return toBase64Url(serializePlan(plan));
5455
+ }
5456
+ function planEditorUrl(baseUrl, plan) {
5457
+ const url = `${baseUrl.replace(/\/$/, "")}/edit#plan=${encodePlan(plan)}`;
5458
+ return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
5459
+ }
5460
+
5461
+ // src/commands/edit-cmd.ts
5462
+ var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
5463
+ function findPlanFile(dir, lang) {
5464
+ if (lang) {
5465
+ const exact = (0, import_node_path21.join)(dir, `plan-${lang}.json`);
5466
+ return (0, import_node_fs14.existsSync)(exact) ? exact : null;
5467
+ }
5468
+ if (!(0, import_node_fs14.existsSync)(dir)) return null;
5469
+ const plans = (0, import_node_fs14.readdirSync)(dir).filter((f) => PLAN_FILE.test(f));
5470
+ const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
5471
+ return preferred ? (0, import_node_path21.join)(dir, preferred) : null;
5472
+ }
5473
+ async function editCommand(url, lang) {
5474
+ console.log();
5475
+ showLogo();
5476
+ const dir = getOutputDirFromUrl(url);
5477
+ const planPath = findPlanFile(dir, lang);
5478
+ if (!planPath) {
5479
+ console.log(import_chalk12.default.red(` No plan found in ${dir}`));
5480
+ console.log(import_chalk12.default.dim(` Record the demo first: `) + import_chalk12.default.cyan(`qp ${url}`));
5481
+ console.log();
5482
+ return;
5483
+ }
5484
+ const parsed = parsePlanText((0, import_node_fs14.readFileSync)(planPath, "utf8"));
5485
+ if (!parsed.ok) {
5486
+ console.log(import_chalk12.default.red(` ${planPath} cannot be opened: ${parsed.error}`));
5487
+ console.log();
5488
+ return;
5489
+ }
5490
+ const base = editorBaseUrl();
5491
+ const editorUrl = planEditorUrl(base, parsed.plan);
5492
+ if (!editorUrl) {
5493
+ console.log(import_chalk12.default.yellow(" This plan is too long to carry in a link."));
5494
+ console.log(import_chalk12.default.dim(` Open ${base}/edit and load `) + import_chalk12.default.cyan(formatClickablePath(planPath)));
5495
+ console.log();
5496
+ await openInBrowser(`${base}/edit`);
5497
+ return;
5498
+ }
5499
+ await openInBrowser(editorUrl);
5500
+ console.log(import_chalk12.default.green(` \u2713 Opened ${parsed.plan.steps.length} steps in the editor`));
5501
+ console.log(import_chalk12.default.dim(` ${formatClickablePath(planPath)}`));
5502
+ console.log(import_chalk12.default.dim(" Save from the editor over this file, then record again."));
5503
+ console.log();
5191
5504
  }
5192
5505
 
5193
5506
  // src/commands/init-cmd.ts
5194
- var import_node_fs13 = require("fs");
5195
- var import_promises13 = require("fs/promises");
5196
- var import_node_path20 = require("path");
5197
- var import_chalk10 = __toESM(require("chalk"));
5507
+ var import_node_fs15 = require("fs");
5508
+ var import_promises14 = require("fs/promises");
5509
+ var import_node_path22 = require("path");
5510
+ var import_chalk13 = __toESM(require("chalk"));
5198
5511
  init_lib();
5199
5512
  async function initCommand() {
5200
- const configPath = (0, import_node_path20.join)(process.cwd(), CONFIG_FILE);
5201
- if ((0, import_node_fs13.existsSync)(configPath)) {
5202
- console.log(import_chalk10.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
5513
+ const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
5514
+ if ((0, import_node_fs15.existsSync)(configPath)) {
5515
+ console.log(import_chalk13.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
5203
5516
  return;
5204
5517
  }
5205
5518
  const defaultConfig = {
@@ -5222,32 +5535,32 @@ async function initCommand() {
5222
5535
  volume: 0.3
5223
5536
  }
5224
5537
  };
5225
- await (0, import_promises13.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
5538
+ await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
5226
5539
  `);
5227
- console.log(import_chalk10.default.green(` \u2714 Created ${CONFIG_FILE}`));
5228
- console.log(import_chalk10.default.dim(` Edit it or use: qp config <key> <value>`));
5540
+ console.log(import_chalk13.default.green(` \u2714 Created ${CONFIG_FILE}`));
5541
+ console.log(import_chalk13.default.dim(` Edit it or use: qp config <key> <value>`));
5229
5542
  }
5230
5543
  async function configCommand(key, value) {
5231
- const configPath = (0, import_node_path20.join)(process.cwd(), CONFIG_FILE);
5544
+ const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
5232
5545
  if (!key) {
5233
- if (!(0, import_node_fs13.existsSync)(configPath)) {
5234
- console.log(import_chalk10.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
5546
+ if (!(0, import_node_fs15.existsSync)(configPath)) {
5547
+ console.log(import_chalk13.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
5235
5548
  return;
5236
5549
  }
5237
- const content = await (0, import_promises13.readFile)(configPath, "utf-8");
5238
- console.log(import_chalk10.default.cyan(` ${CONFIG_FILE}:`));
5550
+ const content = await (0, import_promises14.readFile)(configPath, "utf-8");
5551
+ console.log(import_chalk13.default.cyan(` ${CONFIG_FILE}:`));
5239
5552
  console.log(content);
5240
5553
  return;
5241
5554
  }
5242
5555
  if (!value) {
5243
- console.log(import_chalk10.default.red(` Missing value for ${key}`));
5244
- console.log(import_chalk10.default.dim(` Usage: qp config ${key} <value>`));
5556
+ console.log(import_chalk13.default.red(` Missing value for ${key}`));
5557
+ console.log(import_chalk13.default.dim(` Usage: qp config ${key} <value>`));
5245
5558
  return;
5246
5559
  }
5247
5560
  let config = {};
5248
- if ((0, import_node_fs13.existsSync)(configPath)) {
5561
+ if ((0, import_node_fs15.existsSync)(configPath)) {
5249
5562
  try {
5250
- config = JSON.parse(await (0, import_promises13.readFile)(configPath, "utf-8"));
5563
+ config = JSON.parse(await (0, import_promises14.readFile)(configPath, "utf-8"));
5251
5564
  } catch {
5252
5565
  }
5253
5566
  }
@@ -5269,42 +5582,42 @@ async function configCommand(key, value) {
5269
5582
  }
5270
5583
  obj[keys[keys.length - 1]] = parsedValue;
5271
5584
  }
5272
- await (0, import_promises13.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
5585
+ await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
5273
5586
  `);
5274
- console.log(import_chalk10.default.green(` \u2714 Set ${key} = ${value}`));
5587
+ console.log(import_chalk13.default.green(` \u2714 Set ${key} = ${value}`));
5275
5588
  }
5276
5589
 
5277
5590
  // src/commands/login-cmd.ts
5278
- var import_chalk11 = __toESM(require("chalk"));
5591
+ var import_chalk14 = __toESM(require("chalk"));
5279
5592
  init_lib();
5280
5593
  async function loginCommand() {
5281
5594
  console.log();
5282
5595
  showLogo();
5283
5596
  const existing = await loadUserData();
5284
5597
  if (existing.email && existing.verified) {
5285
- console.log(import_chalk11.default.dim(` Already signed in as ${existing.email}`));
5286
- console.log(import_chalk11.default.dim(" Run ") + import_chalk11.default.cyan("qp logout") + import_chalk11.default.dim(" first to switch accounts."));
5598
+ console.log(import_chalk14.default.dim(` Already signed in as ${existing.email}`));
5599
+ console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp logout") + import_chalk14.default.dim(" first to switch accounts."));
5287
5600
  console.log();
5288
5601
  try {
5289
5602
  const status = await getStatusCached(existing.email);
5290
5603
  const tier2 = await getTierByEmail(existing.email);
5291
- const tierLabel2 = tier2 === "pro" ? import_chalk11.default.green("Pro") : "Free";
5604
+ const tierLabel2 = tier2 === "pro" ? import_chalk14.default.green("Pro") : "Free";
5292
5605
  console.log(` ${tierLabel2} plan`);
5293
5606
  for (const line of formatUsageLines(status, tier2)) {
5294
- console.log(import_chalk11.default.dim(` ${line}`));
5607
+ console.log(import_chalk14.default.dim(` ${line}`));
5295
5608
  }
5296
5609
  } catch {
5297
5610
  }
5298
5611
  console.log();
5299
5612
  return;
5300
5613
  }
5301
- console.log(import_chalk11.default.white(" Sign in with your Google account to track usage"));
5302
- console.log(import_chalk11.default.white(" and unlock Pro features."));
5614
+ console.log(import_chalk14.default.white(" Sign in with your Google account to track usage"));
5615
+ console.log(import_chalk14.default.white(" and unlock Pro features."));
5303
5616
  const email = await browserLogin();
5304
5617
  if (!email) {
5305
5618
  console.log();
5306
- console.log(import_chalk11.default.yellow(" Sign-in cancelled or timed out."));
5307
- console.log(import_chalk11.default.dim(" Run ") + import_chalk11.default.cyan("qp login") + import_chalk11.default.dim(" to try again."));
5619
+ console.log(import_chalk14.default.yellow(" Sign-in cancelled or timed out."));
5620
+ console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp login") + import_chalk14.default.dim(" to try again."));
5308
5621
  console.log();
5309
5622
  return;
5310
5623
  }
@@ -5315,8 +5628,8 @@ async function loginCommand() {
5315
5628
  }
5316
5629
  await saveUserData({ email, verified: true, tier });
5317
5630
  console.log();
5318
- const tierLabel = tier === "pro" ? import_chalk11.default.green("Pro") : "Free";
5319
- console.log(import_chalk11.default.green(` \u2713 Signed in as ${email}`));
5631
+ const tierLabel = tier === "pro" ? import_chalk14.default.green("Pro") : "Free";
5632
+ console.log(import_chalk14.default.green(` \u2713 Signed in as ${email}`));
5320
5633
  console.log(` ${tierLabel} plan`);
5321
5634
  console.log();
5322
5635
  }
@@ -5324,46 +5637,46 @@ async function logoutCommand() {
5324
5637
  console.log();
5325
5638
  const userData = await loadUserData();
5326
5639
  if (!userData.email) {
5327
- console.log(import_chalk11.default.dim(" Not signed in."));
5640
+ console.log(import_chalk14.default.dim(" Not signed in."));
5328
5641
  console.log();
5329
5642
  return;
5330
5643
  }
5331
5644
  const email = userData.email;
5332
5645
  await saveUserData({ email: "", verified: false, tier: "free" });
5333
- console.log(import_chalk11.default.green(` \u2713 Signed out (${email})`));
5646
+ console.log(import_chalk14.default.green(` \u2713 Signed out (${email})`));
5334
5647
  console.log();
5335
5648
  }
5336
5649
 
5337
5650
  // src/commands/record-cmd.ts
5338
- var import_node_fs16 = require("fs");
5339
- var import_promises16 = require("fs/promises");
5340
- var import_node_path23 = require("path");
5341
- var import_chalk16 = __toESM(require("chalk"));
5651
+ var import_node_fs18 = require("fs");
5652
+ var import_promises17 = require("fs/promises");
5653
+ var import_node_path25 = require("path");
5654
+ var import_chalk19 = __toESM(require("chalk"));
5342
5655
  init_lib();
5343
5656
 
5344
5657
  // src/commands/phases/auth-phase.ts
5345
- var import_chalk13 = __toESM(require("chalk"));
5658
+ var import_chalk16 = __toESM(require("chalk"));
5346
5659
  init_lib();
5347
5660
 
5348
5661
  // src/commands/utils/auth-helpers.ts
5349
- var import_chalk12 = __toESM(require("chalk"));
5662
+ var import_chalk15 = __toESM(require("chalk"));
5350
5663
  init_lib();
5351
5664
  async function handleRegistrationGate() {
5352
5665
  console.log();
5353
- console.log(import_chalk12.default.yellow(" \u{1F389} You've made 5 free videos!"));
5354
- console.log(import_chalk12.default.white(" Sign in to continue creating videos."));
5666
+ console.log(import_chalk15.default.yellow(" \u{1F389} You've made 5 free videos!"));
5667
+ console.log(import_chalk15.default.white(" Sign in to continue creating videos."));
5355
5668
  console.log();
5356
- console.log(import_chalk12.default.dim(" Benefits of signing in:"));
5357
- console.log(import_chalk12.default.dim(" \u2022 20 more videos per month"));
5358
- console.log(import_chalk12.default.dim(" \u2022 Keep your video history"));
5359
- console.log(import_chalk12.default.dim(" \u2022 Upgrade to Pro anytime"));
5669
+ console.log(import_chalk15.default.dim(" Benefits of signing in:"));
5670
+ console.log(import_chalk15.default.dim(" \u2022 20 more videos per month"));
5671
+ console.log(import_chalk15.default.dim(" \u2022 Keep your video history"));
5672
+ console.log(import_chalk15.default.dim(" \u2022 Upgrade to Pro anytime"));
5360
5673
  let email = await browserLogin();
5361
5674
  if (!email) {
5362
5675
  console.log();
5363
5676
  email = await promptInput("Email: ");
5364
5677
  }
5365
5678
  if (!email || !email.includes("@")) {
5366
- console.log(import_chalk12.default.red(" Invalid email. Run qp again to try again."));
5679
+ console.log(import_chalk15.default.red(" Invalid email. Run qp again to try again."));
5367
5680
  return null;
5368
5681
  }
5369
5682
  const registerTask = task("Sending verification email");
@@ -5378,10 +5691,10 @@ async function handleRegistrationGate() {
5378
5691
  }
5379
5692
  registerTask.succeed("Check your email!");
5380
5693
  console.log();
5381
- console.log(import_chalk12.default.cyan(" \u{1F4E7} Verification email sent to:"));
5382
- console.log(import_chalk12.default.white(` ${email}`));
5694
+ console.log(import_chalk15.default.cyan(" \u{1F4E7} Verification email sent to:"));
5695
+ console.log(import_chalk15.default.white(` ${email}`));
5383
5696
  console.log();
5384
- console.log(import_chalk12.default.dim(" Click the link in the email, then press Enter to continue..."));
5697
+ console.log(import_chalk15.default.dim(" Click the link in the email, then press Enter to continue..."));
5385
5698
  await promptInput("");
5386
5699
  const verifyTask = task("Checking verification");
5387
5700
  const verified = await isVerified(email);
@@ -5391,10 +5704,10 @@ async function handleRegistrationGate() {
5391
5704
  }
5392
5705
  verifyTask.fail("Not verified yet");
5393
5706
  console.log();
5394
- console.log(import_chalk12.default.yellow(" Email not verified. Check your inbox and spam folder."));
5395
- console.log(import_chalk12.default.dim(" Run qp again after clicking the verification link."));
5707
+ console.log(import_chalk15.default.yellow(" Email not verified. Check your inbox and spam folder."));
5708
+ console.log(import_chalk15.default.dim(" Run qp again after clicking the verification link."));
5396
5709
  console.log();
5397
- console.log(import_chalk12.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
5710
+ console.log(import_chalk15.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
5398
5711
  console.log();
5399
5712
  return { email, verified: false };
5400
5713
  }
@@ -5422,7 +5735,7 @@ async function runAuthPhase(input) {
5422
5735
  } else {
5423
5736
  verifyTask.fail("Email not verified");
5424
5737
  console.log();
5425
- console.log(import_chalk13.default.yellow(" Check your inbox for the verification email."));
5738
+ console.log(import_chalk16.default.yellow(" Check your inbox for the verification email."));
5426
5739
  console.log();
5427
5740
  const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
5428
5741
  if (input2.toLowerCase() === "exit") {
@@ -5447,8 +5760,8 @@ async function runAuthPhase(input) {
5447
5760
  registerTask.succeed("Check your email!");
5448
5761
  await saveUserData({ email: newEmail, verified: false });
5449
5762
  console.log();
5450
- console.log(import_chalk13.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
5451
- console.log(import_chalk13.default.dim(" Click the link in the email, then run qp again."));
5763
+ console.log(import_chalk16.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
5764
+ console.log(import_chalk16.default.dim(" Click the link in the email, then run qp again."));
5452
5765
  console.log();
5453
5766
  gracefulExit();
5454
5767
  return null;
@@ -5464,7 +5777,7 @@ async function runAuthPhase(input) {
5464
5777
  } else {
5465
5778
  recheckTask.fail("Still not verified");
5466
5779
  console.log();
5467
- console.log(import_chalk13.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
5780
+ console.log(import_chalk16.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
5468
5781
  console.log();
5469
5782
  gracefulExit();
5470
5783
  return null;
@@ -5491,10 +5804,16 @@ async function runAuthPhase(input) {
5491
5804
  } else {
5492
5805
  const remaining = FREE_VIDEO_LIMIT - localVideoCount;
5493
5806
  console.log();
5494
- console.log(import_chalk13.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
5495
- console.log(import_chalk13.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
5807
+ console.log(import_chalk16.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
5808
+ console.log(import_chalk16.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
5496
5809
  userEmail = await setupUserEmail();
5497
- await saveUserData({ email: userEmail, verified: false, localVideoCount });
5810
+ let registeredVerified = false;
5811
+ try {
5812
+ const reg = await registerFreeUser(userEmail);
5813
+ registeredVerified = Boolean(reg.success && reg.alreadyVerified);
5814
+ } catch {
5815
+ }
5816
+ await saveUserData({ email: userEmail, verified: registeredVerified, localVideoCount });
5498
5817
  }
5499
5818
  return {
5500
5819
  userEmail: userEmail ?? "anonymous",
@@ -5505,10 +5824,10 @@ async function runAuthPhase(input) {
5505
5824
  }
5506
5825
 
5507
5826
  // src/commands/phases/plan-phase.ts
5508
- var import_node_fs14 = require("fs");
5509
- var import_promises14 = require("fs/promises");
5510
- var import_node_path21 = require("path");
5511
- var import_chalk14 = __toESM(require("chalk"));
5827
+ var import_node_fs16 = require("fs");
5828
+ var import_promises15 = require("fs/promises");
5829
+ var import_node_path23 = require("path");
5830
+ var import_chalk17 = __toESM(require("chalk"));
5512
5831
 
5513
5832
  // src/ai/fix.ts
5514
5833
  init_lib();
@@ -5660,14 +5979,24 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
5660
5979
  // src/commands/phases/plan-phase.ts
5661
5980
  var INDENT6 = " ";
5662
5981
  async function runPlanPhase(params) {
5663
- const { url, config, userEmail, description, planPath, legacyPlanPath, targetLang, userTier = "free" } = params;
5982
+ const {
5983
+ url,
5984
+ config,
5985
+ userEmail,
5986
+ description,
5987
+ planPath,
5988
+ legacyPlanPath,
5989
+ targetLang,
5990
+ userTier = "free",
5991
+ fresh = false
5992
+ } = params;
5664
5993
  const maxSteps = config.plan?.maxSteps ?? 8;
5665
5994
  const extensionPath = config.browser?.extension;
5666
5995
  let existingPlan = null;
5667
5996
  let existingPlanPath = null;
5668
- if ((0, import_node_fs14.existsSync)(planPath)) {
5997
+ if ((0, import_node_fs16.existsSync)(planPath)) {
5669
5998
  try {
5670
- const parsed = JSON.parse(await (0, import_promises14.readFile)(planPath, "utf-8"));
5999
+ const parsed = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
5671
6000
  if (parsed.url && parsed.steps?.length > 0) {
5672
6001
  existingPlan = parsed;
5673
6002
  existingPlanPath = planPath;
@@ -5677,12 +6006,12 @@ async function runPlanPhase(params) {
5677
6006
  }
5678
6007
  if (!existingPlan) {
5679
6008
  try {
5680
- const files = (0, import_node_fs14.readdirSync)(config.output.dir);
5681
- const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path21.basename)(planPath));
6009
+ const files = (0, import_node_fs16.readdirSync)(config.output.dir);
6010
+ const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path23.basename)(planPath));
5682
6011
  for (const file of planFiles) {
5683
- const filePath = (0, import_node_path21.join)(config.output.dir, file);
6012
+ const filePath = (0, import_node_path23.join)(config.output.dir, file);
5684
6013
  try {
5685
- const parsed = JSON.parse(await (0, import_promises14.readFile)(filePath, "utf-8"));
6014
+ const parsed = JSON.parse(await (0, import_promises15.readFile)(filePath, "utf-8"));
5686
6015
  if (parsed.url && parsed.steps?.length > 0) {
5687
6016
  existingPlan = parsed;
5688
6017
  existingPlanPath = filePath;
@@ -5694,9 +6023,9 @@ async function runPlanPhase(params) {
5694
6023
  } catch {
5695
6024
  }
5696
6025
  }
5697
- if (!existingPlan && (0, import_node_fs14.existsSync)(legacyPlanPath)) {
6026
+ if (!existingPlan && (0, import_node_fs16.existsSync)(legacyPlanPath)) {
5698
6027
  try {
5699
- const parsed = JSON.parse(await (0, import_promises14.readFile)(legacyPlanPath, "utf-8"));
6028
+ const parsed = JSON.parse(await (0, import_promises15.readFile)(legacyPlanPath, "utf-8"));
5700
6029
  if (parsed.url && parsed.steps?.length > 0) {
5701
6030
  existingPlan = parsed;
5702
6031
  existingPlanPath = legacyPlanPath;
@@ -5720,25 +6049,25 @@ async function runPlanPhase(params) {
5720
6049
  translateTask.fail(failMsg);
5721
6050
  }
5722
6051
  };
5723
- if (existingPlan && existingPlanPath) {
6052
+ if (existingPlan && existingPlanPath && !fresh) {
5724
6053
  console.log();
5725
- console.log(import_chalk14.default.dim(` Found existing plan: `) + import_chalk14.default.cyan(formatClickablePath(existingPlanPath)));
5726
- console.log(import_chalk14.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
6054
+ console.log(import_chalk17.default.dim(` Found existing plan: `) + import_chalk17.default.cyan(formatClickablePath(existingPlanPath)));
6055
+ console.log(import_chalk17.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
5727
6056
  if (!urlMatches) {
5728
- console.log(import_chalk14.default.yellow(` \u26A0 Plan URL differs from argument:`));
5729
- console.log(import_chalk14.default.yellow(` Plan: ${existingPlan.url}`));
5730
- console.log(import_chalk14.default.yellow(` Arg: ${url}`));
6057
+ console.log(import_chalk17.default.yellow(` \u26A0 Plan URL differs from argument:`));
6058
+ console.log(import_chalk17.default.yellow(` Plan: ${existingPlan.url}`));
6059
+ console.log(import_chalk17.default.yellow(` Arg: ${url}`));
5731
6060
  }
5732
6061
  const existingLang = existingPlan.lang || "en";
5733
6062
  if (existingLang !== targetLang) {
5734
- console.log(import_chalk14.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
6063
+ console.log(import_chalk17.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
5735
6064
  }
5736
6065
  const reuse = await confirm(`${INDENT6}Use existing plan?`);
5737
6066
  if (reuse) {
5738
6067
  plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
5739
6068
  if (existingPlanPath !== planPath) {
5740
6069
  await savePlan(plan, planPath);
5741
- console.log(import_chalk14.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
6070
+ console.log(import_chalk17.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
5742
6071
  }
5743
6072
  if (existingLang !== targetLang) {
5744
6073
  const translate = await confirm(`${INDENT6}Translate to ${targetLang}?`);
@@ -5756,7 +6085,8 @@ async function runPlanPhase(params) {
5756
6085
  userEmail,
5757
6086
  extensionPath,
5758
6087
  config.output.dir,
5759
- userTier
6088
+ userTier,
6089
+ { width: config.video.width, height: config.video.height }
5760
6090
  );
5761
6091
  displayRateLimitInfo();
5762
6092
  if (urlMatches) {
@@ -5773,7 +6103,7 @@ async function runPlanPhase(params) {
5773
6103
  uploadIdx++;
5774
6104
  }
5775
6105
  }
5776
- console.log(import_chalk14.default.dim(` Preserved upload paths from previous plan`));
6106
+ console.log(import_chalk17.default.dim(` Preserved upload paths from previous plan`));
5777
6107
  }
5778
6108
  await savePlan(plan, planPath);
5779
6109
  plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
@@ -5789,7 +6119,9 @@ async function runPlanPhase(params) {
5789
6119
  logError,
5790
6120
  userEmail,
5791
6121
  extensionPath,
5792
- config.output.dir
6122
+ config.output.dir,
6123
+ void 0,
6124
+ { width: config.video.width, height: config.video.height }
5793
6125
  );
5794
6126
  displayRateLimitInfo();
5795
6127
  await savePlan(plan, planPath);
@@ -5797,8 +6129,8 @@ async function runPlanPhase(params) {
5797
6129
  }
5798
6130
  displayPlanSummary(plan);
5799
6131
  console.log();
5800
- console.log(import_chalk14.default.dim(` Plan: ${formatClickablePath(planPath)}`));
5801
- console.log(import_chalk14.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
6132
+ console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
6133
+ console.log(import_chalk17.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
5802
6134
  const MAX_REFINEMENTS = 10;
5803
6135
  let refinementCount = 0;
5804
6136
  while (true) {
@@ -5816,15 +6148,15 @@ async function runPlanPhase(params) {
5816
6148
  refineTask.succeed("Plan refined");
5817
6149
  displayRateLimitInfo();
5818
6150
  if (plan.steps.length > maxSteps) {
5819
- console.log(import_chalk14.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
6151
+ console.log(import_chalk17.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
5820
6152
  }
5821
6153
  displayPlanSummary(plan);
5822
6154
  console.log();
5823
- console.log(import_chalk14.default.dim(` Plan: ${formatClickablePath(planPath)}`));
6155
+ console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
5824
6156
  if (refinementCount >= MAX_REFINEMENTS) {
5825
6157
  console.log();
5826
- console.log(import_chalk14.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
5827
- console.log(import_chalk14.default.dim(` Further refinements may incur additional costs.`));
6158
+ console.log(import_chalk17.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
6159
+ console.log(import_chalk17.default.dim(` Further refinements may incur additional costs.`));
5828
6160
  const continueRefining = await confirm(`${INDENT6}Continue refining?`);
5829
6161
  if (!continueRefining) break;
5830
6162
  }
@@ -5836,12 +6168,12 @@ async function runPlanPhase(params) {
5836
6168
  const planLang = plan.lang || "en";
5837
6169
  if (planLang !== targetLang) {
5838
6170
  let useExistingTranslation = false;
5839
- if ((0, import_node_fs14.existsSync)(planPath) && existingPlanPath !== planPath) {
6171
+ if ((0, import_node_fs16.existsSync)(planPath) && existingPlanPath !== planPath) {
5840
6172
  try {
5841
- const translatedPlan = JSON.parse(await (0, import_promises14.readFile)(planPath, "utf-8"));
6173
+ const translatedPlan = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
5842
6174
  if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
5843
6175
  console.log();
5844
- console.log(import_chalk14.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
6176
+ console.log(import_chalk17.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
5845
6177
  const useTranslated = await confirm(`${INDENT6}Use ${targetLang} translation?`);
5846
6178
  if (useTranslated) {
5847
6179
  plan = translatedPlan;
@@ -5853,7 +6185,7 @@ async function runPlanPhase(params) {
5853
6185
  }
5854
6186
  if (!useExistingTranslation) {
5855
6187
  console.log();
5856
- console.log(import_chalk14.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
6188
+ console.log(import_chalk17.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
5857
6189
  const translate = await confirm(`${INDENT6}Translate captions to ${targetLang}?`);
5858
6190
  if (translate) await doTranslate("Translation failed, using original");
5859
6191
  }
@@ -5862,8 +6194,8 @@ async function runPlanPhase(params) {
5862
6194
  }
5863
6195
 
5864
6196
  // src/commands/phases/validation-phase.ts
5865
- var import_promises15 = require("fs/promises");
5866
- var import_chalk15 = __toESM(require("chalk"));
6197
+ var import_promises16 = require("fs/promises");
6198
+ var import_chalk18 = __toESM(require("chalk"));
5867
6199
  async function runValidationPhase(params) {
5868
6200
  let { plan } = params;
5869
6201
  const { config, planPath, isExtensionUrl, userEmail, userTier = "free" } = params;
@@ -5914,21 +6246,21 @@ async function runValidationPhase(params) {
5914
6246
  const stepNum = result.stepIndex + 1;
5915
6247
  const step = plan.steps[result.stepIndex];
5916
6248
  console.log();
5917
- console.log(import_chalk15.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
6249
+ console.log(import_chalk18.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
5918
6250
  const caption = step.caption || "";
5919
- if (caption) console.log(import_chalk15.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
5920
- console.log(import_chalk15.default.dim(` Target: ${step.target || "none"}`));
5921
- if (step.value) console.log(import_chalk15.default.dim(` Value: ${step.value}`));
5922
- console.log(import_chalk15.default.dim(` Error: ${result.error}`));
6251
+ if (caption) console.log(import_chalk18.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
6252
+ console.log(import_chalk18.default.dim(` Target: ${step.target || "none"}`));
6253
+ if (step.value) console.log(import_chalk18.default.dim(` Value: ${step.value}`));
6254
+ console.log(import_chalk18.default.dim(` Error: ${result.error}`));
5923
6255
  if (result.suggestion) {
5924
- console.log(import_chalk15.default.yellow(` Hint: ${result.suggestion}`));
6256
+ console.log(import_chalk18.default.yellow(` Hint: ${result.suggestion}`));
5925
6257
  }
5926
6258
  console.log();
5927
6259
  const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
5928
6260
  if (isUploadMissingFile) {
5929
6261
  const filePath = await promptInput('Enter file path to upload (or "skip"): ');
5930
6262
  if (filePath.toLowerCase() === "skip") {
5931
- console.log(import_chalk15.default.yellow(` Skipping step ${stepNum}`));
6263
+ console.log(import_chalk18.default.yellow(` Skipping step ${stepNum}`));
5932
6264
  continue;
5933
6265
  }
5934
6266
  if (filePath) {
@@ -5937,13 +6269,13 @@ async function runValidationPhase(params) {
5937
6269
  const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
5938
6270
  const newResult2 = revalidation2.results[result.stepIndex];
5939
6271
  if (newResult2?.success) {
5940
- console.log(import_chalk15.default.green(` \u2713 Step ${stepNum} now passes`));
6272
+ console.log(import_chalk18.default.green(` \u2713 Step ${stepNum} now passes`));
5941
6273
  validationResult = revalidation2;
5942
6274
  continue;
5943
6275
  }
5944
6276
  result.error = newResult2?.error;
5945
6277
  result.suggestion = newResult2?.suggestion;
5946
- console.log(import_chalk15.default.red(` Still failing: ${result.error}`));
6278
+ console.log(import_chalk18.default.red(` Still failing: ${result.error}`));
5947
6279
  }
5948
6280
  }
5949
6281
  const fixTask = task(`Fixing step ${stepNum} with AI`);
@@ -5965,8 +6297,8 @@ async function runValidationPhase(params) {
5965
6297
  await savePlan(plan, planPath);
5966
6298
  fixTask.succeed("AI suggested a fix");
5967
6299
  const fixedStep = plan.steps[result.stepIndex];
5968
- console.log(import_chalk15.default.dim(` New target: ${fixedStep.target || "none"}`));
5969
- if (fixedStep.value) console.log(import_chalk15.default.dim(` New value: ${fixedStep.value}`));
6300
+ console.log(import_chalk18.default.dim(` New target: ${fixedStep.target || "none"}`));
6301
+ if (fixedStep.value) console.log(import_chalk18.default.dim(` New value: ${fixedStep.value}`));
5970
6302
  } catch {
5971
6303
  fixTask.fail("AI could not fix automatically");
5972
6304
  }
@@ -5982,14 +6314,14 @@ async function runValidationPhase(params) {
5982
6314
  result.error = newResult?.error;
5983
6315
  result.suggestion = newResult?.suggestion;
5984
6316
  console.log();
5985
- console.log(import_chalk15.default.yellow(` Edit step ${stepNum} in: `) + import_chalk15.default.cyan(formatClickablePath(planPath)));
6317
+ console.log(import_chalk18.default.yellow(` Edit step ${stepNum} in: `) + import_chalk18.default.cyan(formatClickablePath(planPath)));
5986
6318
  console.log();
5987
6319
  const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
5988
6320
  if (choice.toLowerCase() === "skip") {
5989
- console.log(import_chalk15.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
6321
+ console.log(import_chalk18.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
5990
6322
  continue;
5991
6323
  }
5992
- plan = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
6324
+ plan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
5993
6325
  const finalRevalidateTask = task(`Final validation of step ${stepNum}`);
5994
6326
  const finalRevalidation = await validateStepsOnPage(plan, config, logError, config.output.dir);
5995
6327
  const finalResult = finalRevalidation.results[result.stepIndex];
@@ -6013,31 +6345,31 @@ async function runValidationPhase(params) {
6013
6345
  }
6014
6346
 
6015
6347
  // src/commands/utils/chrome-extension.ts
6016
- var import_node_fs15 = require("fs");
6017
- var import_node_os2 = require("os");
6018
- var import_node_path22 = require("path");
6348
+ var import_node_fs17 = require("fs");
6349
+ var import_node_os3 = require("os");
6350
+ var import_node_path24 = require("path");
6019
6351
  function resolveChromeExtension(extensionId) {
6020
6352
  const platform = process.platform;
6021
6353
  const possibleDirs = [];
6022
6354
  const getChromeExtDirs = (userDataPath) => {
6023
6355
  const dirs = [];
6024
6356
  try {
6025
- const profiles = (0, import_node_fs15.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
6357
+ const profiles = (0, import_node_fs17.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
6026
6358
  for (const profile of profiles) {
6027
- dirs.push((0, import_node_path22.join)(userDataPath, profile, "Extensions"));
6359
+ dirs.push((0, import_node_path24.join)(userDataPath, profile, "Extensions"));
6028
6360
  }
6029
6361
  } catch {
6030
- dirs.push((0, import_node_path22.join)(userDataPath, "Default", "Extensions"));
6362
+ dirs.push((0, import_node_path24.join)(userDataPath, "Default", "Extensions"));
6031
6363
  }
6032
6364
  return dirs;
6033
6365
  };
6034
6366
  if (platform === "win32") {
6035
- possibleDirs.push(...getChromeExtDirs((0, import_node_path22.join)((0, import_node_os2.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
6367
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
6036
6368
  } else if (platform === "darwin") {
6037
- possibleDirs.push(...getChromeExtDirs((0, import_node_path22.join)((0, import_node_os2.homedir)(), "Library", "Application Support", "Google", "Chrome")));
6369
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
6038
6370
  } else {
6039
6371
  try {
6040
- const windowsUsers = (0, import_node_fs15.readdirSync)("/mnt/c/Users").filter(
6372
+ const windowsUsers = (0, import_node_fs17.readdirSync)("/mnt/c/Users").filter(
6041
6373
  (u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
6042
6374
  );
6043
6375
  for (const user of windowsUsers) {
@@ -6045,22 +6377,22 @@ function resolveChromeExtension(extensionId) {
6045
6377
  }
6046
6378
  } catch {
6047
6379
  }
6048
- possibleDirs.push(...getChromeExtDirs((0, import_node_path22.join)((0, import_node_os2.homedir)(), ".config", "google-chrome")));
6380
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
6049
6381
  }
6050
6382
  for (const chromeExtDir of possibleDirs) {
6051
- const extDir = (0, import_node_path22.join)(chromeExtDir, extensionId);
6052
- if (!(0, import_node_fs15.existsSync)(extDir)) continue;
6383
+ const extDir = (0, import_node_path24.join)(chromeExtDir, extensionId);
6384
+ if (!(0, import_node_fs17.existsSync)(extDir)) continue;
6053
6385
  try {
6054
- const versions = (0, import_node_fs15.readdirSync)(extDir).filter((f) => !f.startsWith("."));
6386
+ const versions = (0, import_node_fs17.readdirSync)(extDir).filter((f) => !f.startsWith("."));
6055
6387
  if (versions.length === 0) continue;
6056
6388
  const latestVersion = versions.sort().pop();
6057
- const extPath = (0, import_node_path22.join)(extDir, latestVersion);
6389
+ const extPath = (0, import_node_path24.join)(extDir, latestVersion);
6058
6390
  let name = extensionId;
6059
6391
  let popupPath = "popup.html";
6060
- const manifestPath = (0, import_node_path22.join)(extPath, "manifest.json");
6061
- if ((0, import_node_fs15.existsSync)(manifestPath)) {
6392
+ const manifestPath = (0, import_node_path24.join)(extPath, "manifest.json");
6393
+ if ((0, import_node_fs17.existsSync)(manifestPath)) {
6062
6394
  try {
6063
- const manifest = JSON.parse((0, import_node_fs15.readFileSync)(manifestPath, "utf-8"));
6395
+ const manifest = JSON.parse((0, import_node_fs17.readFileSync)(manifestPath, "utf-8"));
6064
6396
  if (manifest.name && !manifest.name.startsWith("__MSG_")) {
6065
6397
  name = manifest.name;
6066
6398
  }
@@ -6080,7 +6412,8 @@ function resolveChromeExtension(extensionId) {
6080
6412
  }
6081
6413
 
6082
6414
  // src/commands/record-cmd.ts
6083
- async function recordCommand(url, langOverride, description, skipVoice, fresh, autoYes2) {
6415
+ async function recordCommand(url, options = {}) {
6416
+ const { langOverride, description, skipVoice, fresh, autoYes: autoYes2, rethrow } = options;
6084
6417
  if (autoYes2) setAutoYes(true);
6085
6418
  console.log();
6086
6419
  showLogo();
@@ -6090,16 +6423,16 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
6090
6423
  const extensionId = url.slice(7);
6091
6424
  chromeExtension = resolveChromeExtension(extensionId);
6092
6425
  if (!chromeExtension) {
6093
- console.log(import_chalk16.default.red(`
6426
+ console.log(import_chalk19.default.red(`
6094
6427
  \u2717 Chrome extension not found: ${extensionId}`));
6095
- console.log(import_chalk16.default.dim(` Make sure the extension is installed in Chrome
6428
+ console.log(import_chalk19.default.dim(` Make sure the extension is installed in Chrome
6096
6429
  `));
6097
6430
  process.exit(1);
6098
6431
  }
6099
6432
  config.browser = { extension: chromeExtension.path };
6100
6433
  url = chromeExtension.popupUrl;
6101
- console.log(import_chalk16.default.dim(` Extension: ${chromeExtension.name}`));
6102
- console.log(import_chalk16.default.dim(` Path: ${chromeExtension.path}`));
6434
+ console.log(import_chalk19.default.dim(` Extension: ${chromeExtension.name}`));
6435
+ console.log(import_chalk19.default.dim(` Path: ${chromeExtension.path}`));
6103
6436
  }
6104
6437
  config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
6105
6438
  const userData = await loadUserData();
@@ -6112,20 +6445,20 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
6112
6445
  if (!authResult) return;
6113
6446
  const { userEmail, userTier, localVideoCount } = authResult;
6114
6447
  if (configCreated) {
6115
- const configPath = (0, import_node_path23.join)(process.cwd(), CONFIG_FILE);
6448
+ const configPath = (0, import_node_path25.join)(process.cwd(), CONFIG_FILE);
6116
6449
  console.log();
6117
- console.log(import_chalk16.default.green(` \u2714 Created ${CONFIG_FILE}`));
6118
- console.log(import_chalk16.default.dim(` ${configPath}`));
6450
+ console.log(import_chalk19.default.green(` \u2714 Created ${CONFIG_FILE}`));
6451
+ console.log(import_chalk19.default.dim(` ${configPath}`));
6119
6452
  console.log();
6120
- console.log(import_chalk16.default.cyan(" Review the config file to customize:"));
6121
- console.log(import_chalk16.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
6122
- console.log(import_chalk16.default.dim(" \u2022 video: Resolution, quality, effects"));
6123
- console.log(import_chalk16.default.dim(" \u2022 music: Background music path and volume"));
6124
- console.log(import_chalk16.default.dim(" \u2022 output: Output directory"));
6453
+ console.log(import_chalk19.default.cyan(" Review the config file to customize:"));
6454
+ console.log(import_chalk19.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
6455
+ console.log(import_chalk19.default.dim(" \u2022 video: Resolution, quality, effects"));
6456
+ console.log(import_chalk19.default.dim(" \u2022 music: Background music path and volume"));
6457
+ console.log(import_chalk19.default.dim(" \u2022 output: Output directory"));
6125
6458
  console.log();
6126
6459
  const proceed = await confirm(`${INDENT3}Proceed with default settings?`);
6127
6460
  if (!proceed) {
6128
- console.log(import_chalk16.default.yellow(`
6461
+ console.log(import_chalk19.default.yellow(`
6129
6462
  Edit ${CONFIG_FILE} and run again.
6130
6463
  `));
6131
6464
  process.exit(0);
@@ -6133,17 +6466,17 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
6133
6466
  }
6134
6467
  if (langOverride) config.lang = langOverride;
6135
6468
  if (config.music?.path) {
6136
- const musicPath = (0, import_node_path23.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path23.resolve)(config.output.dir, config.music.path);
6137
- if (!(0, import_node_fs16.existsSync)(musicPath)) {
6469
+ const musicPath = (0, import_node_path25.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path25.resolve)(config.output.dir, config.music.path);
6470
+ if (!(0, import_node_fs18.existsSync)(musicPath)) {
6138
6471
  console.log();
6139
- console.log(import_chalk16.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
6140
- console.log(import_chalk16.default.dim(` Config: ${(0, import_node_path23.join)(process.cwd(), CONFIG_FILE)}`));
6472
+ console.log(import_chalk19.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
6473
+ console.log(import_chalk19.default.dim(` Config: ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}`));
6141
6474
  console.log();
6142
6475
  const proceed = await confirm(`${INDENT3}Continue without background music?`);
6143
6476
  if (!proceed) {
6144
- console.log(import_chalk16.default.yellow(`
6477
+ console.log(import_chalk19.default.yellow(`
6145
6478
  Edit music.path in config and run again.`));
6146
- console.log(import_chalk16.default.dim(` ${(0, import_node_path23.join)(process.cwd(), CONFIG_FILE)}
6479
+ console.log(import_chalk19.default.dim(` ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}
6147
6480
  `));
6148
6481
  process.exit(0);
6149
6482
  }
@@ -6153,10 +6486,10 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
6153
6486
  const captions = await loadCaptions();
6154
6487
  const workDir = await prepareWorkDir();
6155
6488
  const targetLang = config.lang || "en";
6156
- const planPath = (0, import_node_path23.join)(config.output.dir, `plan-${targetLang}.json`);
6157
- const legacyPlanPath = (0, import_node_path23.join)(config.output.dir, "plan.json");
6489
+ const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${targetLang}.json`);
6490
+ const legacyPlanPath = (0, import_node_path25.join)(config.output.dir, "plan.json");
6158
6491
  const isExtensionUrl = url.startsWith("chrome-extension://");
6159
- await (0, import_promises16.mkdir)(config.output.dir, { recursive: true });
6492
+ await (0, import_promises17.mkdir)(config.output.dir, { recursive: true });
6160
6493
  await setLogFilePath(config.output.dir);
6161
6494
  try {
6162
6495
  const plan = await runPlanPhase({
@@ -6167,7 +6500,8 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
6167
6500
  ...description !== void 0 && { description },
6168
6501
  planPath,
6169
6502
  legacyPlanPath,
6170
- targetLang
6503
+ targetLang,
6504
+ ...fresh !== void 0 && { fresh }
6171
6505
  });
6172
6506
  const validatedPlan = await runValidationPhase({
6173
6507
  plan,
@@ -6192,35 +6526,37 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
6192
6526
  await saveUserData({ localVideoCount: newCount });
6193
6527
  const remaining = FREE_VIDEO_LIMIT - newCount;
6194
6528
  if (remaining > 0) {
6195
- console.log(import_chalk16.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
6529
+ console.log(import_chalk19.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
6196
6530
  }
6197
6531
  }
6198
6532
  console.log();
6199
- console.log(import_chalk16.default.bold.green(" \u2713 Done!"));
6533
+ console.log(import_chalk19.default.bold.green(" \u2713 Done!"));
6200
6534
  console.log();
6201
- console.log(import_chalk16.default.cyan(` ${formatClickablePath(finalPath)}`));
6535
+ console.log(import_chalk19.default.cyan(` ${formatClickablePath(finalPath)}`));
6202
6536
  console.log();
6537
+ return finalPath;
6203
6538
  } catch (err) {
6204
6539
  await logError("Record Command", err);
6540
+ if (rethrow) throw err;
6205
6541
  console.log();
6206
- console.log(import_chalk16.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
6207
- console.log(import_chalk16.default.dim(` See ${logFilePath} for details`));
6542
+ console.log(import_chalk19.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
6543
+ console.log(import_chalk19.default.dim(` See ${logFilePath} for details`));
6208
6544
  process.exit(1);
6209
6545
  }
6210
6546
  }
6211
6547
 
6212
6548
  // src/commands/script-cmd.ts
6213
- var import_promises17 = require("fs/promises");
6214
- var import_node_path24 = require("path");
6215
- var import_chalk17 = __toESM(require("chalk"));
6549
+ var import_promises18 = require("fs/promises");
6550
+ var import_node_path26 = require("path");
6551
+ var import_chalk20 = __toESM(require("chalk"));
6216
6552
  init_lib();
6217
6553
  async function scriptCommand(url, scriptPath, lang) {
6218
6554
  const { config, workDir, captions } = await initCommandEnv(url, lang);
6219
6555
  header("Script");
6220
- const raw = await (0, import_promises17.readFile)(scriptPath, "utf-8");
6556
+ const raw = await (0, import_promises18.readFile)(scriptPath, "utf-8");
6221
6557
  const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
6222
6558
  if (lines.length === 0) {
6223
- console.log(import_chalk17.default.red(" Script file is empty or has no non-comment lines"));
6559
+ console.log(import_chalk20.default.red(" Script file is empty or has no non-comment lines"));
6224
6560
  process.exit(1);
6225
6561
  }
6226
6562
  info(`${lines.length} captions from script`);
@@ -6232,7 +6568,7 @@ async function scriptCommand(url, scriptPath, lang) {
6232
6568
  const planTask = task("Mapping script to UI actions");
6233
6569
  const appTitle = new URL(url).hostname.replace(/^www\./, "");
6234
6570
  const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
6235
- const planPath = (0, import_node_path24.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
6571
+ const planPath = (0, import_node_path26.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
6236
6572
  await savePlan(plan, planPath);
6237
6573
  planTask.succeed(`${plan.steps.length} steps planned`);
6238
6574
  header("Voiceover");
@@ -6252,8 +6588,8 @@ async function scriptCommand(url, scriptPath, lang) {
6252
6588
  const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
6253
6589
  const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
6254
6590
  console.log();
6255
- console.log(import_chalk17.default.bold.green(" Done!"));
6256
- console.log(import_chalk17.default.cyan(` ${finalPath}`));
6591
+ console.log(import_chalk20.default.bold.green(" Done!"));
6592
+ console.log(import_chalk20.default.cyan(` ${finalPath}`));
6257
6593
  }
6258
6594
 
6259
6595
  // src/qp.ts
@@ -6298,6 +6634,16 @@ function parseArgs(args) {
6298
6634
  if (first === "logout") {
6299
6635
  return { ...base, command: "logout" };
6300
6636
  }
6637
+ if (first === "auth") {
6638
+ const sub = args[1];
6639
+ if (sub === "list" || sub === void 0) {
6640
+ return { ...base, command: "auth-list" };
6641
+ }
6642
+ if (sub === "clear") {
6643
+ return { ...base, command: "auth-clear", url: args[2] ? normalizeUrl3(args[2]) : void 0 };
6644
+ }
6645
+ return { ...base, command: "auth", url: normalizeUrl3(sub) };
6646
+ }
6301
6647
  if (first === "init") {
6302
6648
  return { ...base, command: "init" };
6303
6649
  }
@@ -6313,6 +6659,20 @@ function parseArgs(args) {
6313
6659
  if (first === "record") {
6314
6660
  args = args.slice(1);
6315
6661
  }
6662
+ if (first === "edit") {
6663
+ const rest = args.slice(1);
6664
+ let url2;
6665
+ let lang2;
6666
+ for (const arg2 of rest) {
6667
+ if (arg2.startsWith("lang=")) {
6668
+ lang2 = arg2.slice(5);
6669
+ } else if (!arg2.startsWith("-") && !url2 && !arg2.includes("=")) {
6670
+ url2 = normalizeUrl3(arg2);
6671
+ }
6672
+ }
6673
+ if (!url2) return { ...base, command: "help" };
6674
+ return { ...base, command: "edit", url: url2, lang: lang2 };
6675
+ }
6316
6676
  if (first === "capture") {
6317
6677
  const rest = args.slice(1);
6318
6678
  let url2;
@@ -6378,6 +6738,28 @@ async function main() {
6378
6738
  case "logout":
6379
6739
  await logoutCommand();
6380
6740
  break;
6741
+ case "auth":
6742
+ if (!parsed.url) {
6743
+ console.log(import_chalk21.default.red(" Missing URL"));
6744
+ console.log(import_chalk21.default.dim(" Run: qp auth <url>"));
6745
+ process.exit(1);
6746
+ }
6747
+ await authCommand(parsed.url);
6748
+ break;
6749
+ case "auth-clear":
6750
+ await authClearCommand(parsed.url);
6751
+ break;
6752
+ case "auth-list":
6753
+ await authListCommand();
6754
+ break;
6755
+ case "edit":
6756
+ if (!parsed.url) {
6757
+ console.log(import_chalk21.default.red(" Missing URL"));
6758
+ console.log(import_chalk21.default.dim(" Run: qp edit <url>"));
6759
+ process.exit(1);
6760
+ }
6761
+ await editCommand(parsed.url, parsed.lang);
6762
+ break;
6381
6763
  case "init":
6382
6764
  await initCommand();
6383
6765
  break;
@@ -6386,8 +6768,8 @@ async function main() {
6386
6768
  break;
6387
6769
  case "record":
6388
6770
  if (!parsed.url) {
6389
- console.log(import_chalk18.default.red(" Missing URL"));
6390
- console.log(import_chalk18.default.dim(" Run: qp --help"));
6771
+ console.log(import_chalk21.default.red(" Missing URL"));
6772
+ console.log(import_chalk21.default.dim(" Run: qp --help"));
6391
6773
  process.exit(1);
6392
6774
  }
6393
6775
  setAutoYes(parsed.yes);
@@ -6396,13 +6778,18 @@ async function main() {
6396
6778
  showLogo();
6397
6779
  await scriptCommand(parsed.url, parsed.scriptPath, parsed.lang);
6398
6780
  } else {
6399
- await recordCommand(parsed.url, parsed.lang, parsed.description, parsed.skipVoice, parsed.fresh);
6781
+ await recordCommand(parsed.url, {
6782
+ langOverride: parsed.lang,
6783
+ description: parsed.description,
6784
+ skipVoice: parsed.skipVoice,
6785
+ fresh: parsed.fresh
6786
+ });
6400
6787
  }
6401
6788
  break;
6402
6789
  case "capture":
6403
6790
  if (!parsed.url) {
6404
- console.log(import_chalk18.default.red(" Missing URL"));
6405
- console.log(import_chalk18.default.dim(" Run: qp --help"));
6791
+ console.log(import_chalk21.default.red(" Missing URL"));
6792
+ console.log(import_chalk21.default.dim(" Run: qp --help"));
6406
6793
  process.exit(1);
6407
6794
  }
6408
6795
  console.log();